From f8e09d1c5e2f449542598393343a691e9d50419c Mon Sep 17 00:00:00 2001 From: Oznogon Date: Mon, 2 Mar 2026 17:25:24 -0800 Subject: [PATCH 1/5] Add GuiScrollContainer Add GuiScrollContainer, a subclass of GuiContainer to support arbitrary and nested scrolling elements. This relies on changes in SeriousProton to implement GL_SCISSOR_TEST in RenderTarget. Child element positions and click/hover handling are translated relative to the scroll position. These containers can be nested, and mousewheel and scroll events are passed down the tree. This container element can also replace the bespoke scrolling behaviors in other element types, such as GuiListbox. - Pass focus, text input through GuiScrollContainer. - Position nested GuiSelector popups relative to scroll translation - Add scrollToOffset() function to allow other elements to control scroll position. - Handle layout padding in scissor rects. --- CMakeLists.txt | 2 + src/gui/gui2_container.cpp | 30 +++ src/gui/gui2_container.h | 15 +- src/gui/gui2_scrollcontainer.cpp | 399 +++++++++++++++++++++++++++++++ src/gui/gui2_scrollcontainer.h | 82 +++++++ src/gui/gui2_selector.cpp | 7 +- 6 files changed, 529 insertions(+), 6 deletions(-) create mode 100644 src/gui/gui2_scrollcontainer.cpp create mode 100644 src/gui/gui2_scrollcontainer.h diff --git a/CMakeLists.txt b/CMakeLists.txt index a86dd2be9f..64aa8beac0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -159,6 +159,7 @@ set(GUI_LIB_SOURCES src/gui/gui2_progressbar.cpp src/gui/gui2_progressslider.cpp src/gui/gui2_scrolltext.cpp + src/gui/gui2_scrollcontainer.cpp src/gui/gui2_advancedscrolltext.cpp src/gui/gui2_button.cpp src/gui/gui2_resizabledialog.cpp @@ -196,6 +197,7 @@ set(GUI_LIB_SOURCES src/gui/gui2_resizabledialog.h src/gui/gui2_rotationdial.h src/gui/gui2_scrollbar.h + src/gui/gui2_scrollcontainer.h src/gui/gui2_scrolltext.h src/gui/gui2_selector.h src/gui/gui2_slider.h diff --git a/src/gui/gui2_container.cpp b/src/gui/gui2_container.cpp index 9c87721289..ab4f2049b5 100644 --- a/src/gui/gui2_container.cpp +++ b/src/gui/gui2_container.cpp @@ -132,6 +132,36 @@ void GuiContainer::updateLayout(const sp::Rect& rect) } } +void GuiContainer::clearElementOwner(GuiElement* e) +{ + e->owner = nullptr; +} + +void GuiContainer::setElementHover(GuiElement* e, bool h) +{ + e->hover = h; +} + +void GuiContainer::setElementFocus(GuiElement* e, bool f) +{ + e->focus = f; +} + +void GuiContainer::callDrawElements(GuiContainer* c, glm::vec2 mp, sp::Rect r, sp::RenderTarget& rt) +{ + c->drawElements(mp, r, rt); +} + +GuiElement* GuiContainer::callGetClickElement(GuiContainer* c, sp::io::Pointer::Button b, glm::vec2 p, sp::io::Pointer::ID id) +{ + return c->getClickElement(b, p, id); +} + +GuiElement* GuiContainer::callExecuteScrollOnElement(GuiContainer* c, glm::vec2 p, float v) +{ + return c->executeScrollOnElement(p, v); +} + void GuiContainer::setAttribute(const string& key, const string& value) { if (key == "size") diff --git a/src/gui/gui2_container.h b/src/gui/gui2_container.h index 547aabb80c..c4a44f7203 100644 --- a/src/gui/gui2_container.h +++ b/src/gui/gui2_container.h @@ -54,20 +54,27 @@ class GuiContainer : sp::NonCopyable virtual ~GuiContainer(); template void setLayout() { layout_manager = std::make_unique(); } - void updateLayout(const sp::Rect& rect); + virtual void updateLayout(const sp::Rect& rect); const sp::Rect& getRect() const { return rect; } virtual void setAttribute(const string& key, const string& value); protected: virtual void drawElements(glm::vec2 mouse_position, sp::Rect parent_rect, sp::RenderTarget& window); virtual void drawDebugElements(sp::Rect parent_rect, sp::RenderTarget& window); - GuiElement* getClickElement(sp::io::Pointer::Button button, glm::vec2 position, sp::io::Pointer::ID id); - GuiElement* executeScrollOnElement(glm::vec2 position, float value); + virtual GuiElement* getClickElement(sp::io::Pointer::Button button, glm::vec2 position, sp::io::Pointer::ID id); + virtual GuiElement* executeScrollOnElement(glm::vec2 position, float value); + + // Static helpers for subclass access to protected members. + static void clearElementOwner(GuiElement* element); + static void setElementHover(GuiElement* element, bool has_hover); + static void setElementFocus(GuiElement* element, bool has_focus); + static void callDrawElements(GuiContainer* container, glm::vec2 mouse_pos, sp::Rect rect, sp::RenderTarget& render_target); + static GuiElement* callGetClickElement(GuiContainer* container, sp::io::Pointer::Button button, glm::vec2 pos, sp::io::Pointer::ID id); + static GuiElement* callExecuteScrollOnElement(GuiContainer* container, glm::vec2 pos, float value); friend class GuiElement; sp::Rect rect{0,0,0,0}; -private: std::unique_ptr layout_manager = nullptr; }; diff --git a/src/gui/gui2_scrollcontainer.cpp b/src/gui/gui2_scrollcontainer.cpp new file mode 100644 index 0000000000..24e07b36d0 --- /dev/null +++ b/src/gui/gui2_scrollcontainer.cpp @@ -0,0 +1,399 @@ +#include "gui2_scrollcontainer.h" +#include "gui2_scrollbar.h" +#include "gui2_canvas.h" +#include "gui/layout/layout.h" + + +GuiScrollContainer::GuiScrollContainer(GuiContainer* owner, const string& id, ScrollMode mode) +: GuiElement(owner, id), mode(mode) +{ + // We need to manipulate layout size to hide/show the scrollbar. + layout.match_content_size = false; + + // Add a vertical scrollbar only if this element scrolls or pages. + if (mode == ScrollMode::Scroll || mode == ScrollMode::Page) + { + scrollbar_v = new GuiScrollbar(this, id + "_SCROLLBAR_V", 0, 100, 0, + [this](int value) + { + scroll_offset = static_cast(value); + } + ); + scrollbar_v + ->setPosition(0.0f, 0.0f, sp::Alignment::TopRight) + ->setSize(scrollbar_width, GuiSizeMax); + } +} + +GuiScrollContainer* GuiScrollContainer::setMode(ScrollMode new_mode) +{ + mode = new_mode; + return this; +} + +GuiScrollContainer* GuiScrollContainer::setScrollbarWidth(float width) +{ + scrollbar_width = width; + return this; +} + +void GuiScrollContainer::scrollToFraction(float fraction) +{ + const float max_scroll = std::max(0.0f, content_height - visible_height); + scroll_offset = std::clamp(fraction * max_scroll, 0.0f, max_scroll); + if (scrollbar_v) scrollbar_v->setValue(static_cast(scroll_offset)); +} + +void GuiScrollContainer::scrollToOffset(float pixel_offset) +{ + const float max_scroll = std::max(0.0f, content_height - visible_height); + scroll_offset = std::clamp(pixel_offset, 0.0f, max_scroll); + if (scrollbar_v) scrollbar_v->setValue(static_cast(scroll_offset)); +} + +void GuiScrollContainer::updateLayout(const sp::Rect& rect) +{ + this->rect = rect; + visible_height = rect.size.y - layout.padding.top - layout.padding.bottom; + + // Show the scrollbar only if we're clipping anything. + scrollbar_visible = (scrollbar_v != nullptr) && (content_height > visible_height + 0.5f); + // Don't factor scrollbar width if it isn't visible. + const float sb_width = scrollbar_visible ? scrollbar_width : 0.0f; + + // Manually factor padding into content layout around the scrollbar. + glm::vec2 padding_offset{ + layout.padding.left, + layout.padding.top + }; + + glm::vec2 padding_size{ + layout.padding.left + layout.padding.right, + layout.padding.top + layout.padding.bottom + }; + + sp::Rect content_layout_rect{ + rect.position + padding_offset, + rect.size - padding_size - glm::vec2{sb_width, 0.0f} + }; + + if (!layout_manager) layout_manager = std::make_unique(); + + // Temporarily hide the scrollbar so the layout manager ignores it for + // sizing, then restore it if enabled. + if (scrollbar_v) scrollbar_v->setVisible(false); + + layout_manager->updateLoop(*this, content_layout_rect); + + if (scrollbar_v) + { + scrollbar_v->setVisible(scrollbar_visible); + + // Override the scrollbar rect. + scrollbar_v->updateLayout({ + {rect.position.x + rect.size.x - scrollbar_width, rect.position.y}, + {scrollbar_width, rect.size.y} + }); + } + + // Compute content_height from non-scrollbar visible children. + float max_bottom = 0.0f; + for (GuiElement* child : children) + { + if (child == scrollbar_v) continue; + if (!child->isVisible()) continue; + + const float bottom = child->getRect().position.y + child->getRect().size.y + child->layout.margin.bottom - rect.position.y; + if (bottom > max_bottom) max_bottom = bottom; + } + content_height = max_bottom + layout.padding.bottom; + + // Clamp scroll offset. + scroll_offset = std::clamp(scroll_offset, 0.0f, std::max(0.0f, content_height - visible_height)); + + // Sync scrollbar properties to new layout. + if (scrollbar_v) + { + scrollbar_v->setRange(0, static_cast(content_height)); + scrollbar_v->setValueSize(static_cast(visible_height)); + scrollbar_v->setValue(static_cast(scroll_offset)); + } +} + +void GuiScrollContainer::drawElements(glm::vec2 mouse_position, sp::Rect /* parent_rect */, sp::RenderTarget& renderer) +{ + sp::Rect content_rect = getContentRect(); + + // Capture clipping and scroll translation. + renderer.pushScissorRect(content_rect); + renderer.pushTranslation({0.0f, -scroll_offset}); + + // Track mouse position on element relative to the vertical scroll offset. + glm::vec2 layout_mouse = mouse_position + glm::vec2{0.0f, scroll_offset}; + + // Pass the relative mouse position through to each child element. + for (auto it = children.begin(); it != children.end(); ) + { + GuiElement* element = *it; + + if (element == scrollbar_v) + { + ++it; + continue; + } + + if (element->isDestroyed()) + { + GuiCanvas* canvas = dynamic_cast(element->getTopLevelContainer()); + if (canvas) canvas->unfocusElementTree(element); + + it = children.erase(it); + clearElementOwner(element); + delete element; + + continue; + } + + setElementHover(element, element->getRect().contains(layout_mouse)); + + if (element->isVisible()) + { + element->onDraw(renderer); + callDrawElements(element, layout_mouse, element->getRect(), renderer); + } + + ++it; + } + + // Apply scroll translation and clipping. Order matters here. + renderer.popTranslation(); + renderer.popScissorRect(); + + // Draw the scrollbar. Never clip nor scroll the scrollbar itself. + if (scrollbar_v + && !scrollbar_v->isDestroyed() + && scrollbar_v->isVisible() + ) + { + setElementHover(scrollbar_v, scrollbar_v->getRect().contains(mouse_position)); + scrollbar_v->onDraw(renderer); + callDrawElements(scrollbar_v, mouse_position, scrollbar_v->getRect(), renderer); + } +} + +GuiElement* GuiScrollContainer::getClickElement(sp::io::Pointer::Button button, glm::vec2 position, sp::io::Pointer::ID id) +{ + // Pass the click to the scrollbar first, and don't translate its position. + if (scrollbar_v + && scrollbar_v->isVisible() + && scrollbar_v->isEnabled() + && scrollbar_v->getRect().contains(position) + ) + { + GuiElement* clicked = callGetClickElement(scrollbar_v, button, position, id); + if (clicked) return clicked; + if (scrollbar_v->onMouseDown(button, position, id)) return scrollbar_v; + } + + // Don't pass clicks to elements outside of the content rect. + if (!getContentRect().contains(position)) return nullptr; + + // Pass the click to each nested child, which should take priority if it can + // use it. + glm::vec2 layout_pos = position + glm::vec2{0.0f, scroll_offset}; + + for (auto it = children.rbegin(); it != children.rend(); ++it) + { + GuiElement* element = *it; + + // We already handled the scrollbar. + if (element == scrollbar_v) continue; + // We don't care about buttons that aren't visible or enabled. + if (!element->isVisible() || !element->isEnabled()) continue; + + // Figure out if we can click the element. If so, capture the scroll + // offset to pass to drag events, focus it, and click it. + GuiElement* clicked = callGetClickElement(element, button, layout_pos, id); + if (clicked) + { + switchFocusTo(clicked); + pressed_element = clicked; + pressed_scroll = scroll_offset; + return this; + } + + // The click didn't fire, but we still recurse into children regardless. + // This helps find children or child-like elements (like GuiSelector + // popups) that can exist outside of their parent's rect. + if (element->getRect().contains(layout_pos) && element->onMouseDown(button, layout_pos, id)) + { + switchFocusTo(element); + pressed_element = element; + pressed_scroll = scroll_offset; + return this; + } + } + + // Otherwise, do nothing. + return nullptr; +} + +void GuiScrollContainer::switchFocusTo(GuiElement* new_element) +{ + // Apply focus change, if any. + if (focused_element == new_element) return; + + if (focused_element) + { + setElementFocus(focused_element, false); + focused_element->onFocusLost(); + } + + focused_element = new_element; + + // If this scroll container already has canvas focus, forward focus gained + // to the new child now (GuiCanvas won't call our onFocusGained again). + // If this scroll container is not yet focused, canvas will call our + // onFocusGained after getClickElement returns, which will forward it. + if (focus) + { + setElementFocus(focused_element, true); + focused_element->onFocusGained(); + } +} + +void GuiScrollContainer::onFocusGained() +{ + if (focused_element) + { + setElementFocus(focused_element, true); + focused_element->onFocusGained(); + } +} + +void GuiScrollContainer::onFocusLost() +{ + if (focused_element) + { + setElementFocus(focused_element, false); + focused_element->onFocusLost(); + focused_element = nullptr; + } +} + +void GuiScrollContainer::onTextInput(const string& text) +{ + if (focused_element) focused_element->onTextInput(text); +} + +void GuiScrollContainer::onTextInput(sp::TextInputEvent e) +{ + if (focused_element) focused_element->onTextInput(e); +} + +bool GuiScrollContainer::onMouseDown(sp::io::Pointer::Button button, glm::vec2 position, sp::io::Pointer::ID id) +{ + if (pressed_element) + { + pressed_element->onMouseDown(button, position + glm::vec2{0.0f, pressed_scroll}, id); + pressed_element = nullptr; + return true; + } + + return false; +} + +void GuiScrollContainer::onMouseDrag(glm::vec2 position, sp::io::Pointer::ID id) +{ + if (pressed_element) pressed_element->onMouseDrag(position + glm::vec2{0.0f, pressed_scroll}, id); +} + +void GuiScrollContainer::onMouseUp(glm::vec2 position, sp::io::Pointer::ID id) +{ + if (pressed_element) + { + pressed_element->onMouseUp(position + glm::vec2{0.0f, pressed_scroll}, id); + pressed_element = nullptr; + } +} + +GuiElement* GuiScrollContainer::executeScrollOnElement(glm::vec2 position, float value) +{ + // Pass the scroll to the scrollbar first, and don't translate its position. + if (scrollbar_v + && scrollbar_v->isVisible() + && scrollbar_v->isEnabled() + && scrollbar_v->getRect().contains(position)) + { + GuiElement* scrolled = callExecuteScrollOnElement(scrollbar_v, position, value); + if (scrolled) return scrolled; + // Handle mousewheel scroll, if any. + if (scrollbar_v->onMouseWheelScroll(position, value)) return scrollbar_v; + } + + // Return nothing if the scroll isn't within the container. + if (!getContentRect().contains(position)) return nullptr; + + // Execute the scroll on each nested child. If a child can use the mousewheel + // scroll event, give it to them. + glm::vec2 layout_pos = position + glm::vec2{0.0f, scroll_offset}; + + for (auto it = children.rbegin(); it != children.rend(); ++it) + { + GuiElement* element = *it; + if (element == scrollbar_v) continue; + + if (element + && element->isVisible() + && element->isEnabled() + && element->getRect().contains(layout_pos) + ) + { + GuiElement* scrolled = callExecuteScrollOnElement(element, layout_pos, value); + if (scrolled) return scrolled; + if (element->onMouseWheelScroll(layout_pos, value)) return element; + } + } + + // No child used the mousewheel scroll event, so use it to scroll the + // container. + if (onMouseWheelScroll(position, value)) return this; + + // Otherwise, nothing happens. + return nullptr; +} + +bool GuiScrollContainer::onMouseWheelScroll(glm::vec2 /* position */, float value) +{ + // Don't scroll if used only to clip. + if (mode == ScrollMode::None) return false; + + // Scroll by a default interval of 50, or by the container height if set to + // paged mode. + const float step = (mode == ScrollMode::Page) ? visible_height : 50.0f; + const float max_scroll = std::max(0.0f, content_height - visible_height); + scroll_offset = std::clamp(scroll_offset - value * step, 0.0f, max_scroll); + + // Update the scrollbar if it exists. + if (scrollbar_v) scrollbar_v->setValue(static_cast(scroll_offset)); + + return true; +} + +sp::Rect GuiScrollContainer::getContentRect() const +{ + // Return the rect, inset by padding and minus room for the scrollbar if it's visible. + return sp::Rect{ + rect.position + glm::vec2{layout.padding.left, layout.padding.top}, + { + rect.size.x - layout.padding.left - layout.padding.right - getEffectiveScrollbarWidth(), + rect.size.y - layout.padding.top - layout.padding.bottom + } + }; +} + +float GuiScrollContainer::getEffectiveScrollbarWidth() const +{ + // Save room for the scrollbar only if it's visible. + return (scrollbar_v && scrollbar_visible) ? scrollbar_width : 0.0f; +} diff --git a/src/gui/gui2_scrollcontainer.h b/src/gui/gui2_scrollcontainer.h new file mode 100644 index 0000000000..5185a444d5 --- /dev/null +++ b/src/gui/gui2_scrollcontainer.h @@ -0,0 +1,82 @@ +#pragma once + +#include "gui2_element.h" + +class GuiScrollbar; + +class GuiScrollContainer : public GuiElement +{ +public: + enum class ScrollMode { + None, // Cut overflow off at element borders; no scrolling + Scroll, // Scroll by fixed increments, regardless of contents or element size + Page // Scroll by increments equal to the element size + }; + + // GuiContainer-like GuiElement with support for clipping or scrolling + // arbitrary child elements that overflow its bounds. + GuiScrollContainer(GuiContainer* owner, const string& id, ScrollMode mode = ScrollMode::Scroll); + + // TODO: Right now this clips both horizontally and vertically, but supports + // only vertical scrolling/paging. + + // Set scrolling mode. All modes clip at the element boundaries. + GuiScrollContainer* setMode(ScrollMode mode); + // Set width of scrollbar if visible. + GuiScrollContainer* setScrollbarWidth(float width); + // Scroll element to this fraction of the total scrollbar limit. + // Value passed here represents where the top of the scrollbar pill goes + // on the scrollbar. + void scrollToFraction(float fraction); + // Scroll element to this pixel offset from the top (clamped to valid range). + void scrollToOffset(float pixel_offset); + + // Override layout updates to update child elements and juggle scrollbar + // visibility. + virtual void updateLayout(const sp::Rect& rect) override; + // Handle mousewheel scroll, with behavior depending on the ScrollMode. + virtual bool onMouseWheelScroll(glm::vec2 position, float value) override; + // Pass mouse down to child elements, but only if they're visible. + virtual bool onMouseDown(sp::io::Pointer::Button button, glm::vec2 position, sp::io::Pointer::ID id) override; + // Pass mouse drag to child elements. This relies on + virtual void onMouseDrag(glm::vec2 position, sp::io::Pointer::ID id) override; + // Pass mouse up to child elements. + virtual void onMouseUp(glm::vec2 position, sp::io::Pointer::ID id) override; + // Pass focus to child elements. + virtual void onFocusGained() override; + // Pass focus loss to child elements. + virtual void onFocusLost() override; + // Pass text input events to child elements. + virtual void onTextInput(const string& text) override; + // Pass text input events to child elements. + virtual void onTextInput(sp::TextInputEvent e) override; + +protected: + // Draw elements if they're in view. Translate mouse positions by the scroll + // amount. + virtual void drawElements(glm::vec2 mouse_position, sp::Rect parent_rect, sp::RenderTarget& renderer) override; + // Find the clicked element, checking children of this container if they're + // visible. + virtual GuiElement* getClickElement(sp::io::Pointer::Button button, glm::vec2 position, sp::io::Pointer::ID id) override; + // Scroll the element's children. Pass any mousewheel events to children + // first if they can use it. + virtual GuiElement* executeScrollOnElement(glm::vec2 position, float value) override; + +private: + ScrollMode mode; + float scrollbar_width = 30.0f; + GuiScrollbar* scrollbar_v = nullptr; + + float scroll_offset = 0.0f; + float content_height = 0.0f; + float visible_height = 0.0f; + bool scrollbar_visible = false; + + GuiElement* focused_element = nullptr; + GuiElement* pressed_element = nullptr; + float pressed_scroll = 0.0f; + + sp::Rect getContentRect() const; + float getEffectiveScrollbarWidth() const; + void switchFocusTo(GuiElement* new_element); +}; diff --git a/src/gui/gui2_selector.cpp b/src/gui/gui2_selector.cpp index 72a3fb9967..60dc9cf3d2 100644 --- a/src/gui/gui2_selector.cpp +++ b/src/gui/gui2_selector.cpp @@ -49,13 +49,16 @@ void GuiSelector::onDraw(sp::RenderTarget& renderer) if (!focus) popup->hide(); - float top = rect.position.y; + // rect.position is in layout space; the popup lives at the canvas level + // (no scroll translation), so convert to screen coordinates first. + glm::vec2 screen_pos = rect.position + renderer.getTranslation(); + float top = screen_pos.y; float height = entries.size() * 50; if (selection_index >= 0) top -= selection_index * 50; top = std::max(0.0f, top); top = std::min(900.0f - height, top); - popup->setPosition(rect.position.x, top, sp::Alignment::TopLeft)->setSize(rect.size.x, height); + popup->setPosition(screen_pos.x, top, sp::Alignment::TopLeft)->setSize(rect.size.x, height); } GuiSelector* GuiSelector::setTextSize(float size) From 117dd68a32a0a8398c5ef54b649fc6029eff77ac Mon Sep 17 00:00:00 2001 From: Oznogon Date: Mon, 9 Mar 2026 14:19:16 -0700 Subject: [PATCH 2/5] Edit GuiContainer formatting/style - Use pragma once guard - Internal consistency in formatting - Expand terse varnames - Remove redundant public/protected sections in the header --- src/gui/gui2_container.cpp | 83 +++++++++++++++++--------------------- src/gui/gui2_container.h | 42 ++++++++++--------- 2 files changed, 60 insertions(+), 65 deletions(-) diff --git a/src/gui/gui2_container.cpp b/src/gui/gui2_container.cpp index ab4f2049b5..b6dcdbcc7c 100644 --- a/src/gui/gui2_container.cpp +++ b/src/gui/gui2_container.cpp @@ -4,7 +4,7 @@ GuiContainer::~GuiContainer() { - for(GuiElement* element : children) + for (GuiElement* element : children) { element->owner = nullptr; delete element; @@ -13,15 +13,14 @@ GuiContainer::~GuiContainer() void GuiContainer::drawElements(glm::vec2 mouse_position, sp::Rect parent_rect, sp::RenderTarget& renderer) { - for(auto it = children.begin(); it != children.end(); ) + for (auto it = children.begin(); it != children.end(); ) { GuiElement* element = *it; if (element->destroyed) { //Find the owning cancas, as we need to remove ourselves if we are the focus or click element. GuiCanvas* canvas = dynamic_cast(element->getTopLevelContainer()); - if (canvas) - canvas->unfocusElementTree(element); + if (canvas) canvas->unfocusElementTree(element); //Delete it from our list. it = children.erase(it); @@ -29,7 +28,9 @@ void GuiContainer::drawElements(glm::vec2 mouse_position, sp::Rect parent_rect, // Free up the memory used by the element. element->owner = nullptr; delete element; - }else{ + } + else + { element->hover = element->rect.contains(mouse_position); if (element->visible) @@ -61,49 +62,45 @@ void GuiContainer::drawDebugElements(sp::Rect parent_rect, sp::RenderTarget& ren GuiElement* GuiContainer::getClickElement(sp::io::Pointer::Button button, glm::vec2 position, sp::io::Pointer::ID id) { - for(auto it = children.rbegin(); it != children.rend(); it++) + for (auto it = children.rbegin(); it != children.rend(); it++) { GuiElement* element = *it; if (element->visible && element->enabled && element->rect.contains(position)) { GuiElement* clicked = element->getClickElement(button, position, id); - if (clicked) - return clicked; - if (element->onMouseDown(button, position, id)) - { - return element; - } + if (clicked) return clicked; + if (element->onMouseDown(button, position, id)) return element; } } + return nullptr; } GuiElement* GuiContainer::executeScrollOnElement(glm::vec2 position, float value) { - for(auto it = children.rbegin(); it != children.rend(); it++) + for (auto it = children.rbegin(); it != children.rend(); it++) { GuiElement* element = *it; if (element->visible && element->enabled && element->rect.contains(position)) { GuiElement* scrolled = element->executeScrollOnElement(position, value); - if (scrolled) - return scrolled; - if (element->onMouseWheelScroll(position, value)) - return element; + if (scrolled) return scrolled; + if (element->onMouseWheelScroll(position, value)) return element; } } + return nullptr; } void GuiContainer::updateLayout(const sp::Rect& rect) { this->rect = rect; + if (layout_manager || !children.empty()) { - if (!layout_manager) - layout_manager = std::make_unique(); + if (!layout_manager) layout_manager = std::make_unique(); glm::vec2 padding_size(layout.padding.left + layout.padding.right, layout.padding.top + layout.padding.bottom); layout_manager->updateLoop(*this, sp::Rect(rect.position + glm::vec2{layout.padding.left, layout.padding.top}, rect.size - padding_size)); @@ -111,7 +108,8 @@ void GuiContainer::updateLayout(const sp::Rect& rect) { glm::vec2 content_size_min(std::numeric_limits::max(), std::numeric_limits::max()); glm::vec2 content_size_max(std::numeric_limits::min(), std::numeric_limits::min()); - for(auto w : children) + + for (auto w : children) { if (w && w->isVisible()) { @@ -123,6 +121,7 @@ void GuiContainer::updateLayout(const sp::Rect& rect) content_size_max.y = std::max(content_size_max.y, p1.y + w->layout.margin.bottom); } } + if (content_size_max.x != std::numeric_limits::min()) { this->rect.size = (content_size_max - content_size_min) + padding_size; @@ -132,34 +131,34 @@ void GuiContainer::updateLayout(const sp::Rect& rect) } } -void GuiContainer::clearElementOwner(GuiElement* e) +void GuiContainer::clearElementOwner(GuiElement* element) { - e->owner = nullptr; + element->owner = nullptr; } -void GuiContainer::setElementHover(GuiElement* e, bool h) +void GuiContainer::setElementHover(GuiElement* element, bool has_hover) { - e->hover = h; + element->hover = has_hover; } -void GuiContainer::setElementFocus(GuiElement* e, bool f) +void GuiContainer::setElementFocus(GuiElement* element, bool has_focus) { - e->focus = f; + element->focus = has_focus; } -void GuiContainer::callDrawElements(GuiContainer* c, glm::vec2 mp, sp::Rect r, sp::RenderTarget& rt) +void GuiContainer::callDrawElements(GuiContainer* container, glm::vec2 mouse_pos, sp::Rect rect, sp::RenderTarget& render_target) { - c->drawElements(mp, r, rt); + container->drawElements(mouse_pos, rect, render_target); } -GuiElement* GuiContainer::callGetClickElement(GuiContainer* c, sp::io::Pointer::Button b, glm::vec2 p, sp::io::Pointer::ID id) +GuiElement* GuiContainer::callGetClickElement(GuiContainer* container, sp::io::Pointer::Button button, glm::vec2 pos, sp::io::Pointer::ID id) { - return c->getClickElement(b, p, id); + return container->getClickElement(button, pos, id); } -GuiElement* GuiContainer::callExecuteScrollOnElement(GuiContainer* c, glm::vec2 p, float v) +GuiElement* GuiContainer::callExecuteScrollOnElement(GuiContainer* container, glm::vec2 pos, float value) { - return c->executeScrollOnElement(p, v); + return container->executeScrollOnElement(pos, value); } void GuiContainer::setAttribute(const string& key, const string& value) @@ -217,9 +216,7 @@ void GuiContainer::setAttribute(const string& key, const string& value) { auto values = value.split(",", 3); if (values.size() == 1) - { - layout.padding.top = layout.padding.bottom = layout.padding.left = layout.padding.right = values[0].strip().toFloat(); - } + layout.padding.top = layout.padding.bottom = layout.padding.left = layout.padding.right = values[0].strip().toFloat(); else if (values.size() == 2) { layout.padding.left = layout.padding.right = values[0].strip().toFloat(); @@ -262,17 +259,14 @@ void GuiContainer::setAttribute(const string& key, const string& value) else if (key == "layout") { GuiLayoutClassRegistry* reg; - for(reg = GuiLayoutClassRegistry::first; reg != nullptr; reg = reg->next) - { - if (value == reg->name) - break; - } + + for (reg = GuiLayoutClassRegistry::first; reg != nullptr; reg = reg->next) + if (value == reg->name) break; + if (reg) - { layout_manager = reg->creation_function(); - }else{ + else LOG(Error, "Failed to find layout type:", value); - } } else if (key == "stretch") { @@ -280,6 +274,7 @@ void GuiContainer::setAttribute(const string& key, const string& value) layout.fill_height = layout.fill_width = layout.lock_aspect_ratio = true; else layout.fill_height = layout.fill_width = value.toBool(); + layout.match_content_size = false; } else if (key == "fill_height") @@ -293,7 +288,5 @@ void GuiContainer::setAttribute(const string& key, const string& value) layout.match_content_size = false; } else - { LOG(Warning, "Tried to set unknown widget attribute:", key, "to", value); - } } diff --git a/src/gui/gui2_container.h b/src/gui/gui2_container.h index c4a44f7203..a13799df06 100644 --- a/src/gui/gui2_container.h +++ b/src/gui/gui2_container.h @@ -1,5 +1,4 @@ -#ifndef GUI2_CONTAINER_H -#define GUI2_CONTAINER_H +#pragma once #include #include @@ -17,25 +16,26 @@ namespace sp { class GuiElement; class GuiLayout; class GuiTheme; + class GuiContainer : sp::NonCopyable { public: -public: + // Nested type to capture layout attributes class LayoutInfo { public: class Sides { public: - float left = 0; - float right = 0; - float top = 0; - float bottom = 0; + float left = 0.0f; + float right = 0.0f; + float top = 0.0f; + float bottom = 0.0f; }; - glm::vec2 position{0, 0}; + glm::vec2 position{0.0f, 0.0f}; sp::Alignment alignment = sp::Alignment::TopLeft; - glm::vec2 size{1, 1}; + glm::vec2 size{1.0f, 1.0f}; glm::ivec2 span{1, 1}; Sides margin; Sides padding; @@ -45,20 +45,27 @@ class GuiContainer : sp::NonCopyable bool match_content_size = true; }; - LayoutInfo layout; - std::list children; -protected: - GuiTheme* theme; -public: GuiContainer() = default; virtual ~GuiContainer(); + // Public data + LayoutInfo layout; + std::list children; + + // Public interfaces template void setLayout() { layout_manager = std::make_unique(); } virtual void updateLayout(const sp::Rect& rect); + virtual void setAttribute(const string& key, const string& value); const sp::Rect& getRect() const { return rect; } - virtual void setAttribute(const string& key, const string& value); protected: + GuiTheme* theme; + + // Protected data + sp::Rect rect{0,0,0,0}; + std::unique_ptr layout_manager = nullptr; + + // Protected interfaces virtual void drawElements(glm::vec2 mouse_position, sp::Rect parent_rect, sp::RenderTarget& window); virtual void drawDebugElements(sp::Rect parent_rect, sp::RenderTarget& window); virtual GuiElement* getClickElement(sp::io::Pointer::Button button, glm::vec2 position, sp::io::Pointer::ID id); @@ -73,9 +80,4 @@ class GuiContainer : sp::NonCopyable static GuiElement* callExecuteScrollOnElement(GuiContainer* container, glm::vec2 pos, float value); friend class GuiElement; - - sp::Rect rect{0,0,0,0}; - std::unique_ptr layout_manager = nullptr; }; - -#endif//GUI2_CONTAINER_H From 8b62ab9643b15c676c95e671066bdde41836598d Mon Sep 17 00:00:00 2001 From: oznogon Date: Wed, 11 Mar 2026 22:26:37 -0700 Subject: [PATCH 3/5] Set default GuiScrollContainer click_change to 50 Increase the default scrollbar click_change on GuiScrollContainer to 50, matching mousewheel scroll increments. --- src/gui/gui2_scrollcontainer.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/gui/gui2_scrollcontainer.cpp b/src/gui/gui2_scrollcontainer.cpp index 24e07b36d0..03a1e09c7a 100644 --- a/src/gui/gui2_scrollcontainer.cpp +++ b/src/gui/gui2_scrollcontainer.cpp @@ -19,6 +19,7 @@ GuiScrollContainer::GuiScrollContainer(GuiContainer* owner, const string& id, Sc scroll_offset = static_cast(value); } ); + scrollbar_v->setClickChange(50); scrollbar_v ->setPosition(0.0f, 0.0f, sp::Alignment::TopRight) ->setSize(scrollbar_width, GuiSizeMax); From 40c55218ae63d35116dfea1a28f05bfb4f4533fb Mon Sep 17 00:00:00 2001 From: Oznogon Date: Tue, 10 Mar 2026 11:21:32 -0700 Subject: [PATCH 4/5] Allow sizing of GuiButton icons --- src/gui/gui2_button.cpp | 8 +++++++- src/gui/gui2_button.h | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/gui/gui2_button.cpp b/src/gui/gui2_button.cpp index ab6fb2ce38..e2da6b3782 100644 --- a/src/gui/gui2_button.cpp +++ b/src/gui/gui2_button.cpp @@ -40,7 +40,7 @@ void GuiButton::onDraw(sp::RenderTarget& renderer) text_rect.size.x = rect.size.x - rect.size.y; text_align = sp::Alignment::CenterRight; } - renderer.drawRotatedSprite(icon_name, glm::vec2(icon_x, rect.position.y + rect.size.y * 0.5f), rect.size.y * 0.8f, icon_rotation, front.color); + renderer.drawRotatedSprite(icon_name, glm::vec2(icon_x, rect.position.y + rect.size.y * 0.5f), rect.size.y * icon_size, icon_rotation, front.color); renderer.drawText(text_rect, text, text_align, text_size > 0 ? text_size : front.size, front.font, front.color); }else{ renderer.drawText(rect, text, sp::Alignment::Center, text_size > 0 ? text_size : front.size, front.font, front.color); @@ -90,6 +90,12 @@ GuiButton* GuiButton::setIcon(string icon_name, sp::Alignment icon_alignment, fl return this; } +GuiButton* GuiButton::setIconSize(float size) +{ + icon_size = size; + return this; +} + GuiButton* GuiButton::setStyle(const string& style) { back_style = theme->getStyle(style + ".back"); diff --git a/src/gui/gui2_button.h b/src/gui/gui2_button.h index 3ddcfc591d..a445563696 100644 --- a/src/gui/gui2_button.h +++ b/src/gui/gui2_button.h @@ -17,6 +17,7 @@ class GuiButton : public GuiElement string icon_name; sp::Alignment icon_alignment; float icon_rotation; + float icon_size = 0.8f; const GuiThemeStyle* back_style; const GuiThemeStyle* front_style; public: @@ -29,6 +30,7 @@ class GuiButton : public GuiElement GuiButton* setText(string text); GuiButton* setTextSize(float size); GuiButton* setIcon(string icon_name, sp::Alignment icon_alignment = sp::Alignment::CenterLeft, float rotation = 0); + GuiButton* setIconSize(float size); GuiButton* setStyle(const string& style); string getText() const; string getIcon() const; From 116b9d82e9d37ddbcb9e5b7300262d2fbfd18fff Mon Sep 17 00:00:00 2001 From: Oznogon Date: Tue, 10 Mar 2026 11:21:50 -0700 Subject: [PATCH 5/5] Refactor GuiListbox - Replace bespoke button rendering and audio management with GuiToggleButtons. This aligns audio sfx with GuiButton, and aligns icon and text positioning in listboxes that use icons with GuiButton. - Replace bespoke theme application with GuiButton->setStyle(). - Replace bespoke clipping/scrolling implementation with GuiScrollContainer. - Document functions. --- resources/gui/default.theme.txt | 4 +- src/gui/gui2_container.h | 2 +- src/gui/gui2_listbox.cpp | 164 +++++++++++--------------------- src/gui/gui2_listbox.h | 44 +++++---- src/gui/gui2_togglebutton.cpp | 2 +- 5 files changed, 85 insertions(+), 131 deletions(-) diff --git a/resources/gui/default.theme.txt b/resources/gui/default.theme.txt index 09fbe5c495..f207bd0f53 100644 --- a/resources/gui/default.theme.txt +++ b/resources/gui/default.theme.txt @@ -145,7 +145,7 @@ image: gui/widget/ButtonBackground.png image.hover: gui/widget/ButtonBackground.hover.png image.disabled: gui/widget/ButtonBackground.disabled.png - + [listbox.selected.back] { image: gui/widget/ButtonBackground.active.png image.hover: gui/widget/ButtonBackground.active.png @@ -160,7 +160,7 @@ } } } - + [arrow] { image: gui/widget/IndicatorArrow.png } diff --git a/src/gui/gui2_container.h b/src/gui/gui2_container.h index a13799df06..aea291e249 100644 --- a/src/gui/gui2_container.h +++ b/src/gui/gui2_container.h @@ -71,7 +71,7 @@ class GuiContainer : sp::NonCopyable virtual GuiElement* getClickElement(sp::io::Pointer::Button button, glm::vec2 position, sp::io::Pointer::ID id); virtual GuiElement* executeScrollOnElement(glm::vec2 position, float value); - // Static helpers for subclass access to protected members. + // Access GuiElement/GuiContainer protected members in subclass static void clearElementOwner(GuiElement* element); static void setElementHover(GuiElement* element, bool has_hover); static void setElementFocus(GuiElement* element, bool has_focus); diff --git a/src/gui/gui2_listbox.cpp b/src/gui/gui2_listbox.cpp index de37e62b36..26bb78027e 100644 --- a/src/gui/gui2_listbox.cpp +++ b/src/gui/gui2_listbox.cpp @@ -1,141 +1,85 @@ #include "gui2_listbox.h" -#include "soundManager.h" -#include "theme.h" - -#include "gui2_scrollbar.h" +#include "gui2_scrollcontainer.h" +#include "gui2_togglebutton.h" GuiListbox::GuiListbox(GuiContainer* owner, string id, func_t func) -: GuiEntryList(owner, id, func), text_size(30), button_height(50), text_alignment(sp::Alignment::Center), mouse_scroll_steps(25) +: GuiEntryList(owner, id, func) { - scroll = new GuiScrollbar(this, id + "_SCROLL", 0, 0, 0, [this](int value) {}); - scroll->setPosition(0, 0, sp::Alignment::TopRight)->setSize(button_height, GuiSizeMax)->hide(); - scroll->setClickChange(button_height); - - back_style = theme->getStyle("listbox.back"); - front_style = theme->getStyle("listbox.front"); - back_selected_style = theme->getStyle("listbox.selected.back"); - front_selected_style = theme->getStyle("listbox.selected.front"); + // Wrap the Listbox in a scrolling container. + scroll_container = new GuiScrollContainer(this, id + "_SCROLL"); + scroll_container + ->setScrollbarWidth(button_height) + ->setSize(GuiElement::GuiSizeMax, GuiElement::GuiSizeMax) + ->setAttribute("layout", "vertical"); + + // Listbox theme isn't declared here because it's applied by + // button->setStyle in entriesChanged(). } GuiListbox* GuiListbox::setTextSize(float size) { - text_size = size; + text_size = std::max(0.0f, size); + for (auto* button : buttons) button->setTextSize(size); + return this; +} + +GuiListbox* GuiListbox::setIconSize(float size) +{ + icon_size = std::max(0.0f, size); + for (auto* button : buttons) button->setIconSize(size); return this; } GuiListbox* GuiListbox::setButtonHeight(float height) { - button_height = height; - scroll->setClickChange(button_height); - scroll->setSize(button_height, GuiSizeMax); + button_height = std::max(0.0f, height); + scroll_container->setScrollbarWidth(height); + for (auto* button : buttons) button->setSize(GuiElement::GuiSizeMax, height); return this; } GuiListbox* GuiListbox::scrollTo(int index) { - scroll->setValue(index * button_height); + scroll_container->scrollToOffset(static_cast(index) * button_height); return this; } -void GuiListbox::onDraw(sp::RenderTarget& renderer) +void GuiListbox::entriesChanged() { - hover = false; - const auto& back = back_style->get(getState()); - const auto& back_hover = back_style->get(State::Hover); - const auto& front = front_style->get(getState()); - const auto& back_selected = back_selected_style->get(getState()); - const auto& back_selected_hover = back_selected_style->get(State::Hover); - const auto& front_selected = front_selected_style->get(getState()); - - scroll->setValueSize(rect.size.y); - scroll->setRange(0, entries.size() * button_height); - - // Determine whether to show the scrollbar based on the total height of all - // items in the list. - if ((int)entries.size() <= rect.size.y / button_height) - scroll->hide(); - else - scroll->show(); - - // Draw the button. If the scrollbar is visible, make room. - sp::Rect button_rect{rect.position, {rect.size.x, button_height}}; - - if (scroll->isVisible()) - button_rect.size.x -= scroll->getRect().size.x; - - button_rect.position.y -= scroll->getValue(); - - // For each entry, draw a button. - int index = 0; - - for(auto& e : entries) { - // Draw the button only if it will visible within the container. - if (button_rect.position.y + button_rect.size.y >= rect.position.y - && button_rect.position.y <= rect.position.y + rect.size.y) - { - auto* b = button_rect.contains(hover_coordinates) ? &back_hover : &back; - auto* f = &front; - - // If this is the selected button, change the back and foreground. - if (index == selection_index) + // Create new buttons for entries that don't have one yet. + for (auto n = buttons.size(); n < entries.size(); n++) + { + auto* btn = new GuiToggleButton(scroll_container, id + "_ENTRY_" + string(static_cast(n)), entries[n].name, + [this, n](bool) { - b = button_rect.contains(hover_coordinates) ? &back_selected_hover : &back_selected; - f = &front_selected; + setSelectionIndex(n); + callback(); } - - // Draw the background texture. - renderer.drawStretchedHVClipped(button_rect, rect, button_height * 0.5f, b->texture, b->color); - - // Draw the icon, if one's defined. - // 60% button height and aligned left. - if (e.icon_name != "") - { - renderer.drawSpriteClipped( - e.icon_name, // icon - glm::vec2( // center position - button_rect.position.x + button_rect.size.y * 0.8f, - button_rect.position.y + button_rect.size.y * 0.5f - ), - button_rect.size.y * 0.6f, // size - rect, // clipping rectangle - f->color // color - ); - } - - // Prepare the foreground text style. - auto prepared = f->font->prepare(e.name, 32, text_size, f->color, button_rect.size, sp::Alignment::Center, sp::Font::FlagClip); - for(auto& c : prepared.data) - c.position.y -= rect.position.y - button_rect.position.y; - - // Draw the text. - renderer.drawText(rect, prepared, sp::Font::FlagClip); - } - - // Prepare to draw the next button below this one. - button_rect.position.y += button_height; - index += 1; + ); + btn + ->setStyle("listbox") // Use listbox-specific theme styles. + ->setTextSize(text_size) + ->setIconSize(icon_size) + ->setSize(GuiElement::GuiSizeMax, button_height); + buttons.push_back(btn); } -} -bool GuiListbox::onMouseDown(sp::io::Pointer::Button button, glm::vec2 position, sp::io::Pointer::ID id) -{ - int offset = (position.y - rect.position.y + scroll->getValue()) / button_height; - return offset >= 0 && offset < int(entries.size()); + updateButtonStates(); } -void GuiListbox::onMouseUp(glm::vec2 position, sp::io::Pointer::ID id) +void GuiListbox::updateButtonStates() { - int offset = (position.y - rect.position.y + scroll->getValue()) / button_height; - if (offset >= 0 && offset < int(entries.size())) { - soundManager->playSound("sfx/button.wav"); - setSelectionIndex(offset); - callback(); + // Select only one button in the list. + for (auto n = 0; n < buttons.size(); n++) + { + if (n < entries.size()) + { + buttons[n] + ->setValue(n == selection_index) + ->setText(entries[n].name) + ->setIcon(entries[n].icon_name) + ->show(); + } + else buttons[n]->hide(); } } - -bool GuiListbox::onMouseWheelScroll(glm::vec2 position, float value) -{ - float range = scroll->getCorrectedMax() - scroll->getMin(); - scroll->setValue((scroll->getValue() - value * range / mouse_scroll_steps) ); - return true; -} diff --git a/src/gui/gui2_listbox.h b/src/gui/gui2_listbox.h index 59998de4ab..d835c07dba 100644 --- a/src/gui/gui2_listbox.h +++ b/src/gui/gui2_listbox.h @@ -2,32 +2,42 @@ #include "gui2_entrylist.h" -class GuiScrollbar; +class GuiScrollContainer; +class GuiToggleButton; +// Creates a scrolling list of buttons to represent an entry list. Clicking a +// button selects that value in the list. Supports only one selection at a time. class GuiListbox : public GuiEntryList { protected: - float text_size; - float button_height; - sp::Alignment text_alignment; - GuiScrollbar* scroll; - sp::Rect last_rect; - int mouse_scroll_steps; - - const GuiThemeStyle* back_style; - const GuiThemeStyle* front_style; - const GuiThemeStyle* back_selected_style; - const GuiThemeStyle* front_selected_style; + // The font size for the buttons' text, in virtual pixels. + float text_size = 30.0f; + // The buttons' icon size, as a normalized factor of button height + // (0.0-1.0 = 0-100%). + float icon_size = 0.6f; + // The buttons' height, in virtual pixels. + float button_height = 50.0f; public: GuiListbox(GuiContainer* owner, string id, func_t func); + // Set the font size for the buttons' text, in virtual pixels. GuiListbox* setTextSize(float size); + // Set the buttons' icon size, as a normalized factor of button height + // (0.0-1.0 = 0-100%). + GuiListbox* setIconSize(float size); + // Set the buttons' height, in virtual pixels. GuiListbox* setButtonHeight(float height); - + // Scroll the listbox to the item with the given index. GuiListbox* scrollTo(int index); - virtual void onDraw(sp::RenderTarget& renderer) override; - virtual bool onMouseDown(sp::io::Pointer::Button button, glm::vec2 position, sp::io::Pointer::ID id) override; - virtual void onMouseUp(glm::vec2 position, sp::io::Pointer::ID id) override; - virtual bool onMouseWheelScroll(glm::vec2 position, float value) override; +private: + // Scrolling container wrapping the button list. + GuiScrollContainer* scroll_container; + // Vector of toggle buttons representing list items. + std::vector buttons; + + // Update the listbox when its entries change. + virtual void entriesChanged() override; + // Update listbox button selection states. + void updateButtonStates(); }; diff --git a/src/gui/gui2_togglebutton.cpp b/src/gui/gui2_togglebutton.cpp index 840f914d58..21642f4175 100644 --- a/src/gui/gui2_togglebutton.cpp +++ b/src/gui/gui2_togglebutton.cpp @@ -40,7 +40,7 @@ void GuiToggleButton::onDraw(sp::RenderTarget& renderer) text_rect.size.x = rect.size.x - rect.size.y; text_align = sp::Alignment::CenterRight; } - renderer.drawRotatedSprite(icon_name, glm::vec2(icon_x, rect.position.y + rect.size.y * 0.5f), rect.size.y * 0.8f, icon_rotation, front.color); + renderer.drawRotatedSprite(icon_name, glm::vec2(icon_x, rect.position.y + rect.size.y * 0.5f), rect.size.y * icon_size, icon_rotation, front.color); renderer.drawText(text_rect, text, text_align, text_size > 0 ? text_size : front.size, front.font, front.color); }else{ renderer.drawText(rect, text, sp::Alignment::Center, text_size > 0 ? text_size : front.size, front.font, front.color);