diff --git a/CMakeLists.txt b/CMakeLists.txt index b5a387d050..a927127e86 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -157,7 +157,9 @@ 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 src/gui/gui2_button.cpp src/gui/gui2_resizabledialog.cpp @@ -194,6 +196,8 @@ 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_multilinetext.h src/gui/gui2_scrolltext.h src/gui/gui2_selector.h src/gui/gui2_slider.h 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 diff --git a/src/gui/gui2_container.cpp b/src/gui/gui2_container.cpp index 9c87721289..b6dcdbcc7c 100644 --- a/src/gui/gui2_container.cpp +++ b/src/gui/gui2_container.cpp @@ -4,7 +4,7 @@ GuiContainer::~GuiContainer() { - for(GuiElement* element : children) + for (GuiElement* element : children) { element->owner = nullptr; delete element; @@ -13,15 +13,14 @@ GuiContainer::~GuiContainer() void GuiContainer::drawElements(glm::vec2 mouse_position, sp::Rect parent_rect, sp::RenderTarget& renderer) { - for(auto it = children.begin(); it != children.end(); ) + for (auto it = children.begin(); it != children.end(); ) { GuiElement* element = *it; if (element->destroyed) { //Find the owning cancas, as we need to remove ourselves if we are the focus or click element. GuiCanvas* canvas = dynamic_cast(element->getTopLevelContainer()); - if (canvas) - canvas->unfocusElementTree(element); + if (canvas) canvas->unfocusElementTree(element); //Delete it from our list. it = children.erase(it); @@ -29,7 +28,9 @@ void GuiContainer::drawElements(glm::vec2 mouse_position, sp::Rect parent_rect, // Free up the memory used by the element. element->owner = nullptr; delete element; - }else{ + } + else + { element->hover = element->rect.contains(mouse_position); if (element->visible) @@ -61,49 +62,45 @@ void GuiContainer::drawDebugElements(sp::Rect parent_rect, sp::RenderTarget& ren GuiElement* GuiContainer::getClickElement(sp::io::Pointer::Button button, glm::vec2 position, sp::io::Pointer::ID id) { - for(auto it = children.rbegin(); it != children.rend(); it++) + for (auto it = children.rbegin(); it != children.rend(); it++) { GuiElement* element = *it; if (element->visible && element->enabled && element->rect.contains(position)) { GuiElement* clicked = element->getClickElement(button, position, id); - if (clicked) - return clicked; - if (element->onMouseDown(button, position, id)) - { - return element; - } + if (clicked) return clicked; + if (element->onMouseDown(button, position, id)) return element; } } + return nullptr; } GuiElement* GuiContainer::executeScrollOnElement(glm::vec2 position, float value) { - for(auto it = children.rbegin(); it != children.rend(); it++) + for (auto it = children.rbegin(); it != children.rend(); it++) { GuiElement* element = *it; if (element->visible && element->enabled && element->rect.contains(position)) { GuiElement* scrolled = element->executeScrollOnElement(position, value); - if (scrolled) - return scrolled; - if (element->onMouseWheelScroll(position, value)) - return element; + if (scrolled) return scrolled; + if (element->onMouseWheelScroll(position, value)) return element; } } + return nullptr; } void GuiContainer::updateLayout(const sp::Rect& rect) { this->rect = rect; + if (layout_manager || !children.empty()) { - if (!layout_manager) - layout_manager = std::make_unique(); + if (!layout_manager) layout_manager = std::make_unique(); glm::vec2 padding_size(layout.padding.left + layout.padding.right, layout.padding.top + layout.padding.bottom); layout_manager->updateLoop(*this, sp::Rect(rect.position + glm::vec2{layout.padding.left, layout.padding.top}, rect.size - padding_size)); @@ -111,7 +108,8 @@ void GuiContainer::updateLayout(const sp::Rect& rect) { glm::vec2 content_size_min(std::numeric_limits::max(), std::numeric_limits::max()); glm::vec2 content_size_max(std::numeric_limits::min(), std::numeric_limits::min()); - for(auto w : children) + + for (auto w : children) { if (w && w->isVisible()) { @@ -123,6 +121,7 @@ void GuiContainer::updateLayout(const sp::Rect& rect) content_size_max.y = std::max(content_size_max.y, p1.y + w->layout.margin.bottom); } } + if (content_size_max.x != std::numeric_limits::min()) { this->rect.size = (content_size_max - content_size_min) + padding_size; @@ -132,6 +131,36 @@ void GuiContainer::updateLayout(const sp::Rect& rect) } } +void GuiContainer::clearElementOwner(GuiElement* element) +{ + element->owner = nullptr; +} + +void GuiContainer::setElementHover(GuiElement* element, bool has_hover) +{ + element->hover = has_hover; +} + +void GuiContainer::setElementFocus(GuiElement* element, bool has_focus) +{ + element->focus = has_focus; +} + +void GuiContainer::callDrawElements(GuiContainer* container, glm::vec2 mouse_pos, sp::Rect rect, sp::RenderTarget& render_target) +{ + container->drawElements(mouse_pos, rect, render_target); +} + +GuiElement* GuiContainer::callGetClickElement(GuiContainer* container, sp::io::Pointer::Button button, glm::vec2 pos, sp::io::Pointer::ID id) +{ + return container->getClickElement(button, pos, id); +} + +GuiElement* GuiContainer::callExecuteScrollOnElement(GuiContainer* container, glm::vec2 pos, float value) +{ + return container->executeScrollOnElement(pos, value); +} + void GuiContainer::setAttribute(const string& key, const string& value) { if (key == "size") @@ -187,9 +216,7 @@ void GuiContainer::setAttribute(const string& key, const string& value) { auto values = value.split(",", 3); if (values.size() == 1) - { - layout.padding.top = layout.padding.bottom = layout.padding.left = layout.padding.right = values[0].strip().toFloat(); - } + layout.padding.top = layout.padding.bottom = layout.padding.left = layout.padding.right = values[0].strip().toFloat(); else if (values.size() == 2) { layout.padding.left = layout.padding.right = values[0].strip().toFloat(); @@ -232,17 +259,14 @@ void GuiContainer::setAttribute(const string& key, const string& value) else if (key == "layout") { GuiLayoutClassRegistry* reg; - for(reg = GuiLayoutClassRegistry::first; reg != nullptr; reg = reg->next) - { - if (value == reg->name) - break; - } + + for (reg = GuiLayoutClassRegistry::first; reg != nullptr; reg = reg->next) + if (value == reg->name) break; + if (reg) - { layout_manager = reg->creation_function(); - }else{ + else LOG(Error, "Failed to find layout type:", value); - } } else if (key == "stretch") { @@ -250,6 +274,7 @@ void GuiContainer::setAttribute(const string& key, const string& value) layout.fill_height = layout.fill_width = layout.lock_aspect_ratio = true; else layout.fill_height = layout.fill_width = value.toBool(); + layout.match_content_size = false; } else if (key == "fill_height") @@ -263,7 +288,5 @@ void GuiContainer::setAttribute(const string& key, const string& value) layout.match_content_size = false; } else - { LOG(Warning, "Tried to set unknown widget attribute:", key, "to", value); - } } diff --git a/src/gui/gui2_container.h b/src/gui/gui2_container.h index 547aabb80c..a13799df06 100644 --- a/src/gui/gui2_container.h +++ b/src/gui/gui2_container.h @@ -1,5 +1,4 @@ -#ifndef GUI2_CONTAINER_H -#define GUI2_CONTAINER_H +#pragma once #include #include @@ -17,25 +16,26 @@ namespace sp { class GuiElement; class GuiLayout; class GuiTheme; + class GuiContainer : sp::NonCopyable { public: -public: + // Nested type to capture layout attributes class LayoutInfo { public: class Sides { public: - float left = 0; - float right = 0; - float top = 0; - float bottom = 0; + float left = 0.0f; + float right = 0.0f; + float top = 0.0f; + float bottom = 0.0f; }; - glm::vec2 position{0, 0}; + glm::vec2 position{0.0f, 0.0f}; sp::Alignment alignment = sp::Alignment::TopLeft; - glm::vec2 size{1, 1}; + glm::vec2 size{1.0f, 1.0f}; glm::ivec2 span{1, 1}; Sides margin; Sides padding; @@ -45,30 +45,39 @@ class GuiContainer : sp::NonCopyable bool match_content_size = true; }; - LayoutInfo layout; - std::list children; -protected: - GuiTheme* theme; -public: GuiContainer() = default; virtual ~GuiContainer(); + // Public data + LayoutInfo layout; + std::list children; + + // Public interfaces template void setLayout() { layout_manager = std::make_unique(); } - void updateLayout(const sp::Rect& rect); + virtual void updateLayout(const sp::Rect& rect); + virtual void setAttribute(const string& key, const string& value); const sp::Rect& getRect() const { return rect; } - virtual void setAttribute(const string& key, const string& value); protected: + GuiTheme* theme; + + // Protected data + sp::Rect rect{0,0,0,0}; + std::unique_ptr layout_manager = nullptr; + + // Protected interfaces virtual void drawElements(glm::vec2 mouse_position, sp::Rect parent_rect, sp::RenderTarget& window); virtual void drawDebugElements(sp::Rect parent_rect, sp::RenderTarget& window); - GuiElement* getClickElement(sp::io::Pointer::Button button, glm::vec2 position, sp::io::Pointer::ID id); - GuiElement* executeScrollOnElement(glm::vec2 position, float value); + virtual GuiElement* getClickElement(sp::io::Pointer::Button button, glm::vec2 position, sp::io::Pointer::ID id); + virtual GuiElement* executeScrollOnElement(glm::vec2 position, float value); - friend class GuiElement; + // Static helpers for subclass access to protected members. + static void clearElementOwner(GuiElement* element); + static void setElementHover(GuiElement* element, bool has_hover); + static void setElementFocus(GuiElement* element, bool has_focus); + static void callDrawElements(GuiContainer* container, glm::vec2 mouse_pos, sp::Rect rect, sp::RenderTarget& render_target); + static GuiElement* callGetClickElement(GuiContainer* container, sp::io::Pointer::Button button, glm::vec2 pos, sp::io::Pointer::ID id); + static GuiElement* callExecuteScrollOnElement(GuiContainer* container, glm::vec2 pos, float value); - sp::Rect rect{0,0,0,0}; -private: - std::unique_ptr layout_manager = nullptr; + friend class GuiElement; }; - -#endif//GUI2_CONTAINER_H diff --git a/src/gui/gui2_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; +}; diff --git a/src/gui/gui2_scrollcontainer.cpp b/src/gui/gui2_scrollcontainer.cpp new file mode 100644 index 0000000000..807ef1b595 --- /dev/null +++ b/src/gui/gui2_scrollcontainer.cpp @@ -0,0 +1,392 @@ +#include "gui2_scrollcontainer.h" +#include "gui2_scrollbar.h" +#include "gui2_canvas.h" +#include "gui/layout/layout.h" + + +GuiScrollContainer::GuiScrollContainer(GuiContainer* owner, const string& id, ScrollMode mode) +: GuiElement(owner, id), mode(mode) +{ + // Don't lock content size to element. + // We need to manipulate content size when toggling scrollbar visibility. + layout.match_content_size = false; + + // Define the scrollbar and hide it. + scrollbar_v = new GuiScrollbar(this, id + "_SCROLLBAR_V", 0, 100, 0, + [this](int value) + { + scroll_offset = static_cast(value); + } + ); + scrollbar_v->setClickChange(50); + scrollbar_v + ->setPosition(0.0f, 0.0f, sp::Alignment::TopRight) + ->setSize(scrollbar_width, GuiSizeMax) + ->hide(); +} + +GuiScrollContainer* GuiScrollContainer::setMode(ScrollMode new_mode) +{ + mode = new_mode; + return this; +} + +GuiScrollContainer* GuiScrollContainer::setScrollbarWidth(float width) +{ + scrollbar_width = width; + return this; +} + +void GuiScrollContainer::scrollToFraction(float fraction) +{ + const float max_scroll = std::max(0.0f, content_height - visible_height); + scroll_offset = std::clamp(fraction * max_scroll, 0.0f, max_scroll); + scrollbar_v->setValue(static_cast(scroll_offset)); +} + +void GuiScrollContainer::scrollToOffset(float pixel_offset) +{ + const float max_scroll = std::max(0.0f, content_height - visible_height); + scroll_offset = std::clamp(pixel_offset, 0.0f, max_scroll); + scrollbar_v->setValue(static_cast(scroll_offset)); +} + +void GuiScrollContainer::updateLayout(const sp::Rect& rect) +{ + this->rect = rect; + visible_height = rect.size.y - layout.padding.top - layout.padding.bottom; + + // Show the scrollbar only if we're clipping anything. + bool has_overflow = (mode != ScrollMode::None) && (content_height > visible_height + 0.5f); + scrollbar_v->setVisible(has_overflow); + + // Don't factor scrollbar width if it isn't visible. + const float sb_width = scrollbar_v->isVisible() ? scrollbar_width : 0.0f; + + // Manually factor padding into content layout around the scrollbar. + glm::vec2 padding_offset{ + layout.padding.left, + layout.padding.top + }; + + glm::vec2 padding_size{ + layout.padding.left + layout.padding.right, + layout.padding.top + layout.padding.bottom + }; + + sp::Rect content_layout_rect{ + rect.position + padding_offset, + rect.size - padding_size - glm::vec2{sb_width, 0.0f} + }; + + if (!layout_manager) layout_manager = std::make_unique(); + + // Temporarily hide the scrollbar so the layout manager ignores it for + // sizing, then restore it if enabled. + scrollbar_v->setVisible(false); + + layout_manager->updateLoop(*this, content_layout_rect); + + scrollbar_v->setVisible(has_overflow); + + // Override the scrollbar rect. + scrollbar_v->updateLayout({ + {rect.position.x + rect.size.x - scrollbar_width, rect.position.y}, + {scrollbar_width, rect.size.y} + }); + + // Compute content_height from non-scrollbar visible children. + float max_bottom = 0.0f; + for (GuiElement* child : children) + { + if (child == scrollbar_v) continue; + if (!child->isVisible()) continue; + + const float bottom = child->getRect().position.y + child->getRect().size.y + child->layout.margin.bottom - rect.position.y; + if (bottom > max_bottom) max_bottom = bottom; + } + content_height = max_bottom + layout.padding.bottom; + + // Clamp scroll offset. + scroll_offset = std::clamp(scroll_offset, 0.0f, std::max(0.0f, content_height - visible_height)); + + // Sync scrollbar properties to new layout. + scrollbar_v->setRange(0, static_cast(content_height)); + scrollbar_v->setValueSize(static_cast(visible_height)); + scrollbar_v->setValue(static_cast(scroll_offset)); +} + +void GuiScrollContainer::drawElements(glm::vec2 mouse_position, sp::Rect /* parent_rect */, sp::RenderTarget& renderer) +{ + sp::Rect content_rect = getContentRect(); + + // Capture clipping and scroll translation. + renderer.pushScissorRect(content_rect); + renderer.pushTranslation({0.0f, -scroll_offset}); + + // Track mouse position on element relative to the vertical scroll offset. + glm::vec2 layout_mouse = mouse_position + glm::vec2{0.0f, scroll_offset}; + + // Pass the relative mouse position through to each child element. + for (auto it = children.begin(); it != children.end(); ) + { + GuiElement* element = *it; + + if (element == scrollbar_v) + { + ++it; + continue; + } + + if (element->isDestroyed()) + { + GuiCanvas* canvas = dynamic_cast(element->getTopLevelContainer()); + if (canvas) canvas->unfocusElementTree(element); + + it = children.erase(it); + clearElementOwner(element); + delete element; + + continue; + } + + setElementHover(element, element->getRect().contains(layout_mouse)); + + if (element->isVisible()) + { + element->onDraw(renderer); + callDrawElements(element, layout_mouse, element->getRect(), renderer); + } + + ++it; + } + + // Apply scroll translation and clipping. Order matters here. + renderer.popTranslation(); + renderer.popScissorRect(); + + // Draw the scrollbar if intended to be visible. Never clip nor scroll the + // scrollbar itself. + scrollbar_v->setVisible(scrollbar_v->isVisible() && mode != ScrollMode::None); + if (scrollbar_v->isVisible()) + { + setElementHover(scrollbar_v, scrollbar_v->getRect().contains(mouse_position)); + scrollbar_v->onDraw(renderer); + callDrawElements(scrollbar_v, mouse_position, scrollbar_v->getRect(), renderer); + } +} + +GuiElement* GuiScrollContainer::getClickElement(sp::io::Pointer::Button button, glm::vec2 position, sp::io::Pointer::ID id) +{ + // Pass the click to the scrollbar first, and don't translate its position. + if (scrollbar_v->isVisible() + && scrollbar_v->isEnabled() + && scrollbar_v->getRect().contains(position) + ) + { + GuiElement* clicked = callGetClickElement(scrollbar_v, button, position, id); + if (clicked) return clicked; + if (scrollbar_v->onMouseDown(button, position, id)) return scrollbar_v; + } + + // Don't pass clicks to elements outside of the content rect. + if (!getContentRect().contains(position)) return nullptr; + + // Pass the click to each nested child, which should take priority if it can + // use it. + glm::vec2 layout_pos = position + glm::vec2{0.0f, scroll_offset}; + + for (auto it = children.rbegin(); it != children.rend(); ++it) + { + GuiElement* element = *it; + + // We already handled the scrollbar. + if (element == scrollbar_v) continue; + // We don't care about buttons that aren't visible or enabled. + if (!element->isVisible() || !element->isEnabled()) continue; + + // Figure out if we can click the element. If so, capture the scroll + // offset to pass to drag events, focus it, and click it. + GuiElement* clicked = callGetClickElement(element, button, layout_pos, id); + if (clicked) + { + switchFocusTo(clicked); + pressed_element = clicked; + pressed_scroll = scroll_offset; + return this; + } + + // The click didn't fire, but we still recurse into children regardless. + // This helps find children or child-like elements (like GuiSelector + // popups) that can exist outside of their parent's rect. + if (element->getRect().contains(layout_pos) && element->onMouseDown(button, layout_pos, id)) + { + switchFocusTo(element); + pressed_element = element; + pressed_scroll = scroll_offset; + return this; + } + } + + // Otherwise, do nothing. + return nullptr; +} + +void GuiScrollContainer::switchFocusTo(GuiElement* new_element) +{ + // Apply focus change, if any. + if (focused_element == new_element) return; + + if (focused_element) + { + setElementFocus(focused_element, false); + focused_element->onFocusLost(); + } + + focused_element = new_element; + + // If this scroll container already has canvas focus, forward focus gained + // to the new child now (GuiCanvas won't call our onFocusGained again). + // If this scroll container is not yet focused, canvas will call our + // onFocusGained after getClickElement returns, which will forward it. + if (focus) + { + setElementFocus(focused_element, true); + focused_element->onFocusGained(); + } +} + +void GuiScrollContainer::onFocusGained() +{ + if (focused_element) + { + setElementFocus(focused_element, true); + focused_element->onFocusGained(); + } +} + +void GuiScrollContainer::onFocusLost() +{ + if (focused_element) + { + setElementFocus(focused_element, false); + focused_element->onFocusLost(); + focused_element = nullptr; + } +} + +void GuiScrollContainer::onTextInput(const string& text) +{ + if (focused_element) focused_element->onTextInput(text); +} + +void GuiScrollContainer::onTextInput(sp::TextInputEvent e) +{ + if (focused_element) focused_element->onTextInput(e); +} + +bool GuiScrollContainer::onMouseDown(sp::io::Pointer::Button button, glm::vec2 position, sp::io::Pointer::ID id) +{ + if (pressed_element) + { + pressed_element->onMouseDown(button, position + glm::vec2{0.0f, pressed_scroll}, id); + pressed_element = nullptr; + return true; + } + + return false; +} + +void GuiScrollContainer::onMouseDrag(glm::vec2 position, sp::io::Pointer::ID id) +{ + if (pressed_element) pressed_element->onMouseDrag(position + glm::vec2{0.0f, pressed_scroll}, id); +} + +void GuiScrollContainer::onMouseUp(glm::vec2 position, sp::io::Pointer::ID id) +{ + if (pressed_element) + { + pressed_element->onMouseUp(position + glm::vec2{0.0f, pressed_scroll}, id); + pressed_element = nullptr; + } +} + +GuiElement* GuiScrollContainer::executeScrollOnElement(glm::vec2 position, float value) +{ + // Pass the scroll to the scrollbar first, and don't translate its position. + if (scrollbar_v->isVisible() + && scrollbar_v->isEnabled() + && scrollbar_v->getRect().contains(position)) + { + GuiElement* scrolled = callExecuteScrollOnElement(scrollbar_v, position, value); + if (scrolled) return scrolled; + // Handle mousewheel scroll, if any. + if (scrollbar_v->onMouseWheelScroll(position, value)) return scrollbar_v; + } + + // Return nothing if the scroll isn't within the container. + if (!getContentRect().contains(position)) return nullptr; + + // Execute the scroll on each nested child. If a child can use the mousewheel + // scroll event, give it to them. + glm::vec2 layout_pos = position + glm::vec2{0.0f, scroll_offset}; + + for (auto it = children.rbegin(); it != children.rend(); ++it) + { + GuiElement* element = *it; + if (element == scrollbar_v) continue; + + if (element + && element->isVisible() + && element->isEnabled() + && element->getRect().contains(layout_pos) + ) + { + GuiElement* scrolled = callExecuteScrollOnElement(element, layout_pos, value); + if (scrolled) return scrolled; + if (element->onMouseWheelScroll(layout_pos, value)) return element; + } + } + + // No child used the mousewheel scroll event, so use it to scroll the + // container. + if (onMouseWheelScroll(position, value)) return this; + + // Otherwise, nothing happens. + return nullptr; +} + +bool GuiScrollContainer::onMouseWheelScroll(glm::vec2 /* position */, float value) +{ + // Don't scroll if used only to clip. + if (mode == ScrollMode::None) return false; + + // Scroll by a default interval of 50, or by the container height if set to + // paged mode. + const float step = (mode == ScrollMode::Page) ? visible_height : 50.0f; + const float max_scroll = std::max(0.0f, content_height - visible_height); + scroll_offset = std::clamp(scroll_offset - value * step, 0.0f, max_scroll); + + // Update the scrollbar. + scrollbar_v->setValue(static_cast(scroll_offset)); + + return true; +} + +sp::Rect GuiScrollContainer::getContentRect() const +{ + // Return the rect, inset by padding and minus room for the scrollbar if it's visible. + return sp::Rect{ + rect.position + glm::vec2{layout.padding.left, layout.padding.top}, + { + rect.size.x - layout.padding.left - layout.padding.right - getEffectiveScrollbarWidth(), + rect.size.y - layout.padding.top - layout.padding.bottom + } + }; +} + +float GuiScrollContainer::getEffectiveScrollbarWidth() const +{ + // Save room for the scrollbar only if it's visible. + return scrollbar_v->isVisible() ? scrollbar_width : 0.0f; +} diff --git a/src/gui/gui2_scrollcontainer.h b/src/gui/gui2_scrollcontainer.h new file mode 100644 index 0000000000..73374cbe31 --- /dev/null +++ b/src/gui/gui2_scrollcontainer.h @@ -0,0 +1,95 @@ +#pragma once + +#include "gui2_element.h" + +class GuiScrollbar; + +// GuiContainer-like GuiElement with support for clipping or scrolling arbitrary +// child elements that overflow its bounds. +class GuiScrollContainer : public GuiElement +{ +public: + // Define modes to indicate whether this element scrolls, and if so, how. + enum class ScrollMode { + None, // Cut overflow off at element borders; no scrolling + Scroll, // Scroll by fixed increments, regardless of contents or element size + Page // Scroll by increments equal to the element size + }; + + GuiScrollContainer(GuiContainer* owner, const string& id, ScrollMode mode = ScrollMode::Scroll); + + // TODO: Right now this clips both horizontally and vertically, but supports + // only vertical scrolling/paging. + + // Set scrolling mode. All modes clip at the element boundaries. + GuiScrollContainer* setMode(ScrollMode mode); + // Set width of scrollbar if visible. + GuiScrollContainer* setScrollbarWidth(float width); + // Scroll element to this fraction of the total scrollbar limit. + // Value passed here represents where the top of the scrollbar pill goes + // on the scrollbar. + void scrollToFraction(float fraction); + // Scroll element to this pixel offset from the top (clamped to valid range). + void scrollToOffset(float pixel_offset); + + // Override layout updates to update child elements and juggle scrollbar + // visibility. + virtual void updateLayout(const sp::Rect& rect) override; + // Handle mousewheel scroll, with behavior depending on the ScrollMode. + virtual bool onMouseWheelScroll(glm::vec2 position, float value) override; + // Pass mouse down to child elements, but only if they're visible. + virtual bool onMouseDown(sp::io::Pointer::Button button, glm::vec2 position, sp::io::Pointer::ID id) override; + // Pass mouse drag to child elements. This relies on + virtual void onMouseDrag(glm::vec2 position, sp::io::Pointer::ID id) override; + // Pass mouse up to child elements. + virtual void onMouseUp(glm::vec2 position, sp::io::Pointer::ID id) override; + // Pass focus to child elements. + virtual void onFocusGained() override; + // Pass focus loss to child elements. + virtual void onFocusLost() override; + // Pass text input events to child elements. + virtual void onTextInput(const string& text) override; + // Pass text input events to child elements. + virtual void onTextInput(sp::TextInputEvent e) override; + +protected: + // Draw elements if they're in view. Translate mouse positions by the scroll + // amount. + virtual void drawElements(glm::vec2 mouse_position, sp::Rect parent_rect, sp::RenderTarget& renderer) override; + // Find the clicked element, checking children of this container if they're + // visible. + virtual GuiElement* getClickElement(sp::io::Pointer::Button button, glm::vec2 position, sp::io::Pointer::ID id) override; + // Scroll the element's children. Pass any mousewheel events to children + // first if they can use it. + virtual GuiElement* executeScrollOnElement(glm::vec2 position, float value) override; + +private: + // Define whether this element scrolls, paginates, or only clips content. + ScrollMode mode; + // Defines the scrollbar's width, in virtual pixels. + float scrollbar_width = 30.0f; + // Scrollbar element, visible only if there's overflow. + GuiScrollbar* scrollbar_v; + + // Defines the scroll offset in virtual pixels, with 0 as the top. + float scroll_offset = 0.0f; + // Defines the total height of content, in virtual pixels. + float content_height = 0.0f; + // Defines the visible height of the element, in virtual pixels. + float visible_height = 0.0f; + + // Defines the element that has focus within this element's subtree. + GuiElement* focused_element = nullptr; + // Defines the element being clicked/tapped within this element's subtree. + GuiElement* pressed_element = nullptr; + // Defines the scroll position of the pressed element. + float pressed_scroll = 0.0f; + + // Returns a rect for the area where content is visible. + sp::Rect getContentRect() const; + // Returns the effective scrollbar width, factoring in whether it appears + // at all. + float getEffectiveScrollbarWidth() const; + // Passes focus to another element. + void switchFocusTo(GuiElement* new_element); +}; diff --git a/src/gui/gui2_scrolltext.cpp b/src/gui/gui2_scrolltext.cpp index 4fc5ac2503..e10fa78bfe 100644 --- a/src/gui/gui2_scrolltext.cpp +++ b/src/gui/gui2_scrolltext.cpp @@ -1,148 +1,30 @@ #include "gui2_scrolltext.h" #include "theme.h" -#include "gui2_scrollbar.h" - GuiScrollText::GuiScrollText(GuiContainer* owner, string id, string text) -: GuiElement(owner, id), text(text) +: GuiScrollContainer(owner, id), text(text) { - text_theme = theme->getStyle("textbox.front"); - 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) -{ - const auto& text_style = text_theme->get(getState()); - 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, text_style.color, 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) -{ - const auto& text_style = text_theme->get(getState()); - auto main_color = text_style.color; - 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 5c75387913..eecae0d269 100644 --- a/src/gui/gui2_scrolltext.h +++ b/src/gui/gui2_scrolltext.h @@ -1,44 +1,48 @@ -#ifndef GUI_SCROLLTEXT_H -#define GUI_SCROLLTEXT_H +#pragma once -#include "gui2_element.h" +#include "gui2_scrollcontainer.h" +#include "gui2_multilinetext.h" -class GuiScrollbar; -class GuiThemeStyle; - -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; - const GuiThemeStyle* text_theme; + // 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; }; - -#endif//GUI_SCROLLTEXT_H 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/screenComponents/shipsLogControl.cpp b/src/screenComponents/shipsLogControl.cpp index f808a76664..cb73edf989 100644 --- a/src/screenComponents/shipsLogControl.cpp +++ b/src/screenComponents/shipsLogControl.cpp @@ -9,15 +9,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) @@ -25,50 +30,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, theme->getStyle("panel")->get(getState()).texture); 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); } @@ -77,10 +105,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 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);