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_button.cpp b/src/gui/gui2_button.cpp index ab6fb2ce38..e2da6b3782 100644 --- a/src/gui/gui2_button.cpp +++ b/src/gui/gui2_button.cpp @@ -40,7 +40,7 @@ void GuiButton::onDraw(sp::RenderTarget& renderer) text_rect.size.x = rect.size.x - rect.size.y; text_align = sp::Alignment::CenterRight; } - renderer.drawRotatedSprite(icon_name, glm::vec2(icon_x, rect.position.y + rect.size.y * 0.5f), rect.size.y * 0.8f, icon_rotation, front.color); + renderer.drawRotatedSprite(icon_name, glm::vec2(icon_x, rect.position.y + rect.size.y * 0.5f), rect.size.y * icon_size, icon_rotation, front.color); renderer.drawText(text_rect, text, text_align, text_size > 0 ? text_size : front.size, front.font, front.color); }else{ renderer.drawText(rect, text, sp::Alignment::Center, text_size > 0 ? text_size : front.size, front.font, front.color); @@ -90,6 +90,12 @@ GuiButton* GuiButton::setIcon(string icon_name, sp::Alignment icon_alignment, fl return this; } +GuiButton* GuiButton::setIconSize(float size) +{ + icon_size = size; + return this; +} + GuiButton* GuiButton::setStyle(const string& style) { back_style = theme->getStyle(style + ".back"); diff --git a/src/gui/gui2_button.h b/src/gui/gui2_button.h index 3ddcfc591d..a445563696 100644 --- a/src/gui/gui2_button.h +++ b/src/gui/gui2_button.h @@ -17,6 +17,7 @@ class GuiButton : public GuiElement string icon_name; sp::Alignment icon_alignment; float icon_rotation; + float icon_size = 0.8f; const GuiThemeStyle* back_style; const GuiThemeStyle* front_style; public: @@ -29,6 +30,7 @@ class GuiButton : public GuiElement GuiButton* setText(string text); GuiButton* setTextSize(float size); GuiButton* setIcon(string icon_name, sp::Alignment icon_alignment = sp::Alignment::CenterLeft, float rotation = 0); + GuiButton* setIconSize(float size); GuiButton* setStyle(const string& style); string getText() const; string getIcon() const; 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..aea291e249 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; + // Access GuiElement/GuiContainer protected members in subclass + static void clearElementOwner(GuiElement* element); + static void setElementHover(GuiElement* element, bool has_hover); + static void setElementFocus(GuiElement* element, bool has_focus); + 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_listbox.cpp b/src/gui/gui2_listbox.cpp index de37e62b36..26bb78027e 100644 --- a/src/gui/gui2_listbox.cpp +++ b/src/gui/gui2_listbox.cpp @@ -1,141 +1,85 @@ #include "gui2_listbox.h" -#include "soundManager.h" -#include "theme.h" - -#include "gui2_scrollbar.h" +#include "gui2_scrollcontainer.h" +#include "gui2_togglebutton.h" GuiListbox::GuiListbox(GuiContainer* owner, string id, func_t func) -: GuiEntryList(owner, id, func), text_size(30), button_height(50), text_alignment(sp::Alignment::Center), mouse_scroll_steps(25) +: GuiEntryList(owner, id, func) { - scroll = new GuiScrollbar(this, id + "_SCROLL", 0, 0, 0, [this](int value) {}); - scroll->setPosition(0, 0, sp::Alignment::TopRight)->setSize(button_height, GuiSizeMax)->hide(); - scroll->setClickChange(button_height); - - back_style = theme->getStyle("listbox.back"); - front_style = theme->getStyle("listbox.front"); - back_selected_style = theme->getStyle("listbox.selected.back"); - front_selected_style = theme->getStyle("listbox.selected.front"); + // Wrap the Listbox in a scrolling container. + scroll_container = new GuiScrollContainer(this, id + "_SCROLL"); + scroll_container + ->setScrollbarWidth(button_height) + ->setSize(GuiElement::GuiSizeMax, GuiElement::GuiSizeMax) + ->setAttribute("layout", "vertical"); + + // Listbox theme isn't declared here because it's applied by + // button->setStyle in entriesChanged(). } GuiListbox* GuiListbox::setTextSize(float size) { - text_size = size; + text_size = std::max(0.0f, size); + for (auto* button : buttons) button->setTextSize(size); + return this; +} + +GuiListbox* GuiListbox::setIconSize(float size) +{ + icon_size = std::max(0.0f, size); + for (auto* button : buttons) button->setIconSize(size); return this; } GuiListbox* GuiListbox::setButtonHeight(float height) { - button_height = height; - scroll->setClickChange(button_height); - scroll->setSize(button_height, GuiSizeMax); + button_height = std::max(0.0f, height); + scroll_container->setScrollbarWidth(height); + for (auto* button : buttons) button->setSize(GuiElement::GuiSizeMax, height); return this; } GuiListbox* GuiListbox::scrollTo(int index) { - scroll->setValue(index * button_height); + scroll_container->scrollToOffset(static_cast(index) * button_height); return this; } -void GuiListbox::onDraw(sp::RenderTarget& renderer) +void GuiListbox::entriesChanged() { - hover = false; - const auto& back = back_style->get(getState()); - const auto& back_hover = back_style->get(State::Hover); - const auto& front = front_style->get(getState()); - const auto& back_selected = back_selected_style->get(getState()); - const auto& back_selected_hover = back_selected_style->get(State::Hover); - const auto& front_selected = front_selected_style->get(getState()); - - scroll->setValueSize(rect.size.y); - scroll->setRange(0, entries.size() * button_height); - - // Determine whether to show the scrollbar based on the total height of all - // items in the list. - if ((int)entries.size() <= rect.size.y / button_height) - scroll->hide(); - else - scroll->show(); - - // Draw the button. If the scrollbar is visible, make room. - sp::Rect button_rect{rect.position, {rect.size.x, button_height}}; - - if (scroll->isVisible()) - button_rect.size.x -= scroll->getRect().size.x; - - button_rect.position.y -= scroll->getValue(); - - // For each entry, draw a button. - int index = 0; - - for(auto& e : entries) { - // Draw the button only if it will visible within the container. - if (button_rect.position.y + button_rect.size.y >= rect.position.y - && button_rect.position.y <= rect.position.y + rect.size.y) - { - auto* b = button_rect.contains(hover_coordinates) ? &back_hover : &back; - auto* f = &front; - - // If this is the selected button, change the back and foreground. - if (index == selection_index) + // Create new buttons for entries that don't have one yet. + for (auto n = buttons.size(); n < entries.size(); n++) + { + auto* btn = new GuiToggleButton(scroll_container, id + "_ENTRY_" + string(static_cast(n)), entries[n].name, + [this, n](bool) { - b = button_rect.contains(hover_coordinates) ? &back_selected_hover : &back_selected; - f = &front_selected; + setSelectionIndex(n); + callback(); } - - // Draw the background texture. - renderer.drawStretchedHVClipped(button_rect, rect, button_height * 0.5f, b->texture, b->color); - - // Draw the icon, if one's defined. - // 60% button height and aligned left. - if (e.icon_name != "") - { - renderer.drawSpriteClipped( - e.icon_name, // icon - glm::vec2( // center position - button_rect.position.x + button_rect.size.y * 0.8f, - button_rect.position.y + button_rect.size.y * 0.5f - ), - button_rect.size.y * 0.6f, // size - rect, // clipping rectangle - f->color // color - ); - } - - // Prepare the foreground text style. - auto prepared = f->font->prepare(e.name, 32, text_size, f->color, button_rect.size, sp::Alignment::Center, sp::Font::FlagClip); - for(auto& c : prepared.data) - c.position.y -= rect.position.y - button_rect.position.y; - - // Draw the text. - renderer.drawText(rect, prepared, sp::Font::FlagClip); - } - - // Prepare to draw the next button below this one. - button_rect.position.y += button_height; - index += 1; + ); + btn + ->setStyle("listbox") // Use listbox-specific theme styles. + ->setTextSize(text_size) + ->setIconSize(icon_size) + ->setSize(GuiElement::GuiSizeMax, button_height); + buttons.push_back(btn); } -} -bool GuiListbox::onMouseDown(sp::io::Pointer::Button button, glm::vec2 position, sp::io::Pointer::ID id) -{ - int offset = (position.y - rect.position.y + scroll->getValue()) / button_height; - return offset >= 0 && offset < int(entries.size()); + updateButtonStates(); } -void GuiListbox::onMouseUp(glm::vec2 position, sp::io::Pointer::ID id) +void GuiListbox::updateButtonStates() { - int offset = (position.y - rect.position.y + scroll->getValue()) / button_height; - if (offset >= 0 && offset < int(entries.size())) { - soundManager->playSound("sfx/button.wav"); - setSelectionIndex(offset); - callback(); + // Select only one button in the list. + for (auto n = 0; n < buttons.size(); n++) + { + if (n < entries.size()) + { + buttons[n] + ->setValue(n == selection_index) + ->setText(entries[n].name) + ->setIcon(entries[n].icon_name) + ->show(); + } + else buttons[n]->hide(); } } - -bool GuiListbox::onMouseWheelScroll(glm::vec2 position, float value) -{ - float range = scroll->getCorrectedMax() - scroll->getMin(); - scroll->setValue((scroll->getValue() - value * range / mouse_scroll_steps) ); - return true; -} diff --git a/src/gui/gui2_listbox.h b/src/gui/gui2_listbox.h index 59998de4ab..d835c07dba 100644 --- a/src/gui/gui2_listbox.h +++ b/src/gui/gui2_listbox.h @@ -2,32 +2,42 @@ #include "gui2_entrylist.h" -class GuiScrollbar; +class GuiScrollContainer; +class GuiToggleButton; +// Creates a scrolling list of buttons to represent an entry list. Clicking a +// button selects that value in the list. Supports only one selection at a time. class GuiListbox : public GuiEntryList { protected: - float text_size; - float button_height; - sp::Alignment text_alignment; - GuiScrollbar* scroll; - sp::Rect last_rect; - int mouse_scroll_steps; - - const GuiThemeStyle* back_style; - const GuiThemeStyle* front_style; - const GuiThemeStyle* back_selected_style; - const GuiThemeStyle* front_selected_style; + // The font size for the buttons' text, in virtual pixels. + float text_size = 30.0f; + // The buttons' icon size, as a normalized factor of button height + // (0.0-1.0 = 0-100%). + float icon_size = 0.6f; + // The buttons' height, in virtual pixels. + float button_height = 50.0f; public: GuiListbox(GuiContainer* owner, string id, func_t func); + // Set the font size for the buttons' text, in virtual pixels. GuiListbox* setTextSize(float size); + // Set the buttons' icon size, as a normalized factor of button height + // (0.0-1.0 = 0-100%). + GuiListbox* setIconSize(float size); + // Set the buttons' height, in virtual pixels. GuiListbox* setButtonHeight(float height); - + // Scroll the listbox to the item with the given index. GuiListbox* scrollTo(int index); - virtual void onDraw(sp::RenderTarget& renderer) override; - virtual bool onMouseDown(sp::io::Pointer::Button button, glm::vec2 position, sp::io::Pointer::ID id) override; - virtual void onMouseUp(glm::vec2 position, sp::io::Pointer::ID id) override; - virtual bool onMouseWheelScroll(glm::vec2 position, float value) override; +private: + // Scrolling container wrapping the button list. + GuiScrollContainer* scroll_container; + // Vector of toggle buttons representing list items. + std::vector buttons; + + // Update the listbox when its entries change. + virtual void entriesChanged() override; + // Update listbox button selection states. + void updateButtonStates(); }; diff --git a/src/gui/gui2_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/gui2_togglebutton.cpp b/src/gui/gui2_togglebutton.cpp index 840f914d58..21642f4175 100644 --- a/src/gui/gui2_togglebutton.cpp +++ b/src/gui/gui2_togglebutton.cpp @@ -40,7 +40,7 @@ void GuiToggleButton::onDraw(sp::RenderTarget& renderer) text_rect.size.x = rect.size.x - rect.size.y; text_align = sp::Alignment::CenterRight; } - renderer.drawRotatedSprite(icon_name, glm::vec2(icon_x, rect.position.y + rect.size.y * 0.5f), rect.size.y * 0.8f, icon_rotation, front.color); + renderer.drawRotatedSprite(icon_name, glm::vec2(icon_x, rect.position.y + rect.size.y * 0.5f), rect.size.y * icon_size, icon_rotation, front.color); renderer.drawText(text_rect, text, text_align, text_size > 0 ? text_size : front.size, front.font, front.color); }else{ renderer.drawText(rect, text, sp::Alignment::Center, text_size > 0 ? text_size : front.size, front.font, front.color);