From 52074877d83e648072fabbfedcdcae6209c46cd8 Mon Sep 17 00:00:00 2001 From: Oznogon Date: Mon, 2 Mar 2026 17:25:24 -0800 Subject: [PATCH 01/10] 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 d71b5e5033b16d6ba372f5aed24c25ada8943a94 Mon Sep 17 00:00:00 2001 From: Oznogon Date: Mon, 9 Mar 2026 14:19:16 -0700 Subject: [PATCH 02/10] 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 4c0d2e1eb2472ef45db6b3b1a78c70b02ede1b08 Mon Sep 17 00:00:00 2001 From: oznogon Date: Wed, 11 Mar 2026 22:26:37 -0700 Subject: [PATCH 03/10] 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 386f4fc2f93eba2d8575cbc8c4c84aa371007b70 Mon Sep 17 00:00:00 2001 From: Oznogon Date: Fri, 13 Mar 2026 17:32:37 -0700 Subject: [PATCH 04/10] Simplify GuiScrollContainer scrollbar, add comments --- src/gui/gui2_scrollcontainer.cpp | 81 ++++++++++++++------------------ src/gui/gui2_scrollcontainer.h | 21 +++++++-- 2 files changed, 52 insertions(+), 50 deletions(-) diff --git a/src/gui/gui2_scrollcontainer.cpp b/src/gui/gui2_scrollcontainer.cpp index 03a1e09c7a..63c85a6127 100644 --- a/src/gui/gui2_scrollcontainer.cpp +++ b/src/gui/gui2_scrollcontainer.cpp @@ -7,23 +7,22 @@ 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. + // Don't lock content size to element. + // We need to manipulate content size when toggling scrollbar visibility. 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); - } + // 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) @@ -42,14 +41,14 @@ 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)); + 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)); + scrollbar_v->setValue(static_cast(scroll_offset)); } void GuiScrollContainer::updateLayout(const sp::Rect& rect) @@ -58,9 +57,10 @@ void GuiScrollContainer::updateLayout(const sp::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); + bool has_overflow = 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_visible ? scrollbar_width : 0.0f; + const float sb_width = scrollbar_v->isVisible() ? scrollbar_width : 0.0f; // Manually factor padding into content layout around the scrollbar. glm::vec2 padding_offset{ @@ -82,20 +82,17 @@ void GuiScrollContainer::updateLayout(const sp::Rect& rect) // Temporarily hide the scrollbar so the layout manager ignores it for // sizing, then restore it if enabled. - if (scrollbar_v) scrollbar_v->setVisible(false); + scrollbar_v->setVisible(false); layout_manager->updateLoop(*this, content_layout_rect); - if (scrollbar_v) - { - scrollbar_v->setVisible(scrollbar_visible); + 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} - }); - } + // 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; @@ -113,12 +110,9 @@ void GuiScrollContainer::updateLayout(const sp::Rect& rect) 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)); - } + 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) @@ -171,10 +165,7 @@ void GuiScrollContainer::drawElements(glm::vec2 mouse_position, sp::Rect /* pare renderer.popScissorRect(); // Draw the scrollbar. Never clip nor scroll the scrollbar itself. - if (scrollbar_v - && !scrollbar_v->isDestroyed() - && scrollbar_v->isVisible() - ) + if (!scrollbar_v->isDestroyed() && scrollbar_v->isVisible()) { setElementHover(scrollbar_v, scrollbar_v->getRect().contains(mouse_position)); scrollbar_v->onDraw(renderer); @@ -185,8 +176,7 @@ void GuiScrollContainer::drawElements(glm::vec2 mouse_position, sp::Rect /* pare 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() + if (scrollbar_v->isVisible() && scrollbar_v->isEnabled() && scrollbar_v->getRect().contains(position) ) @@ -321,8 +311,7 @@ void GuiScrollContainer::onMouseUp(glm::vec2 position, sp::io::Pointer::ID id) 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() + if (scrollbar_v->isVisible() && scrollbar_v->isEnabled() && scrollbar_v->getRect().contains(position)) { @@ -375,8 +364,8 @@ bool GuiScrollContainer::onMouseWheelScroll(glm::vec2 /* position */, float valu 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)); + // Update the scrollbar. + scrollbar_v->setValue(static_cast(scroll_offset)); return true; } @@ -396,5 +385,5 @@ sp::Rect GuiScrollContainer::getContentRect() const float GuiScrollContainer::getEffectiveScrollbarWidth() const { // Save room for the scrollbar only if it's visible. - return (scrollbar_v && scrollbar_visible) ? scrollbar_width : 0.0f; + return scrollbar_v->isVisible() ? scrollbar_width : 0.0f; } diff --git a/src/gui/gui2_scrollcontainer.h b/src/gui/gui2_scrollcontainer.h index 5185a444d5..73374cbe31 100644 --- a/src/gui/gui2_scrollcontainer.h +++ b/src/gui/gui2_scrollcontainer.h @@ -4,17 +4,18 @@ 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 }; - // 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 @@ -63,20 +64,32 @@ class GuiScrollContainer : public GuiElement 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; - GuiScrollbar* scrollbar_v = nullptr; + // 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; - bool scrollbar_visible = false; + // 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); }; From 8eb76fd3cbe9e0bc37557f0e10f2014344172f1b Mon Sep 17 00:00:00 2001 From: Oznogon Date: Fri, 13 Mar 2026 17:58:33 -0700 Subject: [PATCH 05/10] Hide scrollbar if ScrollMode is None If mode is ScrollMode::None, hide the scrollbar. This allows hiding the scrollbar by setting the mode, as in ShipsLog. --- src/gui/gui2_scrollcontainer.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/gui/gui2_scrollcontainer.cpp b/src/gui/gui2_scrollcontainer.cpp index 63c85a6127..807ef1b595 100644 --- a/src/gui/gui2_scrollcontainer.cpp +++ b/src/gui/gui2_scrollcontainer.cpp @@ -57,8 +57,9 @@ void GuiScrollContainer::updateLayout(const sp::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 = content_height > visible_height + 0.5f; + 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; @@ -164,8 +165,10 @@ void GuiScrollContainer::drawElements(glm::vec2 mouse_position, sp::Rect /* pare renderer.popTranslation(); renderer.popScissorRect(); - // Draw the scrollbar. Never clip nor scroll the scrollbar itself. - if (!scrollbar_v->isDestroyed() && scrollbar_v->isVisible()) + // 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); From fd4ef815912c657831a8fd9890b78e19866141e6 Mon Sep 17 00:00:00 2001 From: Oznogon Date: Fri, 13 Mar 2026 12:27:21 -0700 Subject: [PATCH 06/10] Move variable init to header; pragma once --- src/gui/gui2_scrolltext.cpp | 3 +-- src/gui/gui2_scrolltext.h | 11 ++++------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/gui/gui2_scrolltext.cpp b/src/gui/gui2_scrolltext.cpp index 2ce4922ecc..db40768816 100644 --- a/src/gui/gui2_scrolltext.cpp +++ b/src/gui/gui2_scrolltext.cpp @@ -3,9 +3,8 @@ GuiScrollText::GuiScrollText(GuiContainer* owner, string id, string text) -: GuiElement(owner, id), text(text), text_size(30), mouse_scroll_steps(25) +: GuiElement(owner, id), text(text) { - auto_scroll_down = false; scrollbar = new GuiScrollbar(this, id + "_SCROLL", 0, 1, 0, nullptr); scrollbar->setPosition(0, 0, sp::Alignment::TopRight)->setSize(50, GuiElement::GuiSizeMax); } diff --git a/src/gui/gui2_scrolltext.h b/src/gui/gui2_scrolltext.h index b6f7a8e434..6ca85c27b7 100644 --- a/src/gui/gui2_scrolltext.h +++ b/src/gui/gui2_scrolltext.h @@ -1,5 +1,4 @@ -#ifndef GUI_SCROLLTEXT_H -#define GUI_SCROLLTEXT_H +#pragma once #include "gui2_element.h" #include "gui2_scrollbar.h" @@ -9,9 +8,9 @@ class GuiScrollText : public GuiElement protected: GuiScrollbar* scrollbar; string text; - float text_size; - bool auto_scroll_down; - int mouse_scroll_steps; + float text_size = 30.0f; + bool auto_scroll_down = false; + int mouse_scroll_steps = 25; public: GuiScrollText(GuiContainer* owner, string id, string text); @@ -37,5 +36,3 @@ class GuiScrollFormattedText : public GuiScrollText virtual void onDraw(sp::RenderTarget& renderer) override; virtual bool onMouseWheelScroll(glm::vec2 position, float value) override; }; - -#endif//GUI_SCROLLTEXT_H From 7bb21c1e9a1d95a4e35421f1be47adca3937c172 Mon Sep 17 00:00:00 2001 From: Oznogon Date: Fri, 13 Mar 2026 14:07:41 -0700 Subject: [PATCH 07/10] Add GuiMultilineText, a GuiElement text subclass Add a GuiElement subclass for rendering multiline text, using the rendering logic from GuiScrollText, but without the font clipping flag enabled. This allows for multiline text to be rendered separately from scrolling implementations. --- CMakeLists.txt | 2 + src/gui/gui2_multilinetext.cpp | 108 +++++++++++++++++++++++++++++++++ src/gui/gui2_multilinetext.h | 41 +++++++++++++ 3 files changed, 151 insertions(+) create mode 100644 src/gui/gui2_multilinetext.cpp create mode 100644 src/gui/gui2_multilinetext.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 64aa8beac0..0433431973 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -158,6 +158,7 @@ set(GUI_LIB_SOURCES src/gui/gui2_entrylist.cpp src/gui/gui2_progressbar.cpp src/gui/gui2_progressslider.cpp + src/gui/gui2_multilinetext.cpp src/gui/gui2_scrolltext.cpp src/gui/gui2_scrollcontainer.cpp src/gui/gui2_advancedscrolltext.cpp @@ -198,6 +199,7 @@ set(GUI_LIB_SOURCES src/gui/gui2_rotationdial.h src/gui/gui2_scrollbar.h src/gui/gui2_scrollcontainer.h + src/gui/gui2_multilinetext.h src/gui/gui2_scrolltext.h src/gui/gui2_selector.h src/gui/gui2_slider.h diff --git a/src/gui/gui2_multilinetext.cpp b/src/gui/gui2_multilinetext.cpp new file mode 100644 index 0000000000..6f3fdfada5 --- /dev/null +++ b/src/gui/gui2_multilinetext.cpp @@ -0,0 +1,108 @@ +#include "gui2_multilinetext.h" +#include "theme.h" + +GuiMultilineText::GuiMultilineText(GuiContainer* owner, string id, string text) +: GuiElement(owner, id), text(text) +{ +} + +GuiMultilineText* GuiMultilineText::setText(string text) +{ + this->text = text; + return this; +} + +string GuiMultilineText::getText() const +{ + return text; +} + +void GuiMultilineText::onDraw(sp::RenderTarget& renderer) +{ + // Prepare the text in one batch. + auto prepared = sp::RenderTarget::getDefaultFont()->prepare( + this->text, + 32, + text_size, + selectColor(colorConfig.textbox.forground), + rect.size, + sp::Alignment::TopLeft, + sp::Font::FlagLineWrap + ); + + // Set the element size to match the text size, plus a buffer for + // descenders. + setSize(GuiElement::GuiSizeMax, prepared.getUsedAreaSize().y + text_size * 0.33f); + // Never resize this element to fill height. We always want the specified + // size. Since setSize resets fill_height to true, we have to flap it here. + layout.fill_height = false; + + // Draw the text in the rect with line wrapping. + renderer.drawText(rect, prepared, sp::Font::FlagLineWrap); +} + +GuiMultilineFormattedText::GuiMultilineFormattedText(GuiContainer* owner, string id, string text) +: GuiMultilineText(owner, id, text) +{ +} + +void GuiMultilineFormattedText::onDraw(sp::RenderTarget& renderer) +{ + auto main_color = selectColor(colorConfig.textbox.forground); + auto current_color = main_color; + // Each piece of tagged text needs formatting, so prepare incrementally + // instead of all at once. + auto prepared = sp::RenderTarget::getDefaultFont()->start( + 32, + rect.size, + sp::Alignment::TopLeft, + sp::Font::FlagLineWrap + ); + int last_end = 0; + float size_mod = 1.0f; + + // Prepare each substring and append it. + for (auto tag_start = text.find('<'); tag_start >= 0; tag_start = text.find('<', tag_start + 1)) + { + prepared.append(text.substr(last_end, tag_start), text_size * size_mod, current_color); + auto tag_end = text.find('>', tag_start + 1); + + if (tag_end != -1) + { + last_end = tag_end + 1; + auto tag = text.substr(tag_start + 1, tag_end); + + // Parse and apply tags. + if (tag == "/") + { + size_mod = 1.0f; + current_color = main_color; + } + else if (tag == "h1") size_mod = 2.0f; + else if (tag == "h2") size_mod = 1.5f; + else if (tag == "h3") size_mod = 1.17f; + else if (tag == "h4") size_mod = 1.0f; + else if (tag == "h5") size_mod = 0.83f; + else if (tag == "h6") size_mod = 0.67f; + else if (tag == "small") size_mod = 0.89f; + else if (tag == "large") size_mod = 1.2f; + else if (tag.startswith("color=")) + current_color = GuiTheme::toColor(tag.substr(6)); + else last_end = tag_start; + } + else last_end = tag_start; + } + + prepared.append(text.substr(last_end), text_size * size_mod, current_color); + prepared.finish(); + + // Set the element size to match the text size, plus a buffer for + // descenders. + setSize(GuiElement::GuiSizeMax, prepared.getUsedAreaSize().y + text_size * 0.33f); + // Never resize this element to fill height. We always want the specified + // size. Since setSize resets fill_height to true, we have to flap it here. + layout.fill_height = false; + + // Draw the text in the rect with line wrapping. + renderer.drawText(rect, prepared, sp::Font::FlagLineWrap); +} diff --git a/src/gui/gui2_multilinetext.h b/src/gui/gui2_multilinetext.h new file mode 100644 index 0000000000..1de6432707 --- /dev/null +++ b/src/gui/gui2_multilinetext.h @@ -0,0 +1,41 @@ +#pragma once + +#include "gui2_element.h" + +// A GuiElement that renders multiline text within its bounds, and without +// scrolling or clipping. To scroll or clip multiline text, instead use +// GuiScrollText. +class GuiMultilineText : public GuiElement +{ +protected: + // The text to render. + string text; + // Base font size, in virtual pixels. + float text_size = 30.0f; + +public: + GuiMultilineText(GuiContainer* owner, string id, string text); + + // Sets the element's text contents. Use control characters like \n to add + // line breaks. + GuiMultilineText* setText(string text); + // Returns the element's text. + string getText() const; + // Sets the font size to a value of at least 1px. + GuiMultilineText* setTextSize(float text_size) { this->text_size = std::max(1.0f, text_size); return this; } + + // Prepares and renders the text. + virtual void onDraw(sp::RenderTarget& renderer) override; +}; + +// A GuiMultilineText that also uses formatting tags to format the text. +// To scroll or clip formatted text, use GuiScrollFormattedText. +class GuiMultilineFormattedText : public GuiMultilineText +{ +public: + GuiMultilineFormattedText(GuiContainer* owner, string id, string text); + + // Overrides GuiMultilineText to apply formatting tags and incrementally + // prepare the text. + virtual void onDraw(sp::RenderTarget& renderer) override; +}; From 345d7c935b4c4bd8222529548333f1684b75c720 Mon Sep 17 00:00:00 2001 From: Oznogon Date: Fri, 13 Mar 2026 14:09:38 -0700 Subject: [PATCH 08/10] Refactor GuiScrollText, GuiScrollFormattedText Refactor scrolling text elements to use GuiScrollContainer and GuiMultilineText. This replaces the bespoke scrolling and clipping behaviors with those of GuiScrollContainer, and works around font preparation sizing issues with descenders by building in a buffer for them. While this reimplementation has the same interface as the prior GuiScrollText elements, its reparenting to GuiScrollContainer forces a change to method chaining order in the GM chat dialog. --- src/gui/gui2_scrolltext.cpp | 140 ++++------------------------------ src/gui/gui2_scrolltext.h | 38 +++++---- src/screens/gm/chatDialog.cpp | 2 +- 3 files changed, 38 insertions(+), 142 deletions(-) diff --git a/src/gui/gui2_scrolltext.cpp b/src/gui/gui2_scrolltext.cpp index db40768816..e10fa78bfe 100644 --- a/src/gui/gui2_scrolltext.cpp +++ b/src/gui/gui2_scrolltext.cpp @@ -1,144 +1,30 @@ #include "gui2_scrolltext.h" #include "theme.h" - GuiScrollText::GuiScrollText(GuiContainer* owner, string id, string text) -: GuiElement(owner, id), text(text) +: GuiScrollContainer(owner, id), text(text) { - scrollbar = new GuiScrollbar(this, id + "_SCROLL", 0, 1, 0, nullptr); - scrollbar->setPosition(0, 0, sp::Alignment::TopRight)->setSize(50, GuiElement::GuiSizeMax); + text_element = new GuiMultilineText(this, "", text); + text_element + ->setSize(GuiElement::GuiSizeMax, GuiElement::GuiSizeMax); } GuiScrollText* GuiScrollText::setText(string text) { this->text = text; + if (text_element) text_element->setText(text); + if (auto_scroll_down) scrollToFraction(1.0f); return this; } -string GuiScrollText::getText() const -{ - return text; -} - -GuiScrollText* GuiScrollText::setScrollbarWidth(float width) -{ - scrollbar->setSize(width, GuiElement::GuiSizeMax); - return this; -} - -void GuiScrollText::onDraw(sp::RenderTarget& renderer) -{ - auto text_rect = sp::Rect(rect.position.x, rect.position.y, rect.size.x - scrollbar->getSize().x, rect.size.y); - auto prepared = sp::RenderTarget::getDefaultFont()->prepare(this->text, 32, text_size, selectColor(colorConfig.textbox.forground), text_rect.size, sp::Alignment::TopLeft, sp::Font::FlagClip | sp::Font::FlagLineWrap); - auto text_draw_size = prepared.getUsedAreaSize(); - - int scroll_max = text_draw_size.y; - if (scrollbar->getMax() != scroll_max) - { - int diff = scroll_max - scrollbar->getMax(); - scrollbar->setRange(0, scroll_max); - scrollbar->setValueSize(text_rect.size.y); - if (auto_scroll_down) - scrollbar->setValue(scrollbar->getValue() + diff); - } - - if (text_rect.size.y >= text_draw_size.y) - { - scrollbar->hide(); - renderer.drawText(rect, prepared, sp::Font::FlagClip | sp::Font::FlagLineWrap); - } - else - { - for(auto& g : prepared.data) - g.position.y -= scrollbar->getValue(); - scrollbar->show(); - renderer.drawText(text_rect, prepared, sp::Font::FlagClip | sp::Font::FlagLineWrap); - } -} - -bool GuiScrollText::onMouseWheelScroll(glm::vec2 position, float value) -{ - float range = scrollbar->getCorrectedMax() - scrollbar->getMin(); - scrollbar->setValue((scrollbar->getValue() - value * range / mouse_scroll_steps) ); - return true; -} - GuiScrollFormattedText::GuiScrollFormattedText(GuiContainer* owner, string id, string text) : GuiScrollText(owner, id, text) { + // Replace the plain text element created by the base constructor with a + // formatted one. Mark the old element for deletion and reassign text_element + // so base class methods (setText, setTextSize) work through the new element. + text_element->destroy(); + text_element = new GuiMultilineFormattedText(this, "", text); + text_element + ->setSize(GuiElement::GuiSizeMax, GuiElement::GuiSizeMax); } - -void GuiScrollFormattedText::onDraw(sp::RenderTarget& renderer) -{ - auto main_color = selectColor(colorConfig.textbox.forground); - auto current_color = main_color; - auto text_rect = sp::Rect(rect.position.x, rect.position.y, rect.size.x - scrollbar->getSize().x, rect.size.y); - auto prepared = sp::RenderTarget::getDefaultFont()->start(32, text_rect.size, sp::Alignment::TopLeft, sp::Font::FlagClip | sp::Font::FlagLineWrap); - int last_end = 0; - float size_mod = 1.0f; - for(auto tag_start = text.find('<'); tag_start >= 0; tag_start = text.find('<', tag_start+1)) { - prepared.append(text.substr(last_end, tag_start), text_size * size_mod, current_color); - auto tag_end = text.find('>', tag_start+1); - if (tag_end != -1) { - last_end = tag_end + 1; - auto tag = text.substr(tag_start + 1, tag_end); - if (tag == "/") { - size_mod = 1.0f; - current_color = main_color; - } else if (tag == "h1") { - size_mod = 2.0f; - } else if (tag == "h2") { - size_mod = 1.5f; - } else if (tag == "h3") { - size_mod = 1.17f; - } else if (tag == "h4") { - size_mod = 1.0f; - } else if (tag == "h5") { - size_mod = 0.83f; - } else if (tag == "h6") { - size_mod = 0.67f; - } else if (tag == "small") { - size_mod = 0.89f; - } else if (tag == "large") { - size_mod = 1.2f; - } else if (tag.startswith("color=")) { - current_color = GuiTheme::toColor(tag.substr(6)); - } else { - last_end = tag_start; - } - } else { - last_end = tag_start; - } - } - prepared.append(text.substr(last_end), text_size * size_mod, current_color); - prepared.finish(); - auto text_draw_size = prepared.getUsedAreaSize(); - - int scroll_max = text_draw_size.y; - if (scrollbar->getMax() != scroll_max) - { - int diff = scroll_max - scrollbar->getMax(); - scrollbar->setRange(0, scroll_max); - scrollbar->setValueSize(text_rect.size.y); - if (auto_scroll_down) - scrollbar->setValue(scrollbar->getValue() + diff); - } - - if (text_rect.size.y >= text_draw_size.y) - { - scrollbar->hide(); - renderer.drawText(rect, prepared, sp::Font::FlagClip | sp::Font::FlagLineWrap); - } - else - { - for(auto& g : prepared.data) - g.position.y -= scrollbar->getValue(); - scrollbar->show(); - renderer.drawText(text_rect, prepared, sp::Font::FlagClip | sp::Font::FlagLineWrap); - } -} - -bool GuiScrollFormattedText::onMouseWheelScroll(glm::vec2 position, float value) -{ - return GuiScrollText::onMouseWheelScroll(position, value); -} \ No newline at end of file diff --git a/src/gui/gui2_scrolltext.h b/src/gui/gui2_scrolltext.h index 6ca85c27b7..eecae0d269 100644 --- a/src/gui/gui2_scrolltext.h +++ b/src/gui/gui2_scrolltext.h @@ -1,38 +1,48 @@ #pragma once -#include "gui2_element.h" -#include "gui2_scrollbar.h" +#include "gui2_scrollcontainer.h" +#include "gui2_multilinetext.h" -class GuiScrollText : public GuiElement +// A GuiScrollContainer wrapper for GuiMultilineText that maintains backward +// compatibility with legacy GuiScrollText. +class GuiScrollText : public GuiScrollContainer { protected: - GuiScrollbar* scrollbar; + // The text to render. Passed directly to GuiMultilineText. string text; + // Base font size, in virtual pixels. Passed directly to GuiMultilineText. float text_size = 30.0f; + // Determines whether to automatically scroll text to the bottom when the + // text changes. bool auto_scroll_down = false; + // Sets the mouse scroll interval as the number of scrollbar steps from top + // to bottom. (Not implemented yet) int mouse_scroll_steps = 25; + // The multiline text element. GuiScrollFormattedText overwrites this with a + // GuiMultilineFormattedText element. + GuiMultilineText* text_element; public: GuiScrollText(GuiContainer* owner, string id, string text); + // Enables automatic scrolling to the bottom when the text changes. GuiScrollText* enableAutoScrollDown() { auto_scroll_down = true; return this; } + // Disables automatic scrolling to the bottom when the text changes. GuiScrollText* disableAutoScrollDown() { auto_scroll_down = false; return this; } + // Sets the element's text contents. Use control characters like \n to add + // line breaks. If automatic scrolling is enabled, this triggers it. GuiScrollText* setText(string text); - string getText() const; - GuiScrollText* setTextSize(float text_size) { this->text_size = text_size; return this; } - - GuiScrollText* setScrollbarWidth(float width); - - virtual void onDraw(sp::RenderTarget& renderer) override; - virtual bool onMouseWheelScroll(glm::vec2 position, float value) override; + // Returns the element's text. + string getText() const { return text; } + // Sets the font size to a value of at least 1px. + GuiScrollText* setTextSize(float text_size) { text_element->setTextSize(text_size); return this; } }; +// A GuiScrollText wrapper for GuiMultilineFormattedText, reusing everything but +// text_element. class GuiScrollFormattedText : public GuiScrollText { public: GuiScrollFormattedText(GuiContainer* owner, string id, string text); - - virtual void onDraw(sp::RenderTarget& renderer) override; - virtual bool onMouseWheelScroll(glm::vec2 position, float value) override; }; diff --git a/src/screens/gm/chatDialog.cpp b/src/screens/gm/chatDialog.cpp index f2e97838f8..ea6bef72b9 100644 --- a/src/screens/gm/chatDialog.cpp +++ b/src/screens/gm/chatDialog.cpp @@ -18,7 +18,7 @@ GameMasterChatDialog::GameMasterChatDialog(GuiContainer* owner, GuiRadarView* ra this->radar = radar; chat_text = new GuiScrollText(contents, "GM_CHAT_TEXT", ""); - chat_text->enableAutoScrollDown()->setScrollbarWidth(25)->setTextSize(20)->setSize(GuiElement::GuiSizeMax, GuiElement::GuiSizeMax); + chat_text->enableAutoScrollDown()->setTextSize(20)->setScrollbarWidth(25)->setSize(GuiElement::GuiSizeMax, GuiElement::GuiSizeMax); text_entry = new GuiTextEntry(contents, "GM_CHAT_ENTRY", ""); text_entry->setTextSize(20)->setSize(GuiElement::GuiSizeMax, 25.0f)->setMargins(0.0f, 10.0f, 0.0f, 0.0f); From 2d7c534cfab6aecaf6f42af25e415209cc5d8ceb Mon Sep 17 00:00:00 2001 From: Oznogon Date: Fri, 13 Mar 2026 18:09:54 -0700 Subject: [PATCH 09/10] Refactor GuiAdvancedScrollText Refactor GuiAdvancedScrollText as a subclass of GuiScrollContainer, and replace its bespoke clipping and scrolling behaviors with GuiScrollContainer's. This implements an EntryCanvas subelement as a subclass of GuiElement to serve as the child element being scrolled, and render the prefix and entry text into the canvas. --- src/gui/gui2_advancedscrolltext.cpp | 238 +++++++++++++++++----------- src/gui/gui2_advancedscrolltext.h | 68 +++++--- 2 files changed, 193 insertions(+), 113 deletions(-) diff --git a/src/gui/gui2_advancedscrolltext.cpp b/src/gui/gui2_advancedscrolltext.cpp index 4c0cec0c12..feec396a15 100644 --- a/src/gui/gui2_advancedscrolltext.cpp +++ b/src/gui/gui2_advancedscrolltext.cpp @@ -1,12 +1,78 @@ #include "gui2_advancedscrolltext.h" GuiAdvancedScrollText::GuiAdvancedScrollText(GuiContainer* owner, string id) -: GuiElement(owner, id), text_size(30.0f), rect_width(rect.size.x), max_prefix_width(0.0f), mouse_scroll_steps(25) +: GuiScrollContainer(owner, id) { - scrollbar = new GuiScrollbar(this, id + "_SCROLL", 0, 1, 0, nullptr); - scrollbar->setPosition(0, 0, sp::Alignment::TopRight)->setSize(50, GuiElement::GuiSizeMax); - // Calculate scrolling a one-line entry by scrollbar arrow buttons. - scrollbar->setClickChange(sp::RenderTarget::getDefaultFont()->prepare("1", 32, text_size, {255, 255, 255, 255}, rect.size, sp::Alignment::TopLeft).getUsedAreaSize().y); + entry_canvas = new EntryCanvas(this, id + "_CANVAS"); + entry_canvas->setSize(GuiElement::GuiSizeMax, GuiElement::GuiSizeMax); +} + +GuiAdvancedScrollText::EntryCanvas::EntryCanvas(GuiAdvancedScrollText* owner, const string& id) +: GuiElement(owner, id) +{ +} + +void GuiAdvancedScrollText::EntryCanvas::onDraw(sp::RenderTarget& renderer) +{ + auto* scroll_text = static_cast(owner); + + // Re-prep all entries if the canvas width has changed. Two passes: first + // collect prefix widths to find the max, then prep text with that width. + if (prev_canvas_width != rect.size.x) + { + prev_canvas_width = rect.size.x; + scroll_text->prefix_widths.clear(); + scroll_text->max_prefix_width = 0.0f; + + for (auto& e : scroll_text->entries) + { + scroll_text->prepEntryPrefix(e, prev_canvas_width); + const float pw = e.prepared_prefix.getUsedAreaSize().x; + scroll_text->prefix_widths[pw] += 1; + scroll_text->max_prefix_width = std::max(scroll_text->max_prefix_width, pw); + } + + const float text_col_width = prev_canvas_width - scroll_text->max_prefix_width; + for (auto& e : scroll_text->entries) + scroll_text->prepEntryText(e, text_col_width); + } + + // Render entries stacked top-to-bottom, preserving the leading gap from the + // original implementation so that ascenders of the first line are visible. + float y_offset = scroll_text->text_size + 12.0f; + + for (auto& e : scroll_text->entries) + { + const float height = e.prepared_text.getUsedAreaSize().y; + const float y_start = e.prepared_prefix.data.empty() ? 0.0f + : e.prepared_prefix.data[0].position.y; + + // Copy prepared strings and remap their y positions to y_offset. + auto draw_prefix = e.prepared_prefix; + auto draw_text = e.prepared_text; + + for (auto& g : draw_prefix.data) + g.position.y = y_offset; + for (auto& g : draw_text.data) + g.position.y = (g.position.y - y_start) + y_offset; + + renderer.drawText(rect, draw_prefix); + renderer.drawText( + sp::Rect{ + rect.position.x + scroll_text->max_prefix_width, + rect.position.y, + rect.size.x - scroll_text->max_prefix_width, + rect.size.y + }, + draw_text + ); + + y_offset += height; + } + + // Auto-size the canvas height to fit all entries. + setSize(GuiElement::GuiSizeMax, y_offset); + layout.fill_height = false; } GuiAdvancedScrollText* GuiAdvancedScrollText::addEntry(string prefix, string text, glm::u8vec4 color, unsigned int seq) @@ -16,20 +82,81 @@ GuiAdvancedScrollText* GuiAdvancedScrollText::addEntry(string prefix, string tex entry.text = text; entry.color = color; entry.seq = seq; - prepEntry(entry); + + const float canvas_width = entry_canvas->getRect().size.x; + + // Prep the prefix to get its width. + prepEntryPrefix(entry, canvas_width); + const float new_prefix_width = entry.prepared_prefix.getUsedAreaSize().x; + prefix_widths[new_prefix_width] += 1; + + if (new_prefix_width > max_prefix_width) + { + // New prefix is wider: re-prep all existing entries' text with the + // narrower text column. + max_prefix_width = new_prefix_width; + const float text_col_width = canvas_width - max_prefix_width; + for (auto& e : entries) + prepEntryText(e, text_col_width); + } + else + { + prepEntryText(entry, canvas_width - max_prefix_width); + } + + if (auto_scroll_down) scrollToFraction(1.0f); return this; } -unsigned int GuiAdvancedScrollText::getEntryCount() const +GuiAdvancedScrollText* GuiAdvancedScrollText::setTextSize(float new_text_size) { - return entries.size(); + text_size = std::max(1.0f, new_text_size); + + // Re-prep all entries with the new text size. The EntryCanvas::onDraw + // resize path will also catch this on next draw if canvas width changed, + // but we force it here for the height-only case. + const float canvas_width = entry_canvas->getRect().size.x; + if (canvas_width > 0.0f) + { + prefix_widths.clear(); + max_prefix_width = 0.0f; + + for (auto& e : entries) + { + prepEntryPrefix(e, canvas_width); + const float pw = e.prepared_prefix.getUsedAreaSize().x; + prefix_widths[pw] += 1; + max_prefix_width = std::max(max_prefix_width, pw); + } + + const float text_col_width = canvas_width - max_prefix_width; + for (auto& e : entries) + prepEntryText(e, text_col_width); + } + + return this; } -GuiAdvancedScrollText* GuiAdvancedScrollText::setTextSize(float text_size) +void GuiAdvancedScrollText::prepEntryPrefix(Entry& e, float canvas_width) { - this->text_size = std::max(1.0F, text_size); - scrollbar->setClickChange(sp::RenderTarget::getDefaultFont()->prepare("1", 32, text_size, {255, 255, 255, 255}, rect.size, sp::Alignment::TopLeft).getUsedAreaSize().y); - return this; + e.prepared_prefix = sp::RenderTarget::getDefaultFont()->prepare( + e.prefix, 32, text_size, {255, 255, 255, 255}, + {canvas_width, 10000.0f}, sp::Alignment::TopLeft + ); +} + +void GuiAdvancedScrollText::prepEntryText(Entry& e, float text_column_width) +{ + e.prepared_text = sp::RenderTarget::getDefaultFont()->prepare( + e.text, 32, text_size, e.color, + {text_column_width, 10000.0f}, + sp::Alignment::TopLeft, sp::Font::FlagLineWrap + ); +} + +unsigned int GuiAdvancedScrollText::getEntryCount() const +{ + return entries.size(); } string GuiAdvancedScrollText::getEntryText(int index) const @@ -39,15 +166,6 @@ string GuiAdvancedScrollText::getEntryText(int index) const return entries[index].text; } -GuiAdvancedScrollText::Entry GuiAdvancedScrollText::prepEntry(GuiAdvancedScrollText::Entry& e){ - e.prepared_prefix = sp::RenderTarget::getDefaultFont()->prepare(e.prefix, 32, text_size, {255, 255, 255, 255}, rect.size, sp::Alignment::TopLeft); - const float entry_prefix_width = e.prepared_prefix.getUsedAreaSize().x; - prefix_widths[entry_prefix_width] += 1; - max_prefix_width = std::max(max_prefix_width, entry_prefix_width); - e.prepared_text = sp::RenderTarget::getDefaultFont()->prepare(e.text, 32, text_size, e.color, {rect.size.x - max_prefix_width - 50.0f, rect.size.y}, sp::Alignment::TopLeft, sp::Font::FlagLineWrap | sp::Font::FlagClip); - return e; -} - unsigned int GuiAdvancedScrollText::getEntrySeq(int index) const { if (index < 0 || index >= static_cast(getEntryCount())) @@ -60,19 +178,15 @@ GuiAdvancedScrollText* GuiAdvancedScrollText::removeEntry(int index) if (index < 0 || index >= static_cast(getEntryCount())) return this; - // Find new max prefix if entry was the last one with the current max const float entry_prefix_width = entries[index].prepared_prefix.getUsedAreaSize().x; - bool last_with_width = false; - if(--prefix_widths[entry_prefix_width] == 0){ - last_with_width = true; + if (--prefix_widths[entry_prefix_width] == 0) + { prefix_widths.erase(entry_prefix_width); - } - if (entry_prefix_width == max_prefix_width && last_with_width){ - max_prefix_width = prefix_widths.end()->first; + if (entry_prefix_width == max_prefix_width) + max_prefix_width = prefix_widths.empty() ? 0.0f : prefix_widths.rbegin()->first; } entries.erase(entries.begin() + index); - return this; } @@ -80,70 +194,6 @@ GuiAdvancedScrollText* GuiAdvancedScrollText::clearEntries() { entries.clear(); prefix_widths.clear(); - max_prefix_width = 0; + max_prefix_width = 0.0f; return this; } - -void GuiAdvancedScrollText::onDraw(sp::RenderTarget& renderer) -{ - const bool is_resized = rect_width != rect.size.x; - if (is_resized) { - rect_width = rect.size.x; - prefix_widths.clear(); - max_prefix_width = 0; - } - - //Draw the visible entries - float draw_offset = -scrollbar->getValue() + text_size + 12.0f; - - for(Entry& e : entries) - { - // Window width has changed. Re-prep fonts. - if (is_resized){ prepEntry(e); } - - const float height = e.prepared_text.getUsedAreaSize().y; - - if (draw_offset + height > 0 - && draw_offset < rect.size.y) - { - const float y_start = e.prepared_prefix.data[0].position.y; - - auto prepared_prefix = e.prepared_prefix; - auto prepared_text = e.prepared_text; - for(auto& g : prepared_prefix.data) - { - g.position.y = draw_offset; - } - for(auto& g : prepared_text.data) - { - g.position.y = (g.position.y - y_start) + draw_offset; - } - renderer.drawText(rect, prepared_prefix, sp::Font::FlagClip); - renderer.drawText(sp::Rect(rect.position.x + max_prefix_width, rect.position.y, rect.size.x - 50 - max_prefix_width, rect.size.y), prepared_text, sp::Font::FlagClip); - } - - draw_offset += height; - } - - //Calculate how many lines we have to display in total. - const int line_count = (draw_offset - text_size - 12.0f) + scrollbar->getValue(); - - //Check if we need to update the scroll bar. - if (scrollbar->getMax() != line_count) - { - const int diff = line_count - scrollbar->getMax(); - scrollbar->setRange(0, line_count); - scrollbar->setValueSize(rect.size.y); - if (auto_scroll_down) - scrollbar->setValue(scrollbar->getValue() + diff); - } - - scrollbar->setVisible(rect.size.y > 100); -} - -bool GuiAdvancedScrollText::onMouseWheelScroll(glm::vec2 position, float value) -{ - float range = scrollbar->getCorrectedMax() - scrollbar->getMin(); - scrollbar->setValue((scrollbar->getValue() - value * range / mouse_scroll_steps) ); - return true; -} diff --git a/src/gui/gui2_advancedscrolltext.h b/src/gui/gui2_advancedscrolltext.h index f5de780c72..302a441cbc 100644 --- a/src/gui/gui2_advancedscrolltext.h +++ b/src/gui/gui2_advancedscrolltext.h @@ -1,12 +1,15 @@ -#ifndef GUI2_ADVANCEDSCROLLTEXT_H -#define GUI2_ADVANCEDSCROLLTEXT_H +#pragma once -#include "gui2_element.h" -#include "gui2_scrollbar.h" +#include "gui2_scrollcontainer.h" -class GuiAdvancedScrollText : public GuiElement +// A GuiScrollContainer for log-like scrolling text composed of individual, +// formatted entries, each with a prefix. Used for the ship's log control. +// For typical scrolling text usage, use GuiScrollText instead. +class GuiAdvancedScrollText : public GuiScrollContainer { protected: + // Defines an entry to add to the text, including its prefix, color, and + // order in the list. class Entry { public: @@ -17,34 +20,61 @@ class GuiAdvancedScrollText : public GuiElement glm::u8vec4 color; unsigned int seq; }; - + // A vector of Entries to render as text. std::vector entries; - GuiScrollbar* scrollbar; - float text_size; - float rect_width; - float max_prefix_width; + + // Base font size, in virtual pixels. + float text_size = 30.0f; + // Define the maximum width of the prefix column, in virtual pixels. + float max_prefix_width = 0.0f; + // Map custom prefix widths to entries. std::map prefix_widths; - bool auto_scroll_down; - Entry prepEntry(Entry& e); - int mouse_scroll_steps; + // Determines whether to automatically scroll text to the bottom when the + // text changes. + bool auto_scroll_down = false; + // Sets the mouse scroll interval as the number of scrollbar steps from top + // to bottom. (Not implemented yet) + int mouse_scroll_steps = 25; + + // Prepare an entry's prefix string for the given canvas width. Doesn't + // update prefix_widths or max_prefix_width. + void prepEntryPrefix(Entry& e, float canvas_width); + // Prepare an entry's text string for the given column width. + void prepEntryText(Entry& e, float text_column_width); + + // Inner element that renders all entries and auto-sizes its height, allowing + // GuiScrollContainer to clip and scroll the content. + class EntryCanvas : public GuiElement + { + public: + EntryCanvas(GuiAdvancedScrollText* owner, const string& id); + virtual void onDraw(sp::RenderTarget& renderer) override; + private: + // Track the canvas width for resizing. + float prev_canvas_width = 0.0f; + }; + EntryCanvas* entry_canvas; public: GuiAdvancedScrollText(GuiContainer* owner, string id); + // Enables automatic scrolling to the bottom when the text changes. GuiAdvancedScrollText* enableAutoScrollDown() { auto_scroll_down = true; return this; } + // Disables automatic scrolling to the bottom when the text changes. GuiAdvancedScrollText* disableAutoScrollDown() { auto_scroll_down = false; return this; } - + // Adds an entry to the list at the given sequence order. GuiAdvancedScrollText* addEntry(string prefix, string text, glm::u8vec4 color, unsigned int seq); + // Sets the font size to a value of at least 1px. GuiAdvancedScrollText* setTextSize(float text_size); + // Returns the number of recorded entries. unsigned int getEntryCount() const; + // Returns the text of the entry with the given index. string getEntryText(int index) const; + // Returns the sequential order value of the entry with the given index. unsigned int getEntrySeq(int index) const; + // Removes the entry with the given index. GuiAdvancedScrollText* removeEntry(int index); + // Removes all entries from the list. GuiAdvancedScrollText* clearEntries(); - - virtual void onDraw(sp::RenderTarget& renderer) override; - virtual bool onMouseWheelScroll(glm::vec2 position, float value) override; }; - -#endif//GUI2_ADVANCEDSCROLLTEXT_H From 3edbe8ff5ae9918c191d92b0b0f822951d8088fe Mon Sep 17 00:00:00 2001 From: Oznogon Date: Fri, 13 Mar 2026 18:10:33 -0700 Subject: [PATCH 10/10] Refactor ShipsLog with GuiAdvancedScrollText updates --- src/screenComponents/shipsLogControl.cpp | 98 +++++++++++++++++------- src/screenComponents/shipsLogControl.h | 7 +- 2 files changed, 72 insertions(+), 33 deletions(-) diff --git a/src/screenComponents/shipsLogControl.cpp b/src/screenComponents/shipsLogControl.cpp index c6d21079a3..0281c61ae3 100644 --- a/src/screenComponents/shipsLogControl.cpp +++ b/src/screenComponents/shipsLogControl.cpp @@ -8,15 +8,20 @@ ShipsLog::ShipsLog(GuiContainer* owner) : GuiElement(owner, "") { - setPosition(0, 0, sp::Alignment::BottomCenter); - setSize(GuiElement::GuiSizeMax, 50); - setMargins(20, 0); + // Start closed and at the bottom center. + setPosition(0.0f, 0.0f, sp::Alignment::BottomCenter); + setSize(GuiElement::GuiSizeMax, 50.0f); + setMargins(20.0f, 0.0f); open = false; log_text = new GuiAdvancedScrollText(this, ""); - log_text->enableAutoScrollDown(); - log_text->setMargins(15, 4, 15, 0)->setPosition(0, 0)->setSize(GuiElement::GuiSizeMax, GuiElement::GuiSizeMax); + log_text + ->enableAutoScrollDown() + ->setMode(GuiScrollContainer::ScrollMode::None) + ->setMargins(SIDE_MARGINS, 0.0f) + ->setPosition(0.0f, 0.0f) + ->setSize(GuiElement::GuiSizeMax, GuiElement::GuiSizeMax); } void ShipsLog::onDraw(sp::RenderTarget& renderer) @@ -24,50 +29,73 @@ void ShipsLog::onDraw(sp::RenderTarget& renderer) renderer.drawStretchedHV(sp::Rect(rect.position.x, rect.position.y, rect.size.x, rect.size.y + 100), 25.0f, "gui/widget/PanelBackground.png"); auto logs = my_spaceship.getComponent(); - if (!logs) - return; + if (!logs) return; + // If the log is now empty, clear any displayed entries. + if (log_text->getEntryCount() > 0 && logs->size() == 0) + log_text->clearEntries(); + + // If the log screen is open, display all entries in the log. if (open) { - if (log_text->getEntryCount() > 0 && logs->size() == 0) - log_text->clearEntries(); + // Add a top margin to log text in order to prevent it from visually + // overflowing the top of GuiPanel while scrolling. + log_text->setMargins(SIDE_MARGINS, 5.0f); - while(log_text->getEntryCount() > logs->size()) - { + // Clear displayed entries until the list of displayed entries isn't + // longer than the log. + while (log_text->getEntryCount() > logs->size()) log_text->removeEntry(0); - } - if (log_text->getEntryCount() > 0 && logs->size() > 0 && log_text->getEntryText(0) != logs->get(0).text) + // If the log is longer than the list of displayed entries, and the last + // entry isn't the same in both the log and the displayed list, check + // for updates and flag if so. + if (log_text->getEntryCount() > 0 + && logs->size() > 0 + && log_text->getEntryText(0) != logs->get(0).text) { bool updated = false; - for(unsigned int n=1; ngetEntryCount(); n++) + for (unsigned int n = 1; n < log_text->getEntryCount(); n++) { if (log_text->getEntryText(n) == logs->get(0).text) { - for(unsigned int m=0; mremoveEntry(0); + for (unsigned int m = 0; m < n; m++) log_text->removeEntry(0); updated = true; break; } } - if (!updated) - log_text->clearEntries(); + + // If no updates, clear the displayed list. + if (!updated) log_text->clearEntries(); } - while(log_text->getEntryCount() < logs->size()) + // Display new entries until the list of displayed entries is no longer + // smaller than the log. + while (log_text->getEntryCount() < logs->size()) { int n = log_text->getEntryCount(); log_text->addEntry(logs->get(n).prefix, logs->get(n).text, logs->get(n).color, 0); } - }else{ - if (log_text->getEntryCount() > 0 && logs->size() == 0) + } + // Otherwise, display only the last entry. + else + { + // Remove top margin from log text while closed to prevent bottom of + // text from being clipped by the screen edge. + log_text->setMargins(SIDE_MARGINS, 0.0f); + // Lock offset to top. + log_text->scrollToOffset(0.0f); + + // Clear displayed entries unless the last entry hasn't changed. + if (log_text->getEntryCount() > 0 + && logs->size() > 0 + && log_text->getEntryText(0) != logs->get(logs->size() - 1).text + ) log_text->clearEntries(); - if (log_text->getEntryCount() > 0 && logs->size() > 0) + + // If the log has changed, display the last one. + if (log_text->getEntryCount() == 0 && logs->size() > 0) { - if (log_text->getEntryText(0) != logs->get(logs->size()-1).text) - log_text->clearEntries(); - } - if (log_text->getEntryCount() == 0 && logs->size() > 0) { const auto& back = logs->get(logs->size() - 1); log_text->addEntry(back.prefix, back.text, back.color, 0); } @@ -76,10 +104,24 @@ void ShipsLog::onDraw(sp::RenderTarget& renderer) bool ShipsLog::onMouseDown(sp::io::Pointer::Button button, glm::vec2 position, sp::io::Pointer::ID id) { + // Toggle the open state on click. open = !open; + + // If the log's now open, expand it. if (open) - setSize(getSize().x, 800); + { + setSize(getSize().x, 800.0f); + // Show scrollbar, scroll to bottom. + log_text->setMode(GuiScrollContainer::ScrollMode::Scroll); + log_text->scrollToFraction(1.0f); + } + // If the log's now closed, contract it to one line. else - setSize(getSize().x, 50); + { + // Hide scrollbar. + setSize(getSize().x, 50.0f); + log_text->setMode(GuiScrollContainer::ScrollMode::None); + } + return true; } diff --git a/src/screenComponents/shipsLogControl.h b/src/screenComponents/shipsLogControl.h index 1128a82728..554e7c332a 100644 --- a/src/screenComponents/shipsLogControl.h +++ b/src/screenComponents/shipsLogControl.h @@ -1,9 +1,7 @@ -#ifndef SHIPS_LOG_CONTROL_H -#define SHIPS_LOG_CONTROL_H +#pragma once #include "gui/gui2_element.h" -class GuiPanel; class GuiAdvancedScrollText; class ShipsLog : public GuiElement @@ -16,6 +14,5 @@ class ShipsLog : public GuiElement private: bool open; GuiAdvancedScrollText* log_text; + const float SIDE_MARGINS = 15.0f; }; - -#endif//SHIPS_LOG_CONTROL_H