From 1ec7598ac085e8533db47ecf88e54074c4fafd69 Mon Sep 17 00:00:00 2001 From: Auri Date: Fri, 20 Aug 2021 16:19:10 -0700 Subject: [PATCH] Expression Parsing --- src/CMakeLists.txt | 2 +- src/client/gui/BoxElement.cpp | 8 +- src/client/gui/BoxElement.h | 7 + src/client/gui/Element.cpp | 55 ++++--- src/client/gui/Element.h | 52 +++++- src/client/gui/Expression.cpp | 175 ++++++++++++++++++++ src/client/gui/Expression.h | 57 +++++++ src/client/gui/Root.cpp | 24 ++- src/client/gui/Root.h | 4 + src/client/gui/Style.h | 38 ++++- src/client/gui/TextElement.cpp | 246 +++++++++++++++++++++++++++++ src/client/gui/TextElement.h | 35 ++++ src/client/scene/MainMenuScene.cpp | 170 +++++++------------- src/client/scene/MainMenuScene.h | 24 +-- subgames/zeus/menu/script/init.lua | 106 +++++++------ 15 files changed, 769 insertions(+), 234 deletions(-) create mode 100644 src/client/gui/Expression.cpp create mode 100644 src/client/gui/Expression.h create mode 100644 src/client/gui/TextElement.cpp create mode 100644 src/client/gui/TextElement.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7e5103cb..b2480480 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -331,6 +331,6 @@ add_library(Zepha_Core client/gui/compound/GuiCellGraph.cpp client/gui/compound/GuiCellGraph.h client/gui/basic/GuiCells.cpp - client/gui/basic/GuiCells.h client/gui/Gui.h client/gui/Root.cpp client/gui/Root.h client/gui/BoxElement.cpp client/gui/BoxElement.h client/gui/Gui.cpp client/gui/Style.h) + client/gui/basic/GuiCells.h client/gui/Gui.h client/gui/Root.cpp client/gui/Root.h client/gui/BoxElement.cpp client/gui/BoxElement.h client/gui/Gui.cpp client/gui/Style.h client/gui/TextElement.cpp client/gui/TextElement.h client/gui/Expression.cpp client/gui/Expression.h) target_include_directories(Zepha_Core PUBLIC .) \ No newline at end of file diff --git a/src/client/gui/BoxElement.cpp b/src/client/gui/BoxElement.cpp index 547e894f..d6885024 100644 --- a/src/client/gui/BoxElement.cpp +++ b/src/client/gui/BoxElement.cpp @@ -2,7 +2,6 @@ #include "client/gui/Root.h" #include "client/graph/Model.h" -#include "game/atlas/asset/AtlasRef.h" #include "client/graph/mesh/EntityMesh.h" void Gui::BoxElement::updateElement() { @@ -20,7 +19,6 @@ void Gui::BoxElement::updateElement() { } curBg = rawBg; - if (isDirty) { const let bgColor = getStyle(bgRule); const string bgImage = getStyle(bgRule, ""); @@ -50,8 +48,10 @@ void Gui::BoxElement::updateElement() { entity.setModel(model); } - entity.setScale(vec3(getComputedSize() * static_cast(PX_SCALE), 0)); - entity.setPos(vec3(getComputedScreenPos() * static_cast(PX_SCALE), 0)); + let margin = getStyle(StyleRule::MARGIN, {}); + + entity.setScale(vec3(getComputedSize(), 0)); + entity.setPos(vec3(getComputedScreenPos() + ivec2 { margin.x, margin.y }, 0)); Element::updateElement(); } \ No newline at end of file diff --git a/src/client/gui/BoxElement.h b/src/client/gui/BoxElement.h index 7c974bca..8ef362d8 100644 --- a/src/client/gui/BoxElement.h +++ b/src/client/gui/BoxElement.h @@ -1,8 +1,15 @@ +#pragma once + #include "Element.h" #include "game/atlas/asset/AtlasRef.h" namespace Gui { + + /** + * A simple box element that may have background and/or children. + */ + class BoxElement: public Element { public: using Element::Element; diff --git a/src/client/gui/Element.cpp b/src/client/gui/Element.cpp index 5437b634..57140305 100644 --- a/src/client/gui/Element.cpp +++ b/src/client/gui/Element.cpp @@ -1,6 +1,7 @@ #include "Element.h" #include "util/Util.h" +#include "client/gui/Root.h" #include "client/graph/Renderer.h" Gui::Element::~Element() { @@ -17,51 +18,55 @@ void Gui::Element::setStyle(StyleRule style, const std::any& value) { } ivec2 Gui::Element::getComputedSize() { - return { - getStyle(StyleRule::WIDTH, std::max(layoutSize.x, 0)), - getStyle(StyleRule::HEIGHT, std::max(layoutSize.y, 0)) - }; + let size = getStyle(StyleRule::SIZE, glm::max(layoutSize, 0)); + return size; +} + +ivec2 Gui::Element::getComputedOuterSize() { + let size = getComputedSize(); + let margin = getStyle(StyleRule::MARGIN, {}); + return ivec2 { size.x + margin.x + margin.z, size.y + margin.y + margin.w }; } ivec2 Gui::Element::getComputedContentSize() { let size = getComputedSize(); - let padding = getStyle(StyleRule::PADDING, {}); - return glm::max(ivec2 { size.x - padding.x - padding.z, size.y - padding.y - padding.w }, ivec2 {}); + let padding = getStyle(StyleRule::PADDING, {}); + return glm::max(ivec2 { size.x - padding.x - padding.z, size.y - padding.y - padding.w }, 0); } ivec2 Gui::Element::getExplicitSize() { - return { - getStyle(StyleRule::WIDTH, -1), - getStyle(StyleRule::HEIGHT, -1) - }; + return getStyle(StyleRule::SIZE, ivec2(-1)); } ivec2 Gui::Element::getComputedPos() { - return { - getStyle(StyleRule::LEFT, layoutPosition.x), - getStyle(StyleRule::TOP, layoutPosition.y) - }; + return getStyle(StyleRule::POS, layoutPosition); } ivec2 Gui::Element::getComputedScreenPos() { return getComputedPos() + parentOffset; } -bool Gui::Element::handleMouseHover(ivec2 mousePos) { +bool Gui::Element::handleMouseHover(ivec2 mousePos, bool& pointer) { bool childIntersects = false; for (let& child : children) - if (child->handleMouseHover(mousePos)) childIntersects = true; + if (child->handleMouseHover(mousePos, pointer)) + childIntersects = true; if (childIntersects) { - hovered = false; + if (hovered) { + hovered = false; + updateElement(); + } return true; } - ivec2 size = getComputedSize() * static_cast(PX_SCALE); - ivec2 pos = getComputedScreenPos() * static_cast(PX_SCALE); + ivec2 size = getComputedSize(); + ivec2 pos = getComputedScreenPos(); bool intersects = mousePos.x >= pos.x && mousePos.x <= pos.x + size.x && mousePos.y >= pos.y && mousePos.y <= pos.y + size.y; + if (intersects) pointer = getStyle(StyleRule::CURSOR, "") == "pointer"; + if (hovered != intersects) { hovered = intersects; updateElement(); @@ -116,8 +121,8 @@ void Gui::Element::layoutChildren() { * The element gap across the primary axis. */ - const i32 gap = getStyle(StyleRule::GAP, ivec2(0))[primary]; - const ivec4& padding = getStyle(StyleRule::PADDING, ivec4 {}); + const i32 gap = getStyle(StyleRule::GAP, ivec2(0))[primary]; + const ivec4& padding = getStyle(StyleRule::PADDING, ivec4 {}); /* * Calculates the explicit spaced used up by children across the primary axis, @@ -133,6 +138,8 @@ void Gui::Element::layoutChildren() { let childExplicitSize = child->getExplicitSize(); if (childExplicitSize[primary] != -1) explicitSize += childExplicitSize[primary]; else implicitCount++; + let childMargin = child->getStyle(StyleRule::MARGIN, {}); + explicitSize += childMargin[primary] + childMargin[primary + 2]; } /** @@ -143,7 +150,7 @@ void Gui::Element::layoutChildren() { if (align[primary] == 1) offset[primary] += selfSize[primary] - explicitSize - (gap * (children.size() - 1)); else if (align[primary] == 0) offset[primary] += selfSize[primary] / 2 - explicitSize / 2 - (gap * (children.size() - 1)) / 2; - + /** * The amount of size each implicitly sized element should occupy. */ @@ -159,6 +166,7 @@ void Gui::Element::layoutChildren() { for (const let& child : children) { let childExplicitSize = child->getExplicitSize(); + let childMargin = child->getStyle(StyleRule::MARGIN, {}); child->layoutSize[primary] = (childExplicitSize[primary] == -1 && align[primary] == 2) ? implicitElemSize : 0; @@ -174,7 +182,8 @@ void Gui::Element::layoutChildren() { child->layoutPosition[primary] = offset[primary]; offset[primary] += ((childExplicitSize[primary] == -1 && align[primary] == 2) - ? implicitElemSize : childExplicitSize[primary]) + gap; + ? implicitElemSize : childExplicitSize[primary]) + + gap + childMargin[primary] + childMargin[primary + 2]; child->parentOffset = selfOffset; diff --git a/src/client/gui/Element.h b/src/client/gui/Element.h index 7eda0968..621ac7a4 100644 --- a/src/client/gui/Element.h +++ b/src/client/gui/Element.h @@ -1,6 +1,5 @@ #pragma once -#include #include #include "client/gui/Gui.h" @@ -15,6 +14,11 @@ class Renderer; namespace Gui { class Root; + /** + * Base class for all Gui Elements. + * Represents an element within a Gui Root, which may be drawn to the screen. + */ + class Element { friend class BoxElement; @@ -28,15 +32,20 @@ namespace Gui { Element(Root& root, vec& stylesheets): root(root), stylesheets(stylesheets) {} ~Element(); - + + /** Sets the element's props to the struct specified. */ virtual void setProps(const Props& props); - + + /** Sets a style rule on the element. */ virtual void setStyle(StyleRule style, const std::any& value); + /** Recalculates the element based on its props. Call when props or stylesheets change. */ virtual void updateElement(); + /** Draws the element to the screen. */ virtual void draw(Renderer& renderer); + /** Creates and prepends an element to this element. */ template, bool> = true> sptr prepend(const Props& props = {}) { const let elem = make_shared(root, stylesheets); @@ -45,6 +54,7 @@ namespace Gui { return elem; }; + /** Prepends an existing element to this element. */ sptr prepend(sptr elem) { children.push_front(elem); elem->parent = this; @@ -52,6 +62,7 @@ namespace Gui { return elem; } + /** Creates and appends an element to this element. */ template, bool> = true> sptr append(const Props& props = {}) { const let elem = make_shared(root, stylesheets); @@ -60,6 +71,7 @@ namespace Gui { return elem; }; + /** Appends an existing element to this element. */ sptr append(sptr elem) { children.push_back(elem); elem->parent = this; @@ -67,12 +79,25 @@ namespace Gui { return elem; } + /** Returns the element's computed size. */ virtual ivec2 getComputedSize(); + + /** Returns the element's computed size + margins. */ + virtual ivec2 getComputedOuterSize(); + + /** Returns the element's computed content size, which is its size - padding. */ virtual ivec2 getComputedContentSize(); + + /** Returns the element's explicit size. Unspecified dimensions are -1. */ virtual ivec2 getExplicitSize(); + + /** Returns the element's computed position relative to its parent. */ virtual ivec2 getComputedPos(); + + /** Returns the element's computed position relative to the screen. */ virtual ivec2 getComputedScreenPos(); + /** Gets a style value from the element's styles or the root's stylesheets. */ const optional getStyle(StyleRule rule) const { const optional opt = props.styles.get(rule); if (opt) return *opt; @@ -86,6 +111,7 @@ namespace Gui { return std::nullopt; } + /** Gets a style value from the element's styles or the root's stylesheets. */ template const optional getStyle(StyleRule rule) const { const optional opt = props.styles.get(rule); @@ -100,6 +126,7 @@ namespace Gui { return std::nullopt; } + /** Gets a style value from the element's styles or the root's stylesheets. */ template const V getStyle(StyleRule rule, V def) const { const optional opt = getStyle(rule); @@ -107,7 +134,18 @@ namespace Gui { return def; } - bool handleMouseHover(ivec2 mousePos); + /** + * Called by the root when the mouse position changes. + * Returns a boolean if the element or its children are hovered. + */ + + bool handleMouseHover(ivec2 mousePos, bool& pointer); + + /** + * Called by the root when the mouse clicks. + * Triggers a click interaction on the hovered element. + */ + bool handleMouseClick(u32 button, bool down); protected: @@ -121,10 +159,16 @@ namespace Gui { bool hovered = false; + /** The screen offset of the parent. */ ivec2 parentOffset {}; + + /** The element's implicit size, as defined by the parent layout. */ ivec2 layoutSize { -1, -1 }; + + /** The element's implicit position, as defined by the parent layout. */ ivec2 layoutPosition {}; + /** Updates child sizes and offsets based on layout styles. */ virtual void layoutChildren(); }; } \ No newline at end of file diff --git a/src/client/gui/Expression.cpp b/src/client/gui/Expression.cpp new file mode 100644 index 00000000..f038dc33 --- /dev/null +++ b/src/client/gui/Expression.cpp @@ -0,0 +1,175 @@ +#include +#include +#include +#include + +#include "client/gui/Expression.h" + +#include "util/Util.h" +#include "Gui.h" + +Gui::Expression::Expression(const string& exp) { + setExpression(exp); +} + +void Gui::Expression::setExpression(string exp) { + // Avoid reparsing the same expression. + usize newHash = std::hash{}(exp); + if (hash == newHash) return; + hash = newHash; + + // Sanitize expression + exp.erase(std::remove_if(exp.begin(), exp.end(), isspace), exp.end()); + + // Process Infix into Postfix (RPN) + infix = {}; + std::stack operators {}; + + bool nextOperatorIsUnary = true; + + String temp = {}; + + while (exp.size()) { + let& c = exp[0]; + // Number or Unit or Keyword + if ((c >= '0' && c <= '9') || c == '.' || (c >= 97 && c <= 122) || + (nextOperatorIsUnary && (c == '+' || c == '-'))) { + temp.v += c; + nextOperatorIsUnary = false; + } + // Binary Operator + else if (!nextOperatorIsUnary && (c == '+' || c == '-' || c == '*' || c == '/' || c == '^')) { + if (temp.v.size()) { + infix.emplace(temp); + temp = {}; + } + + while (operators.size() && operators.top() != '(' && + ((c != '^' && PRECEDENCE.at(operators.top()) >= PRECEDENCE.at(c)) || + PRECEDENCE.at(operators.top()) > PRECEDENCE.at(c))) { + + infix.emplace(string(1, operators.top())); + operators.pop(); + } + + operators.emplace(c); + nextOperatorIsUnary = true; + } + // Opening Parentheses + else if (c == '(') { + if (temp.v.size()) { + infix.emplace(temp); + temp = {}; + } + + operators.push(c); + nextOperatorIsUnary = true; + } + // Closing Parentheses + else if (c == ')') { + if (!temp.v.size()) throw std::logic_error("Empty or mismatched parentheses."); + infix.emplace(temp); + temp = {}; + + if (!operators.size()) throw std::logic_error("Mismatched parentheses."); + while (operators.top() != '(') { + infix.emplace(string(1, operators.top())); + operators.pop(); + if (!operators.size()) throw std::logic_error("Mismatched parentheses."); + } + if (operators.top() != '(') throw std::logic_error("Mismatched parentheses."); + operators.pop(); + nextOperatorIsUnary = false; + } + + exp.erase(0, 1); + } + + if (temp.v.size()) { + infix.push(temp); + temp = {}; + } + + while (operators.size()) { + if (operators.top() == '(') throw std::logic_error("Mismatched parentheses."); + infix.emplace(string(1, operators.top())); + operators.pop(); + } +} + +f32 Gui::Expression::eval() { + let infix = this->infix; + std::stack eval {}; + + while (infix.size()) { + let& t = infix.front(); + infix.pop(); + + if (!t.isOperator()) { + eval.emplace(t); + } + else { + if (eval.size() < 2) throw std::runtime_error("Eval stack has < 2 items! This is an engine error!"); + + String b = eval.top(); + eval.pop(); + String a = eval.top(); + eval.pop(); + + switch (t.v[0]) { + case '+': + eval.emplace(std::to_string(a.eval() + b.eval())); + break; + case '-': + eval.emplace(std::to_string(a.eval() - b.eval())); + break; + case '*': + eval.emplace(std::to_string(a.eval() * b.eval())); + break; + case '/': + eval.emplace(std::to_string(a.eval() / b.eval())); + break; + case '^': + eval.emplace(std::to_string(pow(a.eval(), b.eval()))); + break; + } + } + } + + if (!eval.size()) throw std::runtime_error("Eval stack is empty! This is an engine error!"); + return eval.top().eval(); +} + +const std::unordered_map Gui::Expression::PRECEDENCE { + { '^', 4 }, + { '*', 3 }, + { '/', 3 }, + { '+', 2 }, + { '-', 2 } +}; + +bool Gui::Expression::String::isOperator() { + return v.size() == 1 && (v[0] == '+' || v[0] == '-' || v[0] == '*' || v[0] == '/' || v[0] == '^'); +} + +f32 Gui::Expression::String::eval() { + usize unitInd = -1; + + f32 value = std::stof(v, &unitInd); + string unit = v.substr(unitInd); + + switch (Util::hash(unit.data())) { + default: + throw std::logic_error("Unknown unit '" + unit + "'."); + + case Util::hash("dp"): + return value * Gui::PX_SCALE; + + case Util::hash("deg"): + return value * M_PI / 180.f; + + case Util::hash(""): + case Util::hash("px"): + return value; + } +} diff --git a/src/client/gui/Expression.h b/src/client/gui/Expression.h new file mode 100644 index 00000000..e38545ce --- /dev/null +++ b/src/client/gui/Expression.h @@ -0,0 +1,57 @@ +#pragma once + +#include +#include + +#include "util/Types.h" + +namespace Gui { + enum class UnitOrOperator: u8 { + DISPLAY_PIXEL, + REAL_PIXEL, + DEGREE, + + ADD = 128, + SUBTRACT, + MULTIPLY, + DIVIDE, + EXPONENT + }; + + class Expression { + struct String { + String() = default; + explicit String(const string& v): v(v) {} + + string v {}; + + bool isOperator(); + + f32 eval(); + }; + + struct Token { + Token() = default; + explicit Token(f32 val, UnitOrOperator unit): val(val), unit(unit); + explicit Token(const string& str); + + bool isOperator(); + f32 evalValue(); + + f32 val; + UnitOrOperator unit; + }; + public: + Expression() = default; + Expression(const string& exp); + + void setExpression(string exp); + + f32 eval(); + private: + usize hash = 0; + std::queue infix {}; + + const static std::unordered_map PRECEDENCE; + }; +} \ No newline at end of file diff --git a/src/client/gui/Root.cpp b/src/client/gui/Root.cpp index cc3cc192..8405bb0d 100644 --- a/src/client/gui/Root.cpp +++ b/src/client/gui/Root.cpp @@ -1,5 +1,6 @@ #include "Root.h" +#include "util/Types.h" #include "util/Timer.h" #include "client/gui/BoxElement.h" @@ -7,20 +8,19 @@ Gui::Root::Root(Window& window, TextureAtlas& atlas) : atlas(atlas), window(window), body(make_shared(*this, stylesheets)) { - const ivec2 size = glm::ceil(vec2(window.getSize()) / static_cast(Gui::PX_SCALE)); + const ivec2 size = window.getSize(); body->setProps({ .id = "body", .styles = {{ - { StyleRule::WIDTH, size.x }, - { StyleRule::HEIGHT, size.y } + { StyleRule::SIZE, array { + std::to_string(size.x) + "px", std::to_string(size.y) + "px" } } }} }); lock = window.onResize([&](ivec2 size) { - size = glm::ceil(vec2(window.getSize()) / static_cast(Gui::PX_SCALE)); - body->setStyle(StyleRule::WIDTH, size.x); - body->setStyle(StyleRule::HEIGHT, size.y); + body->setStyle(StyleRule::SIZE, array { + std::to_string(size.x) + "px", std::to_string(size.y) + "px" }); Timer t("Resize UI"); body->updateElement(); t.printElapsedMs(); @@ -29,16 +29,26 @@ Gui::Root::Root(Window& window, TextureAtlas& atlas) : // window.input.bindMouseCallback() } +Gui::Root::~Root() { + window.setCursorHand(false); +} + void Gui::Root::addStylesheet(const std::unordered_map& sheet) { stylesheets.emplace_back(sheet); } void Gui::Root::update() { const let pos = window.input.getMousePos(); - body->handleMouseHover(pos); + bool pointer = false; + body->handleMouseHover(pos, pointer); + setCursorPointer(pointer); } void Gui::Root::draw(Renderer& renderer) { if (!body) return; body->draw(renderer); } + +void Gui::Root::setCursorPointer(bool hand) { + window.setCursorHand(hand); +} \ No newline at end of file diff --git a/src/client/gui/Root.h b/src/client/gui/Root.h index 513f20dd..22fc7c64 100644 --- a/src/client/gui/Root.h +++ b/src/client/gui/Root.h @@ -12,6 +12,8 @@ namespace Gui { public: Root(Window& window, TextureAtlas& atlas); + ~Root(); + template, bool> = true> sptr create(const Element::Props& props = {}, const vec>& children = {}) { let elem = make_shared(*this, stylesheets); @@ -28,6 +30,8 @@ namespace Gui { void draw(Renderer& renderer); + void setCursorPointer(bool hand); + vec stylesheets; const sptr body; diff --git a/src/client/gui/Style.h b/src/client/gui/Style.h index ab7adc8a..a63968ed 100644 --- a/src/client/gui/Style.h +++ b/src/client/gui/Style.h @@ -2,13 +2,14 @@ #include "client/gui/Gui.h" +#include "client/gui/Expression.h" + namespace Gui { enum class StyleRule { + POS, + SIZE, + MARGIN, PADDING, - WIDTH, - HEIGHT, - TOP, - LEFT, GAP, LAYOUT, @@ -16,10 +17,12 @@ namespace Gui { H_ALIGN, V_ALIGN, + CURSOR, OVERFLOW, - BACKGROUND, - BACKGROUND_HOVER + BACKGROUND_HOVER, + + CONTENT }; enum class ValueType { @@ -139,9 +142,30 @@ namespace Gui { L == ValueType::LENGTH, bool> = true> optional get(StyleRule rule) const { - return get(rule); + let raw = get(rule); + if (!raw) return std::nullopt; + return Gui::Expression(*raw).eval(); } + /** + * Returns an optional of the specified Rule's value, + * which is interpreted as a length. + */ + + template || std::is_floating_point_v) && + std::is_same_v> && + L == ValueType::LENGTH, bool> = true> + + optional get(StyleRule rule) const { + let raw = get>(rule); + if (!raw) return std::nullopt; + VN vec; + for (usize i = 0; i < VN::length(); i++) + vec[i] = Gui::Expression((*raw)[i]).eval(); + return vec; + } + /** * Returns the specified Rule's value as a V, * or the default value provided as the second parameter. diff --git a/src/client/gui/TextElement.cpp b/src/client/gui/TextElement.cpp new file mode 100644 index 00000000..5690d8c6 --- /dev/null +++ b/src/client/gui/TextElement.cpp @@ -0,0 +1,246 @@ +#include + +#include "TextElement.h" + +#include "client/gui/Root.h" +#include "client/graph/Model.h" +#include "client/graph/mesh/EntityMesh.h" + +void Gui::TextElement::updateElement() { + const string text = getStyle(StyleRule::CONTENT, ""); + + if (!font) { + font = std::make_unique(root.atlas, root.atlas["font"]); + } + + usize newHash = std::hash{}(text); + if (hash != newHash) { + hash = newHash; + + vec4 textColor = vec4(1); + vec4 backgroundColor = vec4(0, 0, 0, 0.3); + + u32 ind = 0; + u32 width = 0; + + vec vertices; + vertices.reserve(text.length() * 8 + 200); + vec indices; + indices.reserve(text.length() * 12 + 240); + + vec lines; + { + std::stringstream textStream(text); + string line; + while (std::getline(textStream, line, '\n')) lines.emplace_back(line); + } + + vec3 offset = {}; + u32 h = Font::charHeight; + + bool bold = false; + bool italic = false; + i32 underline = -1; + i32 strikethrough = -1; + u32 strikethroughVertStart = 0; + vec4 color = textColor; + + for (usize i = 0; i < lines.size(); i++) { + let& line = lines[i]; + bool empty = line.find_first_not_of(" \t\n") == -1; + + if (empty) { + offset.x = 0; + offset.y += h / 2; + continue; + } + + u32 bgVertStart = 0; + if (backgroundColor.w != 0) { + bgVertStart = vertices.size(); + for (u32 i = 0; i < 4; i++) vertices.push_back({}); + for (u32 i : INDICES) indices.push_back(i + ind); + ind += 4; + } + + for (usize j = 0; j < line.length() + 1; j++) { + char c = j < line.length() ? line[j] : ' '; + if (c == '\t') c = ' '; + + if (c == '`') { + bool flushDecorators = j == line.length(); + + char d = line[++j]; + if (d == '`') goto escape_formatting; + else if (d == ' ') offset.x++; + else if (d == 'b') bold = true; + else if (d == 'i') italic = true; + else if (d == 'u') underline = offset.x; + else if (d == 's') { + strikethrough = offset.x; + strikethroughVertStart = vertices.size(); + for (u32 i = 0; i < 4; i++) vertices.push_back({}); + for (u32 i : INDICES) indices.push_back(i + ind); + ind += 4; + } + else if (d == 'c') flushDecorators = true; + else if (d == 'r') { + bold = false; + italic = false; + flushDecorators = true; + } + + if (flushDecorators) { + if (underline != -1) { + TextElement::drawRect( + { underline, offset.y + h - 1, offset.x, offset.y + h }, + color, vertices, indices, ind); + TextElement::drawRect( + { underline + 1, offset.y + h, offset.x + 1, offset.y + h + 1 }, + color * BG_MULTIPLE, vertices, indices, ind); + underline = offset.x; + } + + if (strikethrough != -1) { + TextElement::drawRect( + { strikethrough, offset.y + h / 2, offset.x, offset.y + h / 2 + 1 }, + color, vertices, indices, ind); + TextElement::drawRect( + { strikethrough + 1, offset.y + h / 2 + 1, offset.x + 1, offset.y + h / 2 + 2 }, + color * BG_MULTIPLE, vertices, indices, ind, strikethroughVertStart); + strikethrough = offset.x; + } + + if (d == 'r') { + color = textColor; + underline = -1; + strikethrough = -1; + } + } + + if (d == 'c') { + char code = line[++j]; + if (code == 'r') color = textColor; + else { + u32 v; + std::stringstream ss; + ss << std::hex << code; + ss >> v; + color = COLORS[v]; + } + } + + continue; + } + + escape_formatting: + if (j == line.length()) continue; + + u32 w = font->getCharWidth(c) + 1; + vec4 UV = font->getCharUVs(c); + + for (u32 k = 0; k < (bold ? 4 : 2); k++) { + vec4 c = color; + + if (k == 0 || (k == 1 && bold)) c *= BG_MULTIPLE; + + if (k == 0) { + offset.x += 1; + offset.y += 1; + } + else if ((k == 1 || k == 3) && bold) { + offset.x += 1; + } + else if ((k == 1 && !bold) || (k == 2 && bold)) { + offset.x -= bold ? 2 : 1; + offset.y -= 1; + } + + vertices.emplace_back(offset + vec3(italic ? 2 : 0, 0, 0), vec4 { UV.x, UV.y, 0, c.w }, + vec3(c), 1.f, vec3 {}, ivec4 {}, vec4 {}); + + vertices.emplace_back(offset + vec3(0, h, 0), vec4 { UV.x, UV.w, 0, c.w }, + vec3(c), 1.f, vec3 {}, ivec4 {}, vec4 {}); + + vertices.emplace_back(offset + vec3(w, h, 0), vec4 { UV.z, UV.w, 0, c.w }, + vec3(c), 1.f, vec3 {}, ivec4 {}, vec4 {}); + + vertices.emplace_back(offset + vec3(w + (italic ? 2 : 0), 0, 0), vec4 { UV.z, UV.y, 0, c.w }, + vec3(c), 1.f, vec3 {}, ivec4 {}, vec4 {}); + + for (u32 i : INDICES) indices.push_back(i + ind); + ind += 4; + } + + offset.x += w; + } + + if (backgroundColor.w != 0) TextElement::drawRect({ -1, offset.y - 1, offset.x + 2, offset.y + h + 1 }, + backgroundColor, vertices, indices, ind, bgVertStart); + + if (offset.x > width) width = offset.x; + offset.x = 0; + offset.y += h + 2; + } + + let mesh = make_unique(); + mesh->create(vertices, indices); + + let model = make_shared(); + model->fromMesh(std::move(mesh)); + entity.setModel(model); + + } + + entity.setScale(PX_SCALE * (2/3.f)); +// entity.setScale(vec3(getComputedSize() * static_cast(PX_SCALE), 0)); + entity.setPos(vec3(getComputedScreenPos() * static_cast(PX_SCALE), 0)); + Element::updateElement(); +} + +void Gui::TextElement::drawRect(const vec4 pos, const vec4 color, + vec& vertices, vec& indices, u32& ind, const u32 insert) { + + vec myVerts = { + { vec3 { pos.x, pos.y, 0 }, color, vec3(1), 0.f, vec3 {}, ivec4 {}, vec4 {} }, + { vec3 { pos.x, pos.w, 0 }, color, vec3(1), 0.f, vec3 {}, ivec4 {}, vec4 {} }, + { vec3 { pos.z, pos.w, 0 }, color, vec3(1), 0.f, vec3 {}, ivec4 {}, vec4 {} }, + { vec3 { pos.z, pos.y, 0 }, color, vec3(1), 0.f, vec3 {}, ivec4 {}, vec4 {} } + }; + + if (insert != -1) { + vertices[insert] = myVerts[0]; + vertices[insert + 1] = myVerts[1]; + vertices[insert + 2] = myVerts[2]; + vertices[insert + 3] = myVerts[3]; + } + else { + for (EntityVertex& vert : myVerts) vertices.emplace_back(vert); + for (u32 i : INDICES) indices.push_back(i + ind); + ind += 4; + } +} + +const array Gui::TextElement::COLORS = { + Util::hexToColorVec("#ffffff"), + Util::hexToColorVec("#aaaaaa"), + Util::hexToColorVec("#666666"), + Util::hexToColorVec("#000000"), + + Util::hexToColorVec("#f53658"), + Util::hexToColorVec("#ff9940"), + Util::hexToColorVec("#fffb82"), + Util::hexToColorVec("#9fff80"), + Util::hexToColorVec("#0fa84f"), + Util::hexToColorVec("#26d4d4"), + Util::hexToColorVec("#7df4ff"), + Util::hexToColorVec("#33a2f5"), + Util::hexToColorVec("#2c58e8"), + Util::hexToColorVec("#b05cff"), + Util::hexToColorVec("#fd7dff"), + Util::hexToColorVec("#ff739f"), +}; + +const array Gui::TextElement::INDICES = { 0, 1, 2, 2, 3, 0 }; + +const vec4 Gui::TextElement::BG_MULTIPLE = { 0.3, 0.3, 0.35, 0.75 }; \ No newline at end of file diff --git a/src/client/gui/TextElement.h b/src/client/gui/TextElement.h new file mode 100644 index 00000000..5794354f --- /dev/null +++ b/src/client/gui/TextElement.h @@ -0,0 +1,35 @@ +#pragma once + +#include "Element.h" + +#include "client/graph/Font.h" +//#include "game/atlas/asset/AtlasRef.h" + +namespace Gui { + + /** + * Displays formatted text specified by the contact property. + */ + + class TextElement: public Element { + public: + using Element::Element; + + virtual void updateElement() override; + + protected: +// sptr tex; +// optional curBg; + + private: + uptr font; + usize hash = 0; + + void drawRect(const vec4 pos, const vec4 color, + vec& vertices, vec& indices, u32& ind, const u32 insert = -1); + + static const vec4 BG_MULTIPLE; + static const array INDICES; + static const array COLORS; + }; +} \ No newline at end of file diff --git a/src/client/scene/MainMenuScene.cpp b/src/client/scene/MainMenuScene.cpp index 9e1cdf20..e7b302d0 100644 --- a/src/client/scene/MainMenuScene.cpp +++ b/src/client/scene/MainMenuScene.cpp @@ -8,8 +8,8 @@ #include "util/Log.h" #include "ConnectScene.h" #include "client/Client.h" -#include "client/gui/Gui.h" #include "client/gui/BoxElement.h" +#include "client/gui/TextElement.h" #include "client/menu/SubgameDef.h" #include "client/gui/basic/GuiText.h" #include "game/atlas/asset/AtlasRef.h" @@ -19,79 +19,48 @@ MainMenuScene::MainMenuScene(Client& client) : Scene(client), root(client.renderer.window, client.game->textures) { -// components(make_unique()), -// menuContainer(make_shared("__menu")), -// sandbox(sandboxArea, client, menuContainer) { - client.renderer.setClearColor(0, 0, 0); client.renderer.window.input.setMouseLocked(false); -// Font f(client.game->textures, client.game->textures["font"]); -// win = client.renderer.window.getSize(); -// sandboxArea = win - ivec2(0, 18 * GS); - -// components->add(menuContainer); -// -// branding = make_shared("zephaBranding"); -// components->add(branding); -// { -// auto zephaText = make_shared("zephaText"); -// zephaText->create({ GS, GS }, {}, { 1, 1, 1, 1 }, {}, f); -// zephaText->setText("Zepha"); -// branding->add(zephaText); -// -// auto alphaText = make_shared("alphaText"); -// alphaText->create({ GS, GS }, {}, { 1, 0.5, 0.7, 1 }, {}, f); -// alphaText->setText("ALPHA"); -// alphaText->setPos({ 25 * GS, 0 }); -// branding->add(alphaText); -// } - -// root.body->setStyle(Gui::Style::Rule::DIRECTION, "row"); -// root.body->setStyle(Gui::Style::Rule::GAP_X, 8); -// root.body->setStyle(Gui::Style::Rule::GAP_Y, 8); -// root.body->setStyle(Gui::Style::Rule::H_ALIGN, "center"); -// root.body->setStyle(Gui::Style::Rule::V_ALIGN, "center"); - root.body->setStyle(Gui::StyleRule::BACKGROUND, string("#123")); root.addStylesheet({ { "sandbox", {{ + { Gui::StyleRule::H_ALIGN, string("center") }, + { Gui::StyleRule::V_ALIGN, string("center") } }}}, { "navigation", {{ - { Gui::StyleRule::HEIGHT, 18 } + { Gui::StyleRule::SIZE, array { "-1", "18dp" } } }}}, { "navigationWrap", {{ { Gui::StyleRule::DIRECTION, string("row") }, - { Gui::StyleRule::TOP, 0 }, - { Gui::StyleRule::LEFT, 0 } + { Gui::StyleRule::POS, array { "0", "0" } } }}}, { "navigationBackground", {{ - { Gui::StyleRule::WIDTH, 64 }, - { Gui::StyleRule::HEIGHT, 18 }, + { Gui::StyleRule::SIZE, array { "64dp", "18dp" } }, { Gui::StyleRule::BACKGROUND, string("menu_bar_bg") } }}}, { "navigationButton", {{ - { Gui::StyleRule::WIDTH, 16 }, - { Gui::StyleRule::HEIGHT, 16 } + { Gui::StyleRule::SIZE, array { "16dp", "16dp" } }, + { Gui::StyleRule::CURSOR, string("pointer") } }}} }); - let sandbox = root.body->append({ .classes = { "sandbox" } }); + let sandbox = root.body->append({ .classes = { "sandbox" } }); let navigation = root.body->append({ .classes = { "navigation" } }); let navigationBG = navigation->append({ .classes = { "navigationWrap" } }); - + for (usize i = 0; i < 2000 / Gui::PX_SCALE / 64; i++) navigationBG->append({ .classes = { "navigationBackground" } }); - + let navigationList = navigation->append({ .classes = { "navigationWrap" }, .styles = {{ - { Gui::StyleRule::PADDING, ivec4(1) }, - { Gui::StyleRule::GAP, ivec2(1) } + { Gui::StyleRule::PADDING, array { "1dp", "1dp", "1dp", "1dp" } }, + { Gui::StyleRule::GAP, array { "1dp", "1dp" } } }} }); - + let serversButton = navigationList->append({ .classes = { "navigationButton" }, .styles = {{ @@ -99,7 +68,7 @@ MainMenuScene::MainMenuScene(Client& client) : Scene(client), { Gui::StyleRule::BACKGROUND_HOVER, string("crop(16, 0, 16, 16, menu_flag_multiplayer)") } }} }); - + let contentButton = navigationList->append({ .classes = { "navigationButton" }, .styles = {{ @@ -107,9 +76,46 @@ MainMenuScene::MainMenuScene(Client& client) : Scene(client), { Gui::StyleRule::BACKGROUND_HOVER, string("crop(16, 0, 16, 16, menu_flag_content)") } }} }); - - navigationList->append({}); - + + navigationList->append({ + .styles = {{ + { Gui::StyleRule::BACKGROUND, string("#fff5") }, + { Gui::StyleRule::SIZE, array { "1dp", "10dp" } }, + { Gui::StyleRule::MARGIN, array { "2dp", "3dp", "2dp", "3dp" } } + }} + }); + + findSubgames(); + + for (usize i = 0; i < subgames.size(); i++) { + let& subgame = subgames[i]; + navigationList->append({ + .classes = { "navigationButton" }, + .styles = {{ + { Gui::StyleRule::BACKGROUND, string("crop(0, 0, 16, 16, " + subgame.iconRef->name + ")") }, + { Gui::StyleRule::BACKGROUND_HOVER, string("crop(16, 0, 16, 16, " + subgame.iconRef->name + ")") } + }} + }); +// +// button->setCallback(Element::CallbackType::PRIMARY, [&](bool down, ivec2) { +// if (!down) return; +// selectedSubgame = &subgame; +// sandbox.load(*selectedSubgame); +// }); + } + + if (subgames.size() > 0) { + selectedSubgame = &subgames[0]; +// sandbox.load(*selectedSubgame); + } + + navigationList->append({ + .styles = {{ + { Gui::StyleRule::BACKGROUND, string("#f006") }, + { Gui::StyleRule::SIZE, array { "-1", "16dp" } } + }} + }); + let settingsButton = navigationList->append({ .classes = { "navigationButton" }, .styles = {{ @@ -117,7 +123,7 @@ MainMenuScene::MainMenuScene(Client& client) : Scene(client), { Gui::StyleRule::BACKGROUND_HOVER, string("crop(16, 0, 16, 16, menu_flag_settings)") } }} }); - + let closeButton = navigationList->append({ .classes = { "navigationButton" }, .styles = {{ @@ -125,7 +131,7 @@ MainMenuScene::MainMenuScene(Client& client) : Scene(client), { Gui::StyleRule::BACKGROUND_HOVER, string("crop(16, 0, 16, 16, menu_flag_quit)") } }} }); - + // closeButton->setCallback(Element::CallbackType::PRIMARY, // [](bool down, ivec2) { if (down) exit(0); }); @@ -134,47 +140,10 @@ MainMenuScene::MainMenuScene(Client& client) : Scene(client), // client.scene.setScene(make_unique(client, Address{ "127.0.0.1" })); // }); -// -// navigationBarIcons->add(contentButton); -// // auto divider = make_shared("divider"); // divider->create({ GS, GS * 10 }, {}, { 1, 1, 1, 0.3 }); // divider->setPos({ GS * 2 + GS * 18 * 2, GS * 4 }); // navigationBarIcons->add(divider); -// -// findSubgames(); -// -// for (usize i = 0; i < subgames.size(); i++) { -// auto& subgame = subgames[i]; -// auto button = make_shared(subgame.config.name); -// -// button->create({ 16 * GS, 16 * GS }, {}, -// client.game->textures["crop(0, 0, 16, 16, " + subgame.iconRef->name + ")"], -// client.game->textures["crop(16, 0, 16, 16, " + subgame.iconRef->name + ")"]); -// -// button->setPos({ GS * 7 + GS * 18 * (i + 2), GS }); -// button->setCallback(Element::CallbackType::PRIMARY, [&](bool down, ivec2) { -// if (!down) return; -// selectedSubgame = &subgame; -// sandbox.load(*selectedSubgame); -// }); -// -// navigationBarIcons->add(button); -// } -// } -// -// if (subgames.size() > 0) { -// selectedSubgame = &subgames[0]; -// sandbox.load(*selectedSubgame); -// } -// -// positionElements(); -// -// lock = client.renderer.window.onResize([&](ivec2 newWin) { -// win = newWin; -// sandboxArea = newWin - ivec2(0, 18 * GS); -// positionElements(); -// }); } void MainMenuScene::findSubgames() { @@ -252,26 +221,6 @@ void MainMenuScene::findSubgames() { [](SubgameDef& a, SubgameDef& b) { return a.config.name < b.config.name; }); } -void MainMenuScene::positionElements() { -// sandbox.windowResized(); -// -// branding->setPos({ win.x - 55 * GS, win.y - 30 * GS }); -// -// navigationBar->setPos({ 0, win.y - 18 * GS }); -// -// auto navigationBarBg = navigationBar->get("navigationBarBg"); -// for (usize i = 0; i < static_cast(win.x) / 64.f / GS; i++) { -// auto segment = make_shared("segment_" + std::to_string(i)); -// segment->create({ 64 * GS, 18 * GS }, {}, client.game->textures["menu_bar_bg"]); -// segment->setPos({ i * 64 * GS, 0 }); -// navigationBarBg->add(segment); -// } -// -// auto navigationBarIcons = navigationBar->get("navigationBarIcons"); -// navigationBarIcons->get("closeButton")->setPos({ win.x - 16 * GS - GS, GS }); -// navigationBarIcons->get("settingsButton")->setPos({ win.x - 16 * GS * 2 - GS * 3, GS }); -} - void MainMenuScene::update() { client.game->textures.update(); root.update(); @@ -289,9 +238,4 @@ void MainMenuScene::draw() { renderer.beginGUIDrawCalls(); renderer.enableTexture(&client.game->textures.atlasTexture); root.draw(renderer); -// components->draw(client.renderer); -} - -void MainMenuScene::cleanup() { - client.renderer.window.setCursorHand(false); } \ No newline at end of file diff --git a/src/client/scene/MainMenuScene.h b/src/client/scene/MainMenuScene.h index c9017756..e44e2f15 100644 --- a/src/client/scene/MainMenuScene.h +++ b/src/client/scene/MainMenuScene.h @@ -24,31 +24,11 @@ public: void draw() override; - void cleanup() override; - private: - /** Repositions elements after a window resize. */ - void positionElements(); - - /** Finds valid subgames in the subgames folder. */ + /** Find valid subgames in the subgames folder. */ void findSubgames(); - /** The UI scaling, in pixels. */ - static constexpr f32 GS = 3; - - /** The dimensions of the window. */ - ivec2 win {}; - - /** The dimensions of the sandbox area. */ - ivec2 sandboxArea {}; - - /** Element references. */ -// uptr components; -// sptr branding; -// sptr navigationBar; -// sptr menuContainer; - /** Provides the API for menu mods. */ // MenuSandbox sandbox; Gui::Root root; @@ -58,7 +38,5 @@ private: /** A reference to the currently selected subgame. */ SubgameDef* selectedSubgame = nullptr; - - Window::RCBLock lock; }; diff --git a/subgames/zeus/menu/script/init.lua b/subgames/zeus/menu/script/init.lua index 009334fc..2edda682 100644 --- a/subgames/zeus/menu/script/init.lua +++ b/subgames/zeus/menu/script/init.lua @@ -1,52 +1,52 @@ local menu = zepha.build_gui(function() - return Gui.Body { - background = "zeus_background_christmas_night", + return Gui.Box { + background = 'zeus_background_christmas_night', - Gui.Rect { - key = "particle_wrap", - size = { pc(100), pc(100) } + Gui.Box { + key = 'particle_wrap', + size = { '100%', '100%' } }, - Gui.Rect { - key = "sidebar", - position = { pc(20), 0 }, - position_anchor = { pc(50), 0 }, - size = { 102, pc(100) }, - background = "#0135", + Gui.Box { + pos = { '20% - 50s%', 0 }, + size = { 102, '100%' }, - Gui.Rect { - key = "logo", - position = { 8, 8 }, + background = '#0135', + + Gui.Box { + pos = { 8, 8 }, size = { 86, 30 }, - background = "zeus_logo" + + background = 'zeus_logo' }, Gui.Button { - key = "buttonPlay", + id = 'button_play', - callbacks = { - primary = function() zepha.start_game_local() end - }, +-- callbacks = { +-- primary = function() zepha.start_game_local() end +-- }, - position = { 6, 50 }, + pos = { 6, 50 }, size = { 90, 20 }, - background = "crop(0, 0, 90, 20, zeus_button)", - background_hover = "crop(0, 20, 90, 20, zeus_button)", - content = "Local Play" + + content = 'Local Play', + background = 'crop(0, 0, 90, 20, zeus_button)', + background_hover = 'crop(0, 20, 90, 20, zeus_button)' }, Gui.Button { - key = "buttonServers", + id = 'button_servers', - callbacks = { - primary = function() zepha.start_game() end - }, +-- callbacks = { +-- primary = function() zepha.start_game() end +-- }, - position = { 6, 74 }, + pos = { 6, 74 }, size = { 90, 20 }, - background = "crop(0, 0, 90, 20, zeus_button)", - background_hover = "crop(0, 20, 90, 20, zeus_button)", - content = "Browse Servers" + content = 'Browse Servers', + background = 'crop(0, 0, 90, 20, zeus_button)', + background_hover = 'crop(0, 20, 90, 20, zeus_button)' } } } @@ -62,35 +62,37 @@ end) -- ) end) -- end, 1) -local particle_wrap = menu:get("particle_wrap") +local particle_wrap = menu:get('particle_wrap') menu(function() for _ = 1, 20 do local scale = 6 + math.random() * 4 particle_wrap:append(Gui.Rect { - position = { math.floor(math.random() * 600), math.floor(math.random() * 320) }, - background = "particle_dark", + pos = { math.floor(math.random() * 600), math.floor(math.random() * 320) }, + background = 'particle_dark', size = { scale, scale } }) end end) -local tick = 0 -zepha.after(function() - local i = 1 - local part = particle_wrap:get(i) - tick = tick + 0.012 - while part ~= nil do - local xO = (-math.sin(tick) * 0.0125 - 0.0025) * part.size[1] - local pos = { part.position[1] + xO, part.position[2] + 0.05 * part.size[1] } - if pos[2] > 320 then pos[2] = -12 end - if pos[1] < -12 then pos[1] = 600 end - part.position = pos - i = i + 1 - part = particle_wrap:get(i) - end - return true -end, 0.016) - - +-- local tick = 0 +-- zepha.after(function() +-- local i = 1 +-- local part = particle_wrap:get(i) +-- tick = tick + 0.012 +-- +-- while part ~= nil do +-- local xO = (-math.sin(tick) * 0.0125 - 0.0025) * part.size[1] +-- local pos = { part.pos[1] + xO, part.pos[2] + 0.05 * part.size[1] } +-- +-- if pos[2] > 320 then pos[2] = -12 end +-- if pos[1] < -12 then pos[1] = 600 end +-- +-- i = i + 1 +-- part.pos = pos +-- part = particle_wrap:get(i) +-- end +-- +-- return true +-- end, 0.016) zepha.set_gui(menu)