From f8e09d1c5e2f449542598393343a691e9d50419c Mon Sep 17 00:00:00 2001 From: Oznogon Date: Mon, 2 Mar 2026 17:25:24 -0800 Subject: [PATCH 01/14] 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 02/14] 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 03/14] 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 379bb0cdcdf6dbda8f9594d9c343c775612be2aa Mon Sep 17 00:00:00 2001 From: Oznogon Date: Mon, 9 Mar 2026 12:37:29 -0700 Subject: [PATCH 04/14] Refactor HotkeyMenu Redesign HotkeyMenu to use a standardized scrolling container, and to split binding inputs into device groups. - Use GuiScrollContainer to make the hotkeys list vertically scrolling instead of horizontally paged. - Use layout attributes for top-level GUI elements. - Split HotkeyMenu binders into keyboard, joystick, and mouse. - Allow binding escape control without triggering it. - Forward-declare GUI subclasses. --- src/menus/hotkeyMenu.cpp | 264 +++++++++++++++++++++------------------ src/menus/hotkeyMenu.h | 35 ++---- 2 files changed, 155 insertions(+), 144 deletions(-) diff --git a/src/menus/hotkeyMenu.cpp b/src/menus/hotkeyMenu.cpp index ea53a5f1ad..83b559adb8 100644 --- a/src/menus/hotkeyMenu.cpp +++ b/src/menus/hotkeyMenu.cpp @@ -1,120 +1,178 @@ +#include "hotkeyMenu.h" #include +#include #include "init/config.h" #include "engine.h" -#include "hotkeyMenu.h" -#include #include "soundManager.h" #include "main.h" #include "gui/hotkeyBinder.h" -#include "gui/gui2_selector.h" +#include "gui/gui2_button.h" +#include "gui/gui2_canvas.h" +#include "gui/gui2_label.h" #include "gui/gui2_overlay.h" -#include "gui/gui2_textentry.h" #include "gui/gui2_panel.h" -#include "gui/gui2_label.h" +#include "gui/gui2_scrollcontainer.h" +#include "gui/gui2_scrolltext.h" +#include "gui/gui2_selector.h" +#include "gui/gui2_textentry.h" HotkeyMenu::HotkeyMenu(OptionsMenu::ReturnTo return_to) : return_to(return_to) { + // Background color/image new GuiOverlay(this, "", colorConfig.background); (new GuiOverlay(this, "", glm::u8vec4{255,255,255,255}))->setTextureTiled("gui/background/crosses.png"); - // TODO: Figure out how to make this an AutoLayout. container = new GuiElement(this, "HOTKEY_CONFIG_CONTAINER"); - container->setSize(GuiElement::GuiSizeMax, GuiElement::GuiSizeMax)->setPosition(0, 0, sp::Alignment::TopLeft)->setMargins(FRAME_MARGIN / 2); + container + ->setSize(GuiElement::GuiSizeMax, GuiElement::GuiSizeMax) + ->setPosition(0.0f, 0.0f, sp::Alignment::TopLeft) + ->setMargins(FRAME_MARGIN * 0.5f) + ->setAttribute("layout", "vertical"); top_row = new GuiElement(container, "TOP_ROW_CONTAINER"); - top_row->setSize(GuiElement::GuiSizeMax, ROW_HEIGHT)->setPosition(0, 0, sp::Alignment::TopLeft); + top_row + ->setSize(GuiElement::GuiSizeMax, ROW_HEIGHT); rebinding_ui = new GuiPanel(container, "REBINDING_UI_CONTAINER"); - rebinding_ui->setSize(GuiElement::GuiSizeMax, KEY_COLUMN_HEIGHT)->setPosition(0, KEY_COLUMN_TOP, sp::Alignment::TopLeft); - info_container = new GuiElement(container, "info_container_CONTAINER"); - info_container->setSize(GuiElement::GuiSizeMax, ROW_HEIGHT)->setPosition(0, KEY_COLUMN_TOP+KEY_COLUMN_HEIGHT, sp::Alignment::TopLeft); + rebinding_ui + ->setSize(GuiElement::GuiSizeMax, GuiElement::GuiSizeMax) + ->setAttribute("layout", "vertical"); + rebinding_ui + ->setAttribute("padding", "20"); + + // Fixed column header row (not scrollable). + auto* header_row = new GuiElement(rebinding_ui, "HOTKEY_HEADER"); + header_row + ->setSize(GuiElement::GuiSizeMax, ROW_HEIGHT * 0.5f) + ->setAttribute("layout", "horizontal"); + + (new GuiLabel(header_row, "HOTKEY_HEADER_SPACER", "", 18.0f)) + ->setSize(KEY_LABEL_WIDTH, GuiElement::GuiSizeMax) + ->setMargins(0.0f, 0.0f, KEY_BINDER_MARGIN, 0.0f); + (new GuiLabel(header_row, "HOTKEY_HEADER_KB", tr("Keyboard"), 18.0f)) + ->setAlignment(sp::Alignment::CenterLeft) + ->setSize(GuiElement::GuiSizeMax, GuiElement::GuiSizeMax) + ->setMargins(0.0f, 0.0f, KEY_BINDER_MARGIN, 0.0f); + (new GuiLabel(header_row, "HOTKEY_HEADER_JS", tr("Joystick"), 18.0f)) + ->setAlignment(sp::Alignment::CenterLeft) + ->setSize(GuiElement::GuiSizeMax, GuiElement::GuiSizeMax) + ->setMargins(0.0f, 0.0f, KEY_BINDER_MARGIN, 0.0f); + (new GuiLabel(header_row, "HOTKEY_HEADER_MS", tr("Mouse"), 18.0f)) + ->setAlignment(sp::Alignment::CenterLeft) + ->setSize(GuiElement::GuiSizeMax, GuiElement::GuiSizeMax) + ->setMargins(0.0f, 0.0f, KEY_BINDER_MARGIN, 0.0f); + + info_container = new GuiElement(container, "INFO_CONTAINER_CONTAINER"); + info_container + ->setSize(GuiElement::GuiSizeMax, ROW_HEIGHT * 3.0f); + bottom_row = new GuiElement(container, "BOTTOM_ROW_CONTAINER"); - bottom_row->setSize(GuiElement::GuiSizeMax, ROW_HEIGHT)->setPosition(0, 0, sp::Alignment::BottomLeft); + bottom_row + ->setSize(GuiElement::GuiSizeMax, ROW_HEIGHT); // Single-column layout // Top: Title and category navigation // Title label - (new GuiLabel(top_row, "CONFIGURE_KEYBOARD_LABEL", tr("Configure Keyboard/Joystick"), 30))->addBackground()->setPosition(0, 0, sp::Alignment::TopLeft)->setSize(350, GuiElement::GuiSizeMax); + (new GuiLabel(top_row, "CONFIGURE_CONTROLS_LABEL", tr("Configure controls"), 30.0f)) + ->addBackground() + ->setSize(350.0f, GuiElement::GuiSizeMax); // Category selector // Get a list of hotkey categories category_list = sp::io::Keybinding::getCategories(); - (new GuiSelector(top_row, "Category", [this](int index, string value) - { - HotkeyMenu::setCategory(index); - }))->setOptions(category_list)->setSelectionIndex(category_index)->setSize(300, GuiElement::GuiSizeMax)->setPosition(0, 0, sp::Alignment::TopCenter); - - // Page navigation - previous_page = new GuiArrowButton(container, "PAGE_LEFT", 0, [this]() - { - HotkeyMenu::pageHotkeys(1); - }); - previous_page->setPosition(0, 0, sp::Alignment::CenterLeft)->setSize(GuiElement::GuiSizeMatchHeight, ROW_HEIGHT)->disable(); - - next_page = new GuiArrowButton(container, "PAGE_RIGHT", 180, [this]() - { - HotkeyMenu::pageHotkeys(-1); - }); - next_page->setPosition(0, 0, sp::Alignment::CenterRight)->setSize(GuiElement::GuiSizeMatchHeight, ROW_HEIGHT)->disable(); + auto* category_selector = new GuiSelector(top_row, "Category", + [this](int index, string value) + { + HotkeyMenu::setCategory(index); + } + ); + category_selector + ->setOptions(category_list) + ->setSelectionIndex(category_index) + ->setSize(300.0f, GuiElement::GuiSizeMax) + ->setPosition(0.0f, 0.0f, sp::Alignment::TopCenter); // Middle: Rebinding UI frame - rebinding_container = new GuiElement(rebinding_ui, "HOTKEY_CONFIG_CONTAINER"); - rebinding_container->setSize(GuiElement::GuiSizeMax, GuiElement::GuiSizeMax)->setPosition(0, 0, sp::Alignment::TopLeft)->setAttribute("layout", "horizontal"); + scroll_container = new GuiScrollContainer(rebinding_ui, "HOTKEY_CONTAINER"); + scroll_container + ->setSize(GuiElement::GuiSizeMax, GuiElement::GuiSizeMax) + ->setAttribute("layout", "vertical"); - // Show category 1 ("General") - HotkeyMenu::setCategory(1); + // Show category 2 ("General") + HotkeyMenu::setCategory(2); + category_selector->setSelectionIndex(2); // Bottom: Menu navigation // Back button to return to the Options menu - (new GuiScrollText(info_container, "INFO_LABEL", tr("Left Click: Assign input. Middle Click: Add input. Right Click: Delete inputs.\nPossible inputs: Keyboard keys, joystick buttons, joystick axes.")))->setPosition(10, 0, sp::Alignment::TopCenter)->setSize(GuiElement::GuiSizeMax, ROW_HEIGHT*3); - (new GuiButton(bottom_row, "BACK", tr("button", "Back"), [this, return_to]() - { - // Close this menu, stop the music, and return to the main menu. - destroy(); - soundManager->stopMusic(); - returnToOptionMenu(return_to); - }))->setPosition(0, 0, sp::Alignment::BottomLeft)->setSize(150, GuiElement::GuiSizeMax); + (new GuiScrollText(info_container, "INFO_LABEL", + tr("Left click: Assign input. Middle click: Add input. Right click: Remove last input.\nSupported inputs: Keyboard keys, joystick buttons and axes, mouse buttons and axes.") + )) + ->setPosition(10.0f, 0.0f, sp::Alignment::TopCenter) + ->setSize(GuiElement::GuiSizeMax, ROW_HEIGHT * 3) + ->setAttribute("margin", "0, 0, 20, 0"); + + (new GuiButton(bottom_row, "BACK", tr("button", "Back"), + [this, return_to]() + { + // Close this menu, stop the music, and return to the main menu. + destroy(); + soundManager->stopMusic(); + returnToOptionMenu(return_to); + } + )) + ->setPosition(0.0f, 0.0f, sp::Alignment::BottomLeft) + ->setSize(150.0f, GuiElement::GuiSizeMax); // Reset keybinds confirmation reset_label = new GuiLabel(bottom_row, "RESET_LABEL", tr("Bindings reset to defaults"), 30.0f); - reset_label->setAlignment(sp::Alignment::CenterRight)->setPosition(0.0f, -50.0f, sp::Alignment::BottomRight)->setSize(100.0f, 50.0f)->hide(); + reset_label + ->setAlignment(sp::Alignment::CenterRight) + ->setPosition(0.0f, -50.0f, sp::Alignment::BottomRight) + ->setSize(100.0f, 50.0f) + ->hide(); // Reset keybinds button - (new GuiButton(bottom_row, "RESET", tr("button", "Reset"), [this]() - { - reset_label->setVisible(true); - reset_label_timer = RESET_LABEL_TIMEOUT; - - // Iterate through all bindings and reset to defaults. - for (auto category : sp::io::Keybinding::getCategories()) + (new GuiButton(bottom_row, "RESET", tr("button", "Reset"), + [this]() { - for (auto item : sp::io::Keybinding::listAllByCategory(category)) - { - // Clear current binding. - item->clearKeys(); + reset_label->setVisible(true); + reset_label_timer = RESET_LABEL_TIMEOUT; - // Get the default binding, if any, and set it as the item's new - // binding. - std::vector default_bindings = item->getDefaultBindings(); - for (auto binding : default_bindings) item->addKey(binding); + // Iterate through all bindings and reset to defaults. + for (auto category : sp::io::Keybinding::getCategories()) + { + for (auto item : sp::io::Keybinding::listAllByCategory(category)) + { + // Clear current binding. + item->clearKeys(); + + // Get the default binding, if any, and set it as the item's new + // binding. + std::vector default_bindings = item->getDefaultBindings(); + for (auto binding : default_bindings) item->addKey(binding); + } } } - }))->setPosition(0.0f, 0.0f, sp::Alignment::BottomRight)->setSize(150.0f, GuiElement::GuiSizeMax); + )) + ->setPosition(0.0f, 0.0f, sp::Alignment::BottomRight) + ->setSize(150.0f, GuiElement::GuiSizeMax); } void HotkeyMenu::update(float delta) { + // Tick countdown to hiding the reset indicator. if (reset_label->isVisible()) { reset_label_timer -= delta; reset_label->setVisible(reset_label_timer > 0.0f); } - if (keys.escape.getDown()) + // Return to the options menu on Esc/Home bind, but not while rebinding. + if (keys.escape.getDown() && !GuiHotkeyBinder::isAnyRebinding()) { destroy(); returnToOptionMenu(return_to); @@ -131,79 +189,47 @@ void HotkeyMenu::setCategory(int cat) label_entries.clear(); for (auto row : rebinding_rows) row->destroy(); rebinding_rows.clear(); - for (auto column : rebinding_columns) column->destroy(); - rebinding_columns.clear(); - // Reset the hotkey frame size and position - int rebinding_ui_width = KEY_COLUMN_WIDTH; - rebinding_ui->setPosition(0, KEY_COLUMN_TOP, sp::Alignment::TopLeft)->setSize(KEY_COLUMN_WIDTH + FRAME_MARGIN, ROW_HEIGHT * (KEY_ROW_COUNT + 2)); + // Reset scroll to top when switching categories. + scroll_container->scrollToOffset(0.0f); // Get the chosen category category_index = cat; category = category_list[cat]; - // Initialize column row count so we can split columns. - int column_row_count = 0; - // Get all hotkeys in this category. hotkey_list = sp::io::Keybinding::listAllByCategory(category); + const sp::io::Keybinding::Type joystick_type = sp::io::Keybinding::Type::Joystick | sp::io::Keybinding::Type::Controller; + const sp::io::Keybinding::Type mouse_type = sp::io::Keybinding::Type::Pointer | sp::io::Keybinding::Type::MouseMovement | sp::io::Keybinding::Type::MouseWheel; + // Begin rendering hotkey rebinding fields for this category. for (auto item : hotkey_list) { - // If we've filled a column, or don't have any rows yet, make a new column. - if (rebinding_rows.size() == 0 || column_row_count >= KEY_ROW_COUNT) - { - column_row_count = 0; - rebinding_columns.push_back(new GuiElement(rebinding_container, "")); - rebinding_columns.back()->setSize(KEY_COLUMN_WIDTH, KEY_COLUMN_HEIGHT)->setMargins(0, 50)->setAttribute("layout", "vertical"); - } - - // Add a rebinding row to the current column. - column_row_count += 1; - rebinding_rows.push_back(new GuiElement(rebinding_columns.back(), "")); - rebinding_rows.back()->setSize(GuiElement::GuiSizeMax, ROW_HEIGHT)->setAttribute("layout", "horizontal"); + // Add a rebinding row to the scroll container. + rebinding_rows.push_back(new GuiElement(scroll_container, "")); + rebinding_rows.back() + ->setSize(GuiElement::GuiSizeMax, ROW_HEIGHT) + ->setAttribute("layout", "horizontal"); // Add a label to the current row. - label_entries.push_back(new GuiLabel(rebinding_rows.back(), "HOTKEY_LABEL_" + item->getName(), item->getLabel(), 30)); - label_entries.back()->setAlignment(sp::Alignment::CenterRight)->setSize(KEY_LABEL_WIDTH, GuiElement::GuiSizeMax)->setMargins(0, 0, FRAME_MARGIN / 2, 0); - - // Add a hotkey rebinding field to the current row. - text_entries.push_back(new GuiHotkeyBinder(rebinding_rows.back(), "HOTKEY_VALUE_" + item->getName(), item)); - text_entries.back()->setSize(KEY_FIELD_WIDTH, GuiElement::GuiSizeMax)->setMargins(0, 0, FRAME_MARGIN / 2, 0); - } - - // Resize the rendering UI panel based on the number of columns. - rebinding_ui_width = KEY_COLUMN_WIDTH * rebinding_columns.size() + FRAME_MARGIN; - rebinding_ui->setSize(rebinding_ui_width, KEY_COLUMN_HEIGHT); - - // Enable pagination buttons if pagination is necessary. - // TODO: Detect viewport width instead of hardcoding breakpoint at - // two columns - if (rebinding_columns.size() > 2) - { - previous_page->enable(); - next_page->enable(); - } else { - previous_page->disable(); - next_page->disable(); + label_entries.push_back(new GuiLabel(rebinding_rows.back(), "HOTKEY_LABEL_" + item->getName(), item->getLabel(), 30.0f)); + label_entries.back() + ->setAlignment(sp::Alignment::CenterRight) + ->setSize(KEY_LABEL_WIDTH, GuiElement::GuiSizeMax) + ->setMargins(0.0f, 0.0f, KEY_BINDER_MARGIN, 0.0f); + + // Keyboard-only binder. + text_entries.push_back(new GuiHotkeyBinder(rebinding_rows.back(), "HOTKEY_KB_" + item->getName(), item, sp::io::Keybinding::Type::Keyboard, sp::io::Keybinding::Type::Keyboard)); + text_entries.back()->setSize(GuiElement::GuiSizeMax, GuiElement::GuiSizeMax)->setMargins(0.0f, 0.0f, KEY_BINDER_MARGIN, 0.0f); + + // Joystick/controller-only binder. + text_entries.push_back(new GuiHotkeyBinder(rebinding_rows.back(), "HOTKEY_JS_" + item->getName(), item, joystick_type, joystick_type)); + text_entries.back()->setSize(GuiElement::GuiSizeMax, GuiElement::GuiSizeMax)->setMargins(0.0f, 0.0f, KEY_BINDER_MARGIN, 0.0f); + + // Mouse-only binder. + text_entries.push_back(new GuiHotkeyBinder(rebinding_rows.back(), "HOTKEY_MS_" + item->getName(), item, mouse_type, mouse_type)); + text_entries.back()->setSize(GuiElement::GuiSizeMax, GuiElement::GuiSizeMax)->setMargins(0.0f, 0.0f, KEY_BINDER_MARGIN, 0.0f); } } -void HotkeyMenu::pageHotkeys(int direction) -{ - auto frame_position = rebinding_ui->getPositionOffset(); - auto frame_size = rebinding_ui->getSize(); - - if (frame_size.x < KEY_COLUMN_WIDTH * 2) return; - - // Move the frame left if the direction is negative, right if it's positive - int new_offset = frame_position.x + KEY_COLUMN_WIDTH * direction; - - // Don't let the frame move right if its left edge is on screen. - // Move the frame left only if its right edge is not on screen. - if (new_offset >= 0) - rebinding_ui->setPosition(0, KEY_COLUMN_TOP, sp::Alignment::TopLeft); - else if (new_offset > -frame_size.x + KEY_COLUMN_WIDTH + FRAME_MARGIN) - rebinding_ui->setPosition(new_offset, KEY_COLUMN_TOP, sp::Alignment::TopLeft); -} diff --git a/src/menus/hotkeyMenu.h b/src/menus/hotkeyMenu.h index ed15975fe0..ae6574553d 100644 --- a/src/menus/hotkeyMenu.h +++ b/src/menus/hotkeyMenu.h @@ -1,36 +1,25 @@ #pragma once #include "optionsMenu.h" -#include "gui/gui2_arrowbutton.h" -#include "gui/gui2_entrylist.h" -#include "gui/gui2_canvas.h" -#include "gui/gui2_scrollbar.h" -#include "gui/gui2_scrolltext.h" #include "gui/hotkeyBinder.h" #include "Updatable.h" -class GuiArrowButton; -class GuiOverlay; -class GuiSlider; -class GuiLabel; class GuiCanvas; +class GuiHotkeyBinder; +class GuiLabel; +class GuiOverlay; class GuiPanel; +class GuiScrollContainer; class GuiScrollText; -class GuiHotkeyBinder; +class GuiSlider; class HotkeyMenu : public GuiCanvas, public Updatable { private: - const int ROW_HEIGHT = 50; - const int FRAME_MARGIN = 50; - const int KEY_LABEL_WIDTH = 375; - const int KEY_FIELD_WIDTH = 150; - const int KEY_LABEL_MARGIN = 25; - const int KEY_COLUMN_TOP = ROW_HEIGHT * 1.5; - const int KEY_ROW_COUNT = 10; - const int KEY_COLUMN_WIDTH = KEY_LABEL_WIDTH + KEY_LABEL_MARGIN + KEY_FIELD_WIDTH; - const int KEY_COLUMN_HEIGHT = ROW_HEIGHT * KEY_ROW_COUNT + FRAME_MARGIN * 2; - const int PAGER_BREAKPOINT = KEY_COLUMN_WIDTH * 2 + FRAME_MARGIN * 2; + const float ROW_HEIGHT = 50.0f; + const float FRAME_MARGIN = 50.0f; + const float KEY_LABEL_WIDTH = 400.0f; + const float KEY_BINDER_MARGIN = 12.5f; const float RESET_LABEL_TIMEOUT = 5.0f; GuiScrollText* help_text; @@ -39,14 +28,11 @@ class HotkeyMenu : public GuiCanvas, public Updatable GuiPanel* rebinding_ui; GuiElement* bottom_row; - GuiElement* rebinding_container; + GuiScrollContainer* scroll_container; GuiElement* info_container; - std::vector rebinding_columns; std::vector rebinding_rows; std::vector text_entries; std::vector label_entries; - GuiArrowButton* previous_page; - GuiArrowButton* next_page; GuiLabel* reset_label; string category = ""; @@ -57,7 +43,6 @@ class HotkeyMenu : public GuiCanvas, public Updatable OptionsMenu::ReturnTo return_to; void setCategory(int cat); - void pageHotkeys(int direction); public: HotkeyMenu(OptionsMenu::ReturnTo return_to=OptionsMenu::ReturnTo::Main); From 2f44a8de5dddd38d50e7888b8153cf6c4420f9f1 Mon Sep 17 00:00:00 2001 From: Oznogon Date: Mon, 9 Mar 2026 14:03:26 -0700 Subject: [PATCH 05/14] Refactor HotkeyBinder - Allow binder to limit scope to mouse, keyboard, or joystick/ gamecontroller types. - Allow binding left/right click without immediately canceling or rebinding. - Update right click to pop last bind instead of clearing all binds. - Prevent premature clearing of binder field before input occurs. --- src/gui/hotkeyBinder.cpp | 120 ++++++++++++++++++++++++++++++++++----- src/gui/hotkeyBinder.h | 22 ++++--- 2 files changed, 121 insertions(+), 21 deletions(-) diff --git a/src/gui/hotkeyBinder.cpp b/src/gui/hotkeyBinder.cpp index a38f2e81d6..f5b9bd0614 100644 --- a/src/gui/hotkeyBinder.cpp +++ b/src/gui/hotkeyBinder.cpp @@ -1,38 +1,130 @@ +#include "hotkeyBinder.h" #include #include "engine.h" #include "hotkeyConfig.h" -#include "hotkeyBinder.h" #include "theme.h" +// Track which binder and which key are actively performing a rebind. +static GuiHotkeyBinder* active_rebinder = nullptr; +static sp::io::Keybinding* active_key = nullptr; -GuiHotkeyBinder::GuiHotkeyBinder(GuiContainer* owner, string id, sp::io::Keybinding* key) -: GuiElement(owner, id), has_focus(false), key(key) +GuiHotkeyBinder::GuiHotkeyBinder(GuiContainer* owner, string id, sp::io::Keybinding* key, + sp::io::Keybinding::Type display_filter, sp::io::Keybinding::Type capture_filter) +: GuiElement(owner, id), key(key), display_filter(display_filter), capture_filter(capture_filter) { + // Use textentry theme styles for binder inputs. + // Someday, this should allow for icon representations instead of relying + // on text. front_style = theme->getStyle("textentry.front"); back_style = theme->getStyle("textentry.back"); } +bool GuiHotkeyBinder::isAnyRebinding() +{ + return active_rebinder != nullptr; +} + +void GuiHotkeyBinder::clearFilteredKeys() +{ + // Filter binds for this control by their type. + int count = 0; + while (key->getKeyType(count) != sp::io::Keybinding::Type::None) count++; + for (int i = count - 1; i >= 0; --i) + if (key->getKeyType(i) & display_filter) key->removeKey(i); +} + bool GuiHotkeyBinder::onMouseDown(sp::io::Pointer::Button button, glm::vec2 position, sp::io::Pointer::ID id) { - if (button != sp::io::Pointer::Button::Middle) - key->clearKeys(); - if (button != sp::io::Pointer::Button::Right) - key->startUserRebind(sp::io::Keybinding::Type::Keyboard | sp::io::Keybinding::Type::Joystick | sp::io::Keybinding::Type::Controller | sp::io::Keybinding::Type::Virtual); + // If this binder is already rebinding, just take the input and skip this. + // This should allow binding left/middle/right-click without also changing + // the binder's state at the same time. + if (active_rebinder == this) return true; + + // Left click: Assign input. Middle click: Add input. + // Right click: Remove last input. Ignore all other mouse buttons. + if (button == sp::io::Pointer::Button::Left) + clearFilteredKeys(); + if (button == sp::io::Pointer::Button::Right) + { + int count = 0; + while (key->getKeyType(count) != sp::io::Keybinding::Type::None) count++; + for (int i = count - 1; i >= 0; --i) + { + if (key->getKeyType(i) & display_filter) + { + key->removeKey(i); + break; + } + } + } + + if (button == sp::io::Pointer::Button::Left || button == sp::io::Pointer::Button::Middle) + { + const sp::io::Keybinding::Type mouse_types = sp::io::Keybinding::Type::Pointer | sp::io::Keybinding::Type::MouseMovement | sp::io::Keybinding::Type::MouseWheel; + if (capture_filter & mouse_types) + { + // Delay startUserRebind until onMouseUp so that the triggering + // mouse click is not immediately captured as the new binding. + pending_rebind = true; + } + else + { + active_rebinder = this; + active_key = key; + key->startUserRebind(capture_filter); + } + } + return true; } +void GuiHotkeyBinder::onMouseUp(glm::vec2 position, sp::io::Pointer::ID id) +{ + // Complete a pending rebind action. + if (pending_rebind) + { + pending_rebind = false; + active_rebinder = this; + active_key = key; + key->startUserRebind(capture_filter); + } +} + void GuiHotkeyBinder::onDraw(sp::RenderTarget& renderer) { - focus = key->isUserRebinding(); + // Clear the active rebind indicator only when the tracked key's rebind + // completes. + if (active_key != nullptr && !active_key->isUserRebinding()) + { + active_rebinder = nullptr; + active_key = nullptr; + } + + bool is_my_rebind = (active_rebinder == this); + focus = is_my_rebind; + const auto& back = back_style->get(getState()); const auto& front = front_style->get(getState()); renderer.drawStretched(rect, back.texture, back.color); - string text = key->getHumanReadableKeyName(0); - for(int n=1; key->getKeyType(n) != sp::io::Keybinding::Type::None; n++) - text += "," + key->getHumanReadableKeyName(n); - if (key->isUserRebinding()) - text = tr("[New input]"); - renderer.drawText(sp::Rect(rect.position.x + 16, rect.position.y, rect.size.x, rect.size.y), text, sp::Alignment::CenterLeft, front.size, front.font, front.color); + string text; + + // If this is the active rebinder, update its state to indicate that it's + // ready for input. Otherwise, list the associated binds. + // TODO: This list can get quite long. What should it do on overflow? + if (is_my_rebind) text = tr("[New input]"); + else + { + for (int n = 0; key->getKeyType(n) != sp::io::Keybinding::Type::None; n++) + { + if (key->getKeyType(n) & display_filter) + { + if (!text.empty()) text += ","; + text += key->getHumanReadableKeyName(n); + } + } + } + + renderer.drawText(sp::Rect(rect.position.x + 16.0f, rect.position.y, rect.size.x, rect.size.y), text, sp::Alignment::CenterLeft, front.size, front.font, front.color); } diff --git a/src/gui/hotkeyBinder.h b/src/gui/hotkeyBinder.h index d5ddf146ac..627000d394 100644 --- a/src/gui/hotkeyBinder.h +++ b/src/gui/hotkeyBinder.h @@ -1,23 +1,31 @@ -#ifndef HOTKEYBINDER_H -#define HOTKEYBINDER_H +#pragma once #include "gui2_element.h" - +#include "io/keybinding.h" class GuiThemeStyle; + class GuiHotkeyBinder : public GuiElement { private: - bool has_focus; sp::io::Keybinding* key; + sp::io::Keybinding::Type display_filter; + sp::io::Keybinding::Type capture_filter; + bool pending_rebind = false; const GuiThemeStyle* front_style; const GuiThemeStyle* back_style; + + void clearFilteredKeys(); public: - GuiHotkeyBinder(GuiContainer* owner, string id, sp::io::Keybinding* key); + GuiHotkeyBinder(GuiContainer* owner, string id, sp::io::Keybinding* key, sp::io::Keybinding::Type display_filter = sp::io::Keybinding::Type::Default, sp::io::Keybinding::Type capture_filter = sp::io::Keybinding::Type::Default); + + // Returns true if any binder is actively rebinding. Used to prevent + // game-wide binds like escape from being handled while binding a key. + // The escape control can't be rebound otherwise. + static bool isAnyRebinding(); 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 void onDraw(sp::RenderTarget& renderer) override; }; - -#endif //HOTKEYBINDER_H From 8a39f97e07c8d035a11d0fbf5342e68ac3cc2d16 Mon Sep 17 00:00:00 2001 From: Oznogon Date: Mon, 2 Mar 2026 17:25:24 -0800 Subject: [PATCH 06/14] 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. - Hide scrollbar if ScrollMode is None. --- CMakeLists.txt | 2 + src/gui/gui2_container.cpp | 30 +++ src/gui/gui2_container.h | 15 +- src/gui/gui2_scrollcontainer.cpp | 392 +++++++++++++++++++++++++++++++ src/gui/gui2_scrollcontainer.h | 95 ++++++++ src/gui/gui2_selector.cpp | 7 +- 6 files changed, 535 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 4b0174f048..ddfdbbd9b2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -158,6 +158,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 @@ -194,6 +195,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..807ef1b595 --- /dev/null +++ b/src/gui/gui2_scrollcontainer.cpp @@ -0,0 +1,392 @@ +#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) +{ + // Don't lock content size to element. + // We need to manipulate content size when toggling scrollbar visibility. + layout.match_content_size = false; + + // Define the scrollbar and hide it. + scrollbar_v = new GuiScrollbar(this, id + "_SCROLLBAR_V", 0, 100, 0, + [this](int value) + { + scroll_offset = static_cast(value); + } + ); + scrollbar_v->setClickChange(50); + scrollbar_v + ->setPosition(0.0f, 0.0f, sp::Alignment::TopRight) + ->setSize(scrollbar_width, GuiSizeMax) + ->hide(); +} + +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); + 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); + 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. + bool has_overflow = (mode != ScrollMode::None) && (content_height > visible_height + 0.5f); + scrollbar_v->setVisible(has_overflow); + + // Don't factor scrollbar width if it isn't visible. + const float sb_width = scrollbar_v->isVisible() ? 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. + scrollbar_v->setVisible(false); + + layout_manager->updateLoop(*this, content_layout_rect); + + scrollbar_v->setVisible(has_overflow); + + // 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. + 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 if intended to be visible. Never clip nor scroll the + // scrollbar itself. + scrollbar_v->setVisible(scrollbar_v->isVisible() && mode != ScrollMode::None); + if (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->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->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. + 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->isVisible() ? scrollbar_width : 0.0f; +} diff --git a/src/gui/gui2_scrollcontainer.h b/src/gui/gui2_scrollcontainer.h new file mode 100644 index 0000000000..73374cbe31 --- /dev/null +++ b/src/gui/gui2_scrollcontainer.h @@ -0,0 +1,95 @@ +#pragma once + +#include "gui2_element.h" + +class GuiScrollbar; + +// GuiContainer-like GuiElement with support for clipping or scrolling arbitrary +// child elements that overflow its bounds. +class GuiScrollContainer : public GuiElement +{ +public: + // Define modes to indicate whether this element scrolls, and if so, how. + 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 + }; + + 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: + // Define whether this element scrolls, paginates, or only clips content. + ScrollMode mode; + // Defines the scrollbar's width, in virtual pixels. + float scrollbar_width = 30.0f; + // Scrollbar element, visible only if there's overflow. + GuiScrollbar* scrollbar_v; + + // Defines the scroll offset in virtual pixels, with 0 as the top. + float scroll_offset = 0.0f; + // Defines the total height of content, in virtual pixels. + float content_height = 0.0f; + // Defines the visible height of the element, in virtual pixels. + float visible_height = 0.0f; + + // Defines the element that has focus within this element's subtree. + GuiElement* focused_element = nullptr; + // Defines the element being clicked/tapped within this element's subtree. + GuiElement* pressed_element = nullptr; + // Defines the scroll position of the pressed element. + float pressed_scroll = 0.0f; + + // Returns a rect for the area where content is visible. + sp::Rect getContentRect() const; + // Returns the effective scrollbar width, factoring in whether it appears + // at all. + float getEffectiveScrollbarWidth() const; + // Passes focus to another element. + void switchFocusTo(GuiElement* new_element); +}; diff --git a/src/gui/gui2_selector.cpp b/src/gui/gui2_selector.cpp index 3eb375901e..eaea76f67a 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 1b391a83325a81412b25f1250e3173c57b783f65 Mon Sep 17 00:00:00 2001 From: Oznogon Date: Mon, 9 Mar 2026 14:19:16 -0700 Subject: [PATCH 07/14] 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 02bc96cd4816519d7ef81a9f0bde690d0ba1d3cb Mon Sep 17 00:00:00 2001 From: oznogon Date: Sat, 21 Mar 2026 11:46:19 -0700 Subject: [PATCH 08/14] Rename ScissorRect refs to ClipRegion --- src/gui/gui2_scrollcontainer.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gui/gui2_scrollcontainer.cpp b/src/gui/gui2_scrollcontainer.cpp index 807ef1b595..cb3bb2e154 100644 --- a/src/gui/gui2_scrollcontainer.cpp +++ b/src/gui/gui2_scrollcontainer.cpp @@ -121,7 +121,7 @@ void GuiScrollContainer::drawElements(glm::vec2 mouse_position, sp::Rect /* pare sp::Rect content_rect = getContentRect(); // Capture clipping and scroll translation. - renderer.pushScissorRect(content_rect); + renderer.pushClipRegion(content_rect); renderer.pushTranslation({0.0f, -scroll_offset}); // Track mouse position on element relative to the vertical scroll offset. @@ -163,7 +163,7 @@ void GuiScrollContainer::drawElements(glm::vec2 mouse_position, sp::Rect /* pare // Apply scroll translation and clipping. Order matters here. renderer.popTranslation(); - renderer.popScissorRect(); + renderer.popClipRegion(); // Draw the scrollbar if intended to be visible. Never clip nor scroll the // scrollbar itself. From 0a70a47eb62a8f229cb58592204ba6bd10463a01 Mon Sep 17 00:00:00 2001 From: oznogon Date: Sat, 21 Mar 2026 12:29:59 -0700 Subject: [PATCH 09/14] Reimplement recursive descendent scrolling in GuiContainer Remove use of SP-based translation/reprojection and implement scrolling by recursively manipulating descendant rects. - Add GuiContainer::offsetElementRect() to recursively apply the given positional offset. - Replace SP translation methods in GuiScrollContainer with GuiContainer::offsetElementRect(). - Update GuiSelector popup handling. --- src/gui/gui2_container.cpp | 7 +++++ src/gui/gui2_container.h | 3 ++ src/gui/gui2_scrollcontainer.cpp | 50 +++++++++++++++----------------- src/gui/gui2_scrollcontainer.h | 2 -- src/gui/gui2_selector.cpp | 7 ++--- 5 files changed, 35 insertions(+), 34 deletions(-) diff --git a/src/gui/gui2_container.cpp b/src/gui/gui2_container.cpp index b6dcdbcc7c..b4ddb190ac 100644 --- a/src/gui/gui2_container.cpp +++ b/src/gui/gui2_container.cpp @@ -161,6 +161,13 @@ GuiElement* GuiContainer::callExecuteScrollOnElement(GuiContainer* container, gl return container->executeScrollOnElement(pos, value); } +void GuiContainer::offsetElementRect(GuiContainer* container, glm::vec2 offset) +{ + container->rect.position += offset; + for (auto* child : container->children) + offsetElementRect(child, offset); +} + 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 a13799df06..7fd47d1ddf 100644 --- a/src/gui/gui2_container.h +++ b/src/gui/gui2_container.h @@ -78,6 +78,9 @@ class GuiContainer : sp::NonCopyable 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); + // Apply an offset to this rect's position, and then recursively to all of + // its descendents. Used for nested scrolling/GuiScrollContainer. + static void offsetElementRect(GuiContainer* container, glm::vec2 offset); friend class GuiElement; }; diff --git a/src/gui/gui2_scrollcontainer.cpp b/src/gui/gui2_scrollcontainer.cpp index cb3bb2e154..b52b3c3999 100644 --- a/src/gui/gui2_scrollcontainer.cpp +++ b/src/gui/gui2_scrollcontainer.cpp @@ -110,6 +110,14 @@ void GuiScrollContainer::updateLayout(const sp::Rect& rect) // Clamp scroll offset. scroll_offset = std::clamp(scroll_offset, 0.0f, std::max(0.0f, content_height - visible_height)); + // Apply the scroll offset to non-scrollbar children so their rects are in + // screen space. + for (GuiElement* child : children) + { + if (child == scrollbar_v) continue; + offsetElementRect(child, {0.0f, -scroll_offset}); + } + // Sync scrollbar properties to new layout. scrollbar_v->setRange(0, static_cast(content_height)); scrollbar_v->setValueSize(static_cast(visible_height)); @@ -120,14 +128,11 @@ void GuiScrollContainer::drawElements(glm::vec2 mouse_position, sp::Rect /* pare { sp::Rect content_rect = getContentRect(); - // Capture clipping and scroll translation. + // Clip child rendering to the visible content area. renderer.pushClipRegion(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. + // Draw each child element and pass mouse events, skipping this container's + // scrollbar. for (auto it = children.begin(); it != children.end(); ) { GuiElement* element = *it; @@ -150,19 +155,17 @@ void GuiScrollContainer::drawElements(glm::vec2 mouse_position, sp::Rect /* pare continue; } - setElementHover(element, element->getRect().contains(layout_mouse)); + setElementHover(element, element->getRect().contains(mouse_position)); if (element->isVisible()) { element->onDraw(renderer); - callDrawElements(element, layout_mouse, element->getRect(), renderer); + callDrawElements(element, mouse_position, element->getRect(), renderer); } ++it; } - // Apply scroll translation and clipping. Order matters here. - renderer.popTranslation(); renderer.popClipRegion(); // Draw the scrollbar if intended to be visible. Never clip nor scroll the @@ -194,8 +197,6 @@ GuiElement* GuiScrollContainer::getClickElement(sp::io::Pointer::Button button, // 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; @@ -205,25 +206,22 @@ GuiElement* GuiScrollContainer::getClickElement(sp::io::Pointer::Button button, // 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); + // Figure out if we can click the element. If so, focus it and click it. + GuiElement* clicked = callGetClickElement(element, button, position, 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)) + if (element->getRect().contains(position) && element->onMouseDown(button, position, id)) { switchFocusTo(element); pressed_element = element; - pressed_scroll = scroll_offset; return this; } } @@ -289,7 +287,7 @@ bool GuiScrollContainer::onMouseDown(sp::io::Pointer::Button button, glm::vec2 p { if (pressed_element) { - pressed_element->onMouseDown(button, position + glm::vec2{0.0f, pressed_scroll}, id); + pressed_element->onMouseDown(button, position, id); pressed_element = nullptr; return true; } @@ -299,14 +297,14 @@ bool GuiScrollContainer::onMouseDown(sp::io::Pointer::Button button, glm::vec2 p 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); -} + if (pressed_element) pressed_element->onMouseDrag(position, 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->onMouseUp(position, id); pressed_element = nullptr; } } @@ -329,8 +327,6 @@ GuiElement* GuiScrollContainer::executeScrollOnElement(glm::vec2 position, float // 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; @@ -339,12 +335,12 @@ GuiElement* GuiScrollContainer::executeScrollOnElement(glm::vec2 position, float if (element && element->isVisible() && element->isEnabled() - && element->getRect().contains(layout_pos) + && element->getRect().contains(position) ) { - GuiElement* scrolled = callExecuteScrollOnElement(element, layout_pos, value); + GuiElement* scrolled = callExecuteScrollOnElement(element, position, value); if (scrolled) return scrolled; - if (element->onMouseWheelScroll(layout_pos, value)) return element; + if (element->onMouseWheelScroll(position, value)) return element; } } diff --git a/src/gui/gui2_scrollcontainer.h b/src/gui/gui2_scrollcontainer.h index 73374cbe31..f593d6bd29 100644 --- a/src/gui/gui2_scrollcontainer.h +++ b/src/gui/gui2_scrollcontainer.h @@ -82,8 +82,6 @@ class GuiScrollContainer : public GuiElement GuiElement* focused_element = nullptr; // Defines the element being clicked/tapped within this element's subtree. GuiElement* pressed_element = nullptr; - // Defines the scroll position of the pressed element. - float pressed_scroll = 0.0f; // Returns a rect for the area where content is visible. sp::Rect getContentRect() const; diff --git a/src/gui/gui2_selector.cpp b/src/gui/gui2_selector.cpp index eaea76f67a..3eb375901e 100644 --- a/src/gui/gui2_selector.cpp +++ b/src/gui/gui2_selector.cpp @@ -49,16 +49,13 @@ void GuiSelector::onDraw(sp::RenderTarget& renderer) if (!focus) popup->hide(); - // 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 top = rect.position.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(screen_pos.x, top, sp::Alignment::TopLeft)->setSize(rect.size.x, height); + popup->setPosition(rect.position.x, top, sp::Alignment::TopLeft)->setSize(rect.size.x, height); } GuiSelector* GuiSelector::setTextSize(float size) From 487774e0c1a6317415594f029b9b0fff9892e131 Mon Sep 17 00:00:00 2001 From: oznogon Date: Sat, 21 Mar 2026 20:22:28 -0700 Subject: [PATCH 10/14] Consolidate scrolling to updateLayout Remove offsetElementRect() (which recursively loops child elements) and instead move elements by scroll_offset in updateLayout() (which already recursively loops child elements). --- src/gui/gui2_container.cpp | 6 ------ src/gui/gui2_container.h | 3 --- src/gui/gui2_scrollcontainer.cpp | 34 ++++++++++++++++---------------- 3 files changed, 17 insertions(+), 26 deletions(-) diff --git a/src/gui/gui2_container.cpp b/src/gui/gui2_container.cpp index b4ddb190ac..1e86a52994 100644 --- a/src/gui/gui2_container.cpp +++ b/src/gui/gui2_container.cpp @@ -161,12 +161,6 @@ GuiElement* GuiContainer::callExecuteScrollOnElement(GuiContainer* container, gl return container->executeScrollOnElement(pos, value); } -void GuiContainer::offsetElementRect(GuiContainer* container, glm::vec2 offset) -{ - container->rect.position += offset; - for (auto* child : container->children) - offsetElementRect(child, offset); -} void GuiContainer::setAttribute(const string& key, const string& value) { diff --git a/src/gui/gui2_container.h b/src/gui/gui2_container.h index 7fd47d1ddf..a13799df06 100644 --- a/src/gui/gui2_container.h +++ b/src/gui/gui2_container.h @@ -78,9 +78,6 @@ class GuiContainer : sp::NonCopyable 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); - // Apply an offset to this rect's position, and then recursively to all of - // its descendents. Used for nested scrolling/GuiScrollContainer. - static void offsetElementRect(GuiContainer* container, glm::vec2 offset); friend class GuiElement; }; diff --git a/src/gui/gui2_scrollcontainer.cpp b/src/gui/gui2_scrollcontainer.cpp index b52b3c3999..f916a1004b 100644 --- a/src/gui/gui2_scrollcontainer.cpp +++ b/src/gui/gui2_scrollcontainer.cpp @@ -18,8 +18,8 @@ GuiScrollContainer::GuiScrollContainer(GuiContainer* owner, const string& id, Sc scroll_offset = static_cast(value); } ); - scrollbar_v->setClickChange(50); scrollbar_v + ->setClickChange(50) ->setPosition(0.0f, 0.0f, sp::Alignment::TopRight) ->setSize(scrollbar_width, GuiSizeMax) ->hide(); @@ -56,6 +56,10 @@ void GuiScrollContainer::updateLayout(const sp::Rect& rect) this->rect = rect; visible_height = rect.size.y - layout.padding.top - layout.padding.bottom; + // Clamp scroll_offset using last frame's content_height to validate the + // value passed to the layout manager. + scroll_offset = std::clamp(scroll_offset, 0.0f, std::max(0.0f, content_height - visible_height)); + // Show the scrollbar only if we're clipping anything. bool has_overflow = (mode != ScrollMode::None) && (content_height > visible_height + 0.5f); scrollbar_v->setVisible(has_overflow); @@ -63,7 +67,7 @@ void GuiScrollContainer::updateLayout(const sp::Rect& rect) // Don't factor scrollbar width if it isn't visible. const float sb_width = scrollbar_v->isVisible() ? scrollbar_width : 0.0f; - // Manually factor padding into content layout around the scrollbar. + // Factor layout padding. glm::vec2 padding_offset{ layout.padding.left, layout.padding.top @@ -74,8 +78,10 @@ void GuiScrollContainer::updateLayout(const sp::Rect& rect) layout.padding.top + layout.padding.bottom }; + // Subtract scroll_offset from the layout rect so all children (and their + // descendants) are offset. sp::Rect content_layout_rect{ - rect.position + padding_offset, + rect.position + padding_offset + glm::vec2{0.0f, -scroll_offset}, rect.size - padding_size - glm::vec2{sb_width, 0.0f} }; @@ -95,33 +101,27 @@ void GuiScrollContainer::updateLayout(const sp::Rect& rect) {scrollbar_width, rect.size.y} }); - // Compute content_height from non-scrollbar visible children. + // Compute content_height. Child elements are scrolled, so add + // scroll_offset from extents. 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; + const float bottom = child->getRect().position.y + child->getRect().size.y + child->layout.margin.bottom - rect.position.y + scroll_offset; if (bottom > max_bottom) max_bottom = bottom; } content_height = max_bottom + layout.padding.bottom; - // Clamp scroll offset. + // Clamp again in case content shrank this frame. scroll_offset = std::clamp(scroll_offset, 0.0f, std::max(0.0f, content_height - visible_height)); - // Apply the scroll offset to non-scrollbar children so their rects are in - // screen space. - for (GuiElement* child : children) - { - if (child == scrollbar_v) continue; - offsetElementRect(child, {0.0f, -scroll_offset}); - } - // Sync scrollbar properties to new layout. - scrollbar_v->setRange(0, static_cast(content_height)); - scrollbar_v->setValueSize(static_cast(visible_height)); - scrollbar_v->setValue(static_cast(scroll_offset)); + scrollbar_v + ->setRange(0, static_cast(content_height)) + ->setValueSize(static_cast(visible_height)) + ->setValue(static_cast(scroll_offset)); } void GuiScrollContainer::drawElements(glm::vec2 mouse_position, sp::Rect /* parent_rect */, sp::RenderTarget& renderer) From 2265ee7d0d7ff7c48cfc12f94f908b7e51548870 Mon Sep 17 00:00:00 2001 From: oznogon Date: Sun, 22 Mar 2026 23:46:33 -0700 Subject: [PATCH 11/14] Fix GuiSelector merge issue --- src/gui/gui2_selector.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/gui/gui2_selector.cpp b/src/gui/gui2_selector.cpp index eaea76f67a..3eb375901e 100644 --- a/src/gui/gui2_selector.cpp +++ b/src/gui/gui2_selector.cpp @@ -49,16 +49,13 @@ void GuiSelector::onDraw(sp::RenderTarget& renderer) if (!focus) popup->hide(); - // 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 top = rect.position.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(screen_pos.x, top, sp::Alignment::TopLeft)->setSize(rect.size.x, height); + popup->setPosition(rect.position.x, top, sp::Alignment::TopLeft)->setSize(rect.size.x, height); } GuiSelector* GuiSelector::setTextSize(float size) From e811794085a7c90b4c0606fc8b0de9bb5be4a775 Mon Sep 17 00:00:00 2001 From: oznogon Date: Mon, 23 Mar 2026 00:00:15 -0700 Subject: [PATCH 12/14] Use sp::io::Keybinding::Type::Mouse --- src/gui/hotkeyBinder.cpp | 3 +-- src/menus/hotkeyMenu.cpp | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/gui/hotkeyBinder.cpp b/src/gui/hotkeyBinder.cpp index f5b9bd0614..3cfdb96c7b 100644 --- a/src/gui/hotkeyBinder.cpp +++ b/src/gui/hotkeyBinder.cpp @@ -60,8 +60,7 @@ bool GuiHotkeyBinder::onMouseDown(sp::io::Pointer::Button button, glm::vec2 posi if (button == sp::io::Pointer::Button::Left || button == sp::io::Pointer::Button::Middle) { - const sp::io::Keybinding::Type mouse_types = sp::io::Keybinding::Type::Pointer | sp::io::Keybinding::Type::MouseMovement | sp::io::Keybinding::Type::MouseWheel; - if (capture_filter & mouse_types) + if (capture_filter & sp::io::Keybinding::Type::Mouse) { // Delay startUserRebind until onMouseUp so that the triggering // mouse click is not immediately captured as the new binding. diff --git a/src/menus/hotkeyMenu.cpp b/src/menus/hotkeyMenu.cpp index e769a8abee..fda31664f6 100644 --- a/src/menus/hotkeyMenu.cpp +++ b/src/menus/hotkeyMenu.cpp @@ -201,7 +201,6 @@ void HotkeyMenu::setCategory(int cat) hotkey_list = sp::io::Keybinding::listAllByCategory(category); const sp::io::Keybinding::Type joystick_type = sp::io::Keybinding::Type::Joystick | sp::io::Keybinding::Type::Controller; - const sp::io::Keybinding::Type mouse_type = sp::io::Keybinding::Type::Pointer | sp::io::Keybinding::Type::MouseMovement | sp::io::Keybinding::Type::MouseWheel; // Begin rendering hotkey rebinding fields for this category. for (auto item : hotkey_list) @@ -228,7 +227,7 @@ void HotkeyMenu::setCategory(int cat) text_entries.back()->setSize(GuiElement::GuiSizeMax, GuiElement::GuiSizeMax)->setMargins(0.0f, 0.0f, KEY_BINDER_MARGIN, 0.0f); // Mouse-only binder. - text_entries.push_back(new GuiHotkeyBinder(rebinding_rows.back(), "HOTKEY_MS_" + item->getName(), item, mouse_type, mouse_type)); + text_entries.push_back(new GuiHotkeyBinder(rebinding_rows.back(), "HOTKEY_MS_" + item->getName(), item, sp::io::Keybinding::Type::Mouse, sp::io::Keybinding::Type::Mouse)); text_entries.back()->setSize(GuiElement::GuiSizeMax, GuiElement::GuiSizeMax)->setMargins(0.0f, 0.0f, KEY_BINDER_MARGIN, 0.0f); } } From 523f47cad30c8a66b3d8b91d532945676243ac68 Mon Sep 17 00:00:00 2001 From: oznogon Date: Mon, 23 Mar 2026 23:39:10 -0700 Subject: [PATCH 13/14] Consolidate GuiHotkeyBinder logic --- src/gui/hotkeyBinder.cpp | 37 +++++++++++++++++++------------------ src/gui/hotkeyBinder.h | 1 + 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/gui/hotkeyBinder.cpp b/src/gui/hotkeyBinder.cpp index 3cfdb96c7b..e1506e46bc 100644 --- a/src/gui/hotkeyBinder.cpp +++ b/src/gui/hotkeyBinder.cpp @@ -8,8 +8,7 @@ static GuiHotkeyBinder* active_rebinder = nullptr; static sp::io::Keybinding* active_key = nullptr; -GuiHotkeyBinder::GuiHotkeyBinder(GuiContainer* owner, string id, sp::io::Keybinding* key, - sp::io::Keybinding::Type display_filter, sp::io::Keybinding::Type capture_filter) +GuiHotkeyBinder::GuiHotkeyBinder(GuiContainer* owner, string id, sp::io::Keybinding* key, sp::io::Keybinding::Type display_filter, sp::io::Keybinding::Type capture_filter) : GuiElement(owner, id), key(key), display_filter(display_filter), capture_filter(capture_filter) { // Use textentry theme styles for binder inputs. @@ -24,6 +23,17 @@ bool GuiHotkeyBinder::isAnyRebinding() return active_rebinder != nullptr; } +void GuiHotkeyBinder::startRebind() +{ + active_rebinder = this; + active_key = key; + // Strip globally prohibited types from the capture filter so they are + // skipped rather than ending the rebind when the user triggers them. + sp::io::Keybinding::Type effective_capture = static_cast( + static_cast(capture_filter) & ~static_cast(sp::io::Keybinding::getGloballyProhibitedTypes())); + key->startUserRebind(effective_capture); +} + void GuiHotkeyBinder::clearFilteredKeys() { // Filter binds for this control by their type. @@ -36,14 +46,13 @@ void GuiHotkeyBinder::clearFilteredKeys() bool GuiHotkeyBinder::onMouseDown(sp::io::Pointer::Button button, glm::vec2 position, sp::io::Pointer::ID id) { // If this binder is already rebinding, just take the input and skip this. - // This should allow binding left/middle/right-click without also changing - // the binder's state at the same time. + // This should allow binding middle/right-click without also changing the + // binder's state at the same time. if (active_rebinder == this) return true; // Left click: Assign input. Middle click: Add input. // Right click: Remove last input. Ignore all other mouse buttons. - if (button == sp::io::Pointer::Button::Left) - clearFilteredKeys(); + if (button == sp::io::Pointer::Button::Left) clearFilteredKeys(); if (button == sp::io::Pointer::Button::Right) { int count = 0; @@ -60,18 +69,12 @@ bool GuiHotkeyBinder::onMouseDown(sp::io::Pointer::Button button, glm::vec2 posi if (button == sp::io::Pointer::Button::Left || button == sp::io::Pointer::Button::Middle) { + // Delay startUserRebind until onMouseUp so that the triggering + // mouse click is not immediately captured as the new binding. if (capture_filter & sp::io::Keybinding::Type::Mouse) - { - // Delay startUserRebind until onMouseUp so that the triggering - // mouse click is not immediately captured as the new binding. pending_rebind = true; - } else - { - active_rebinder = this; - active_key = key; - key->startUserRebind(capture_filter); - } + startRebind(); } return true; @@ -83,9 +86,7 @@ void GuiHotkeyBinder::onMouseUp(glm::vec2 position, sp::io::Pointer::ID id) if (pending_rebind) { pending_rebind = false; - active_rebinder = this; - active_key = key; - key->startUserRebind(capture_filter); + startRebind(); } } diff --git a/src/gui/hotkeyBinder.h b/src/gui/hotkeyBinder.h index 627000d394..a5be6715fa 100644 --- a/src/gui/hotkeyBinder.h +++ b/src/gui/hotkeyBinder.h @@ -17,6 +17,7 @@ class GuiHotkeyBinder : public GuiElement const GuiThemeStyle* back_style; void clearFilteredKeys(); + void startRebind(); public: GuiHotkeyBinder(GuiContainer* owner, string id, sp::io::Keybinding* key, sp::io::Keybinding::Type display_filter = sp::io::Keybinding::Type::Default, sp::io::Keybinding::Type capture_filter = sp::io::Keybinding::Type::Default); From 291762ca32f0358186eea3271a605fb34711dbe7 Mon Sep 17 00:00:00 2001 From: oznogon Date: Mon, 23 Mar 2026 23:39:50 -0700 Subject: [PATCH 14/14] Implement SP input prohibition in hotkeys Implement addProhibitedKey/Type and addGloballyProhibitedKey/Type functions, and designate prohibited middle and/or right mouse button inputs for GM screen, main screen, and Relay. --- src/gui/hotkeyConfig.cpp | 42 +++++++++++++++++++++++++++++++++++++--- src/menus/hotkeyMenu.cpp | 2 +- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/src/gui/hotkeyConfig.cpp b/src/gui/hotkeyConfig.cpp index c215e7fbd4..a7acee87d1 100644 --- a/src/gui/hotkeyConfig.cpp +++ b/src/gui/hotkeyConfig.cpp @@ -325,6 +325,15 @@ Keys::Keys() : void Keys::init() { + // Global keybinding prohibitions: + // - Mouse movement axes + // - Mouse wheel + // - Left mouse button (pointer:1) + // Features using these inputs are implemented outside of binds, and any + // binds that used them would conflict. + sp::io::Keybinding::addGloballyProhibitedTypes(sp::io::Keybinding::Type::MouseMovement | sp::io::Keybinding::Type::MouseWheel); + sp::io::Keybinding::addGloballyProhibitedKey("pointer:1"); + // Common binds game-wide pause.setLabel(tr("hotkey_menu", "General"), tr("hotkey_General", "Pause game")); help.setLabel(tr("hotkey_menu", "General"), tr("hotkey_General", "Show in-game help")); @@ -346,15 +355,32 @@ void Keys::init() station_science.setLabel(tr("hotkey_menu", "Crew screens"), tr("hotkey_CrewScreen", "Switch to science screen")); station_relay.setLabel(tr("hotkey_menu", "Crew screens"), tr("hotkey_CrewScreen", "Switch to relay screen")); - // Main screen + // Main screen (prohibit right/middle mouse muttons, which are hardcoded to + // cycle views) mainscreen_forward.setLabel(tr("hotkey_menu", "Main screen"), tr("hotkey_MainScreen", "View forward")); + mainscreen_forward.addProhibitedKey("pointer:2"); + mainscreen_forward.addProhibitedKey("pointer:3"); mainscreen_left.setLabel(tr("hotkey_menu", "Main screen"), tr("hotkey_MainScreen", "View left")); + mainscreen_left.addProhibitedKey("pointer:2"); + mainscreen_left.addProhibitedKey("pointer:3"); mainscreen_right.setLabel(tr("hotkey_menu", "Main screen"), tr("hotkey_MainScreen", "View right")); + mainscreen_right.addProhibitedKey("pointer:2"); + mainscreen_right.addProhibitedKey("pointer:3"); mainscreen_back.setLabel(tr("hotkey_menu", "Main screen"), tr("hotkey_MainScreen", "View backward")); + mainscreen_back.addProhibitedKey("pointer:2"); + mainscreen_back.addProhibitedKey("pointer:3"); mainscreen_target.setLabel(tr("hotkey_menu", "Main screen"), tr("hotkey_MainScreen", "Lock view on weapons target")); + mainscreen_target.addProhibitedKey("pointer:2"); + mainscreen_target.addProhibitedKey("pointer:3"); mainscreen_tactical_radar.setLabel(tr("hotkey_menu", "Main screen"), tr("hotkey_MainScreen", "View tactical radar")); + mainscreen_tactical_radar.addProhibitedKey("pointer:2"); + mainscreen_tactical_radar.addProhibitedKey("pointer:3"); mainscreen_long_range_radar.setLabel(tr("hotkey_menu", "Main screen"), tr("hotkey_MainScreen", "View long-range radar")); + mainscreen_long_range_radar.addProhibitedKey("pointer:2"); + mainscreen_long_range_radar.addProhibitedKey("pointer:3"); mainscreen_first_person.setLabel(tr("hotkey_menu", "Main screen"), tr("hotkey_MainScreen", "Toggle first-person view")); + mainscreen_first_person.addProhibitedKey("pointer:2"); + mainscreen_first_person.addProhibitedKey("pointer:3"); // Helms helms_increase_impulse.setLabel(tr("hotkey_menu", "Helms"), tr("hotkey_Helms", "Increase impulse")); @@ -493,10 +519,13 @@ void Keys::init() engineering_set_coolant_for_system[static_cast(ShipSystem::Type::FrontShield)].setLabel(tr("hotkey_menu", "Engineering"), tr("hotkey_Engineering", "Set front shields coolant (joystick)")); engineering_set_coolant_for_system[static_cast(ShipSystem::Type::RearShield)].setLabel(tr("hotkey_menu", "Engineering"), tr("hotkey_Engineering", "Set rear shields coolant (joystick)")); - // Relay + // Relay (prohibit right click, hardcoded in hacking minigames) relay_alert_level_none.setLabel(tr("hotkey_menu", "Relay"), tr("hotkey_Relay", "Alert level: Normal")); + relay_alert_level_none.addProhibitedKey("pointer:3"); relay_alert_level_yellow.setLabel(tr("hotkey_menu", "Relay"), tr("hotkey_Relay", "Alert level: Yellow")); + relay_alert_level_yellow.addProhibitedKey("pointer:3"); relay_alert_level_red.setLabel(tr("hotkey_menu", "Relay"), tr("hotkey_Relay", "Alert level: Red")); + relay_alert_level_red.addProhibitedKey("pointer:3"); // Cinematic view cinematic.init(); @@ -504,10 +533,17 @@ void Keys::init() // Top-down view topdown.init(); - // GM screen + // GM screen (prohibit middle and right mouse buttons pointer:2 and :3 + // until/unless their GM screen functions are made rebindable) gm_delete.setLabel(tr("hotkey_menu", "GM screen"), tr("hotkey_GM", "Delete")); + gm_delete.addProhibitedKey("pointer:2"); + gm_delete.addProhibitedKey("pointer:3"); gm_clipboardcopy.setLabel(tr("hotkey_menu", "GM screen"), tr("hotkey_GM", "Copy to clipboard")); + gm_clipboardcopy.addProhibitedKey("pointer:2"); + gm_clipboardcopy.addProhibitedKey("pointer:3"); gm_show_callsigns.setLabel(tr("hotkey_menu", "GM screen"), tr("hotkey_GM", "Show callsigns (GM)")); + gm_show_callsigns.addProhibitedKey("pointer:2"); + gm_show_callsigns.addProhibitedKey("pointer:3"); // Spectator screen spectator_show_callsigns.setLabel(tr("hotkey_menu", "Spectator view"), tr("hotkey_Spectator", "Show callsigns (spectator)")); diff --git a/src/menus/hotkeyMenu.cpp b/src/menus/hotkeyMenu.cpp index fda31664f6..925bde2995 100644 --- a/src/menus/hotkeyMenu.cpp +++ b/src/menus/hotkeyMenu.cpp @@ -109,7 +109,7 @@ HotkeyMenu::HotkeyMenu(OptionsMenu::ReturnTo return_to) // Back button to return to the Options menu (new GuiScrollText(info_container, "INFO_LABEL", - tr("Left click: Assign input. Middle click: Add input. Right click: Remove last input.\nSupported inputs: Keyboard keys, joystick buttons and axes, mouse buttons and axes.") + tr("Left click: Assign input. Middle click: Add input. Right click: Remove last input.\nSupported inputs: Keyboard keys, joystick buttons and axes, mouse buttons (except left mouse button).") )) ->setPosition(10.0f, 0.0f, sp::Alignment::TopCenter) ->setSize(GuiElement::GuiSizeMax, ROW_HEIGHT * 3)