diff --git a/CMakeLists.txt b/CMakeLists.txt index b5a387d050..3a53e87a84 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..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,6 +131,36 @@ void GuiContainer::updateLayout(const sp::Rect& rect) } } +void GuiContainer::clearElementOwner(GuiElement* element) +{ + element->owner = nullptr; +} + +void GuiContainer::setElementHover(GuiElement* element, bool has_hover) +{ + element->hover = has_hover; +} + +void GuiContainer::setElementFocus(GuiElement* element, bool has_focus) +{ + element->focus = has_focus; +} + +void GuiContainer::callDrawElements(GuiContainer* container, glm::vec2 mouse_pos, sp::Rect rect, sp::RenderTarget& render_target) +{ + container->drawElements(mouse_pos, rect, render_target); +} + +GuiElement* GuiContainer::callGetClickElement(GuiContainer* container, sp::io::Pointer::Button button, glm::vec2 pos, sp::io::Pointer::ID id) +{ + return container->getClickElement(button, pos, id); +} + +GuiElement* GuiContainer::callExecuteScrollOnElement(GuiContainer* container, glm::vec2 pos, float value) +{ + return container->executeScrollOnElement(pos, value); +} + void GuiContainer::setAttribute(const string& key, const string& value) { if (key == "size") @@ -187,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(); @@ -232,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") { @@ -250,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") @@ -263,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 547aabb80c..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,30 +45,39 @@ 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(); } - void updateLayout(const sp::Rect& rect); + 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); - 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); - friend class GuiElement; + // 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); - sp::Rect rect{0,0,0,0}; -private: - std::unique_ptr layout_manager = nullptr; + friend class GuiElement; }; - -#endif//GUI2_CONTAINER_H diff --git a/src/gui/gui2_scrollcontainer.cpp b/src/gui/gui2_scrollcontainer.cpp new file mode 100644 index 0000000000..03a1e09c7a --- /dev/null +++ b/src/gui/gui2_scrollcontainer.cpp @@ -0,0 +1,400 @@ +#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->setClickChange(50); + 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 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) 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 diff --git a/src/menus/hotkeyMenu.cpp b/src/menus/hotkeyMenu.cpp index bac55ec79f..e769a8abee 100644 --- a/src/menus/hotkeyMenu.cpp +++ b/src/menus/hotkeyMenu.cpp @@ -1,18 +1,22 @@ +#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/theme.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) @@ -20,55 +24,82 @@ HotkeyMenu::HotkeyMenu(OptionsMenu::ReturnTo return_to) new GuiOverlay(this, "", GuiTheme::getColor("background")); (new GuiOverlay(this, "", glm::u8vec4{255,255,255,255}))->setTextureTiledThemed("background.crosses"); - // 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(); - auto category_selector = new GuiSelector(top_row, "Category", [this](int index, string value) - { - HotkeyMenu::setCategory(index); - }); + 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); - // 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(); - // 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 0 ("General") HotkeyMenu::setCategory(0); @@ -77,51 +108,71 @@ HotkeyMenu::HotkeyMenu(OptionsMenu::ReturnTo return_to) // 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); @@ -138,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);