Expression Parsing

master
Auri 2021-08-20 16:19:10 -07:00
parent d8dac4d74e
commit 1ec7598ac0
15 changed files with 769 additions and 234 deletions

View File

@ -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 .)

View File

@ -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<vec4, ValueType::COLOR>(bgRule);
const string bgImage = getStyle<string>(bgRule, "");
@ -50,8 +48,10 @@ void Gui::BoxElement::updateElement() {
entity.setModel(model);
}
entity.setScale(vec3(getComputedSize() * static_cast<i32>(PX_SCALE), 0));
entity.setPos(vec3(getComputedScreenPos() * static_cast<i32>(PX_SCALE), 0));
let margin = getStyle<ivec4, ValueType::LENGTH>(StyleRule::MARGIN, {});
entity.setScale(vec3(getComputedSize(), 0));
entity.setPos(vec3(getComputedScreenPos() + ivec2 { margin.x, margin.y }, 0));
Element::updateElement();
}

View File

@ -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;

View File

@ -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<i32, ValueType::LENGTH>(StyleRule::WIDTH, std::max(layoutSize.x, 0)),
getStyle<i32, ValueType::LENGTH>(StyleRule::HEIGHT, std::max(layoutSize.y, 0))
};
let size = getStyle<ivec2, ValueType::LENGTH>(StyleRule::SIZE, glm::max(layoutSize, 0));
return size;
}
ivec2 Gui::Element::getComputedOuterSize() {
let size = getComputedSize();
let margin = getStyle<ivec4, ValueType::LENGTH>(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<ivec4>(StyleRule::PADDING, {});
return glm::max(ivec2 { size.x - padding.x - padding.z, size.y - padding.y - padding.w }, ivec2 {});
let padding = getStyle<ivec4, ValueType::LENGTH>(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<i32, ValueType::LENGTH>(StyleRule::WIDTH, -1),
getStyle<i32, ValueType::LENGTH>(StyleRule::HEIGHT, -1)
};
return getStyle<ivec2, ValueType::LENGTH>(StyleRule::SIZE, ivec2(-1));
}
ivec2 Gui::Element::getComputedPos() {
return {
getStyle<i32, ValueType::LENGTH>(StyleRule::LEFT, layoutPosition.x),
getStyle<i32, ValueType::LENGTH>(StyleRule::TOP, layoutPosition.y)
};
return getStyle<ivec2, ValueType::LENGTH>(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<i32>(PX_SCALE);
ivec2 pos = getComputedScreenPos() * static_cast<i32>(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<string>(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<ivec2>(StyleRule::GAP, ivec2(0))[primary];
const ivec4& padding = getStyle<ivec4>(StyleRule::PADDING, ivec4 {});
const i32 gap = getStyle<ivec2, ValueType::LENGTH>(StyleRule::GAP, ivec2(0))[primary];
const ivec4& padding = getStyle<ivec4, ValueType::LENGTH>(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<ivec4, ValueType::LENGTH>(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<ivec4, ValueType::LENGTH>(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;

View File

@ -1,6 +1,5 @@
#pragma once
#include <any>
#include <list>
#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<StyleSheet>& 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<typename E, std::enable_if_t<std::is_base_of_v<Element, E>, bool> = true>
sptr<E> prepend(const Props& props = {}) {
const let elem = make_shared<E>(root, stylesheets);
@ -45,6 +54,7 @@ namespace Gui {
return elem;
};
/** Prepends an existing element to this element. */
sptr<Element> prepend(sptr<Element> 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<typename E, std::enable_if_t<std::is_base_of_v<Element, E>, bool> = true>
sptr<E> append(const Props& props = {}) {
const let elem = make_shared<E>(root, stylesheets);
@ -60,6 +71,7 @@ namespace Gui {
return elem;
};
/** Appends an existing element to this element. */
sptr<Element> append(sptr<Element> 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<any> getStyle(StyleRule rule) const {
const optional<any> 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<typename V, ValueType T = ValueType::LITERAL>
const optional<V> getStyle(StyleRule rule) const {
const optional<V> opt = props.styles.get<V, T>(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<typename V, ValueType T = ValueType::LITERAL>
const V getStyle(StyleRule rule, V def) const {
const optional<V> opt = getStyle<V, T>(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();
};
}

View File

@ -0,0 +1,175 @@
#include <stack>
#include <math.h>
#include <iostream>
#include <algorithm>
#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<string>{}(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<char> 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<String> 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<char, u8> 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;
}
}

View File

@ -0,0 +1,57 @@
#pragma once
#include <queue>
#include <unordered_map>
#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<String> infix {};
const static std::unordered_map<char, u8> PRECEDENCE;
};
}

View File

@ -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<BoxElement>(*this, stylesheets)) {
const ivec2 size = glm::ceil(vec2(window.getSize()) / static_cast<f32>(Gui::PX_SCALE));
const ivec2 size = window.getSize();
body->setProps({
.id = "body",
.styles = {{
{ StyleRule::WIDTH, size.x },
{ StyleRule::HEIGHT, size.y }
{ StyleRule::SIZE, array<string, 2> {
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<f32>(Gui::PX_SCALE));
body->setStyle(StyleRule::WIDTH, size.x);
body->setStyle(StyleRule::HEIGHT, size.y);
body->setStyle(StyleRule::SIZE, array<string, 2> {
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<string, Style>& 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);
}

View File

@ -12,6 +12,8 @@ namespace Gui {
public:
Root(Window& window, TextureAtlas& atlas);
~Root();
template<typename E, std::enable_if_t<std::is_base_of_v<Element, E>, bool> = true>
sptr<E> create(const Element::Props& props = {}, const vec<sptr<Element>>& children = {}) {
let elem = make_shared<E>(*this, stylesheets);
@ -28,6 +30,8 @@ namespace Gui {
void draw(Renderer& renderer);
void setCursorPointer(bool hand);
vec<StyleSheet> stylesheets;
const sptr<Element> body;

View File

@ -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<N> get(StyleRule rule) const {
return get<N>(rule);
let raw = get<string>(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<typename VN, ValueType L, std::enable_if_t<
(std::is_integral_v<typename VN::value_type> || std::is_floating_point_v<typename VN::value_type>) &&
std::is_same_v<VN, glm::vec<VN::length(), typename VN::value_type>> &&
L == ValueType::LENGTH, bool> = true>
optional<VN> get(StyleRule rule) const {
let raw = get<array<string, VN::length()>>(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.

View File

@ -0,0 +1,246 @@
#include <functional>
#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<string>(StyleRule::CONTENT, "");
if (!font) {
font = std::make_unique<Font>(root.atlas, root.atlas["font"]);
}
usize newHash = std::hash<string>{}(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<EntityVertex> vertices;
vertices.reserve(text.length() * 8 + 200);
vec<u32> indices;
indices.reserve(text.length() * 12 + 240);
vec<string> 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<EntityMesh>();
mesh->create(vertices, indices);
let model = make_shared<Model>();
model->fromMesh(std::move(mesh));
entity.setModel(model);
}
entity.setScale(PX_SCALE * (2/3.f));
// entity.setScale(vec3(getComputedSize() * static_cast<i32>(PX_SCALE), 0));
entity.setPos(vec3(getComputedScreenPos() * static_cast<i32>(PX_SCALE), 0));
Element::updateElement();
}
void Gui::TextElement::drawRect(const vec4 pos, const vec4 color,
vec<EntityVertex>& vertices, vec<u32>& indices, u32& ind, const u32 insert) {
vec<EntityVertex> 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<vec4, 16> 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<u32, 6> Gui::TextElement::INDICES = { 0, 1, 2, 2, 3, 0 };
const vec4 Gui::TextElement::BG_MULTIPLE = { 0.3, 0.3, 0.35, 0.75 };

View File

@ -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<AtlasRef> tex;
// optional<any> curBg;
private:
uptr<Font> font;
usize hash = 0;
void drawRect(const vec4 pos, const vec4 color,
vec<EntityVertex>& vertices, vec<u32>& indices, u32& ind, const u32 insert = -1);
static const vec4 BG_MULTIPLE;
static const array<u32, 6> INDICES;
static const array<vec4, 16> COLORS;
};
}

View File

@ -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<GuiContainer>()),
// menuContainer(make_shared<GuiContainer>("__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<GuiContainer>("zephaBranding");
// components->add(branding);
// {
// auto zephaText = make_shared<GuiText>("zephaText");
// zephaText->create({ GS, GS }, {}, { 1, 1, 1, 1 }, {}, f);
// zephaText->setText("Zepha");
// branding->add(zephaText);
//
// auto alphaText = make_shared<GuiText>("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<string, 2> { "-1", "18dp" } }
}}},
{ "navigationWrap", {{
{ Gui::StyleRule::DIRECTION, string("row") },
{ Gui::StyleRule::TOP, 0 },
{ Gui::StyleRule::LEFT, 0 }
{ Gui::StyleRule::POS, array<string, 2> { "0", "0" } }
}}},
{ "navigationBackground", {{
{ Gui::StyleRule::WIDTH, 64 },
{ Gui::StyleRule::HEIGHT, 18 },
{ Gui::StyleRule::SIZE, array<string, 2> { "64dp", "18dp" } },
{ Gui::StyleRule::BACKGROUND, string("menu_bar_bg") }
}}},
{ "navigationButton", {{
{ Gui::StyleRule::WIDTH, 16 },
{ Gui::StyleRule::HEIGHT, 16 }
{ Gui::StyleRule::SIZE, array<string, 2> { "16dp", "16dp" } },
{ Gui::StyleRule::CURSOR, string("pointer") }
}}}
});
let sandbox = root.body->append<Gui::BoxElement>({ .classes = { "sandbox" } });
let sandbox = root.body->append<Gui::BoxElement>({ .classes = { "sandbox" } });
let navigation = root.body->append<Gui::BoxElement>({ .classes = { "navigation" } });
let navigationBG = navigation->append<Gui::BoxElement>({ .classes = { "navigationWrap" } });
for (usize i = 0; i < 2000 / Gui::PX_SCALE / 64; i++)
navigationBG->append<Gui::BoxElement>({ .classes = { "navigationBackground" } });
let navigationList = navigation->append<Gui::BoxElement>({
.classes = { "navigationWrap" },
.styles = {{
{ Gui::StyleRule::PADDING, ivec4(1) },
{ Gui::StyleRule::GAP, ivec2(1) }
{ Gui::StyleRule::PADDING, array<string, 4> { "1dp", "1dp", "1dp", "1dp" } },
{ Gui::StyleRule::GAP, array<string, 2> { "1dp", "1dp" } }
}}
});
let serversButton = navigationList->append<Gui::BoxElement>({
.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<Gui::BoxElement>({
.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<Gui::BoxElement>({});
navigationList->append<Gui::BoxElement>({
.styles = {{
{ Gui::StyleRule::BACKGROUND, string("#fff5") },
{ Gui::StyleRule::SIZE, array<string, 2> { "1dp", "10dp" } },
{ Gui::StyleRule::MARGIN, array<string, 4> { "2dp", "3dp", "2dp", "3dp" } }
}}
});
findSubgames();
for (usize i = 0; i < subgames.size(); i++) {
let& subgame = subgames[i];
navigationList->append<Gui::BoxElement>({
.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<Gui::BoxElement>({
.styles = {{
{ Gui::StyleRule::BACKGROUND, string("#f006") },
{ Gui::StyleRule::SIZE, array<string, 2> { "-1", "16dp" } }
}}
});
let settingsButton = navigationList->append<Gui::BoxElement>({
.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<Gui::BoxElement>({
.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<ConnectScene>(client, Address{ "127.0.0.1" }));
// });
//
// navigationBarIcons->add(contentButton);
//
// auto divider = make_shared<GuiRect>("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<GuiImageButton>(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<GuiContainer>("navigationBarBg");
// for (usize i = 0; i < static_cast<f32>(win.x) / 64.f / GS; i++) {
// auto segment = make_shared<GuiRect>("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<GuiContainer>("navigationBarIcons");
// navigationBarIcons->get<GuiImageButton>("closeButton")->setPos({ win.x - 16 * GS - GS, GS });
// navigationBarIcons->get<GuiImageButton>("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);
}

View File

@ -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<GuiContainer> components;
// sptr<GuiContainer> branding;
// sptr<GuiContainer> navigationBar;
// sptr<GuiContainer> 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;
};

View File

@ -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)