diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f9648c5b..1a094980 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -53,6 +53,7 @@ set( app/gui/widgets.cc app/gui/color.cc app/gui/pipeline.cc + app/gui/zeml.cc ) set( diff --git a/src/app/gui/zeml.cc b/src/app/gui/zeml.cc new file mode 100644 index 00000000..b9326bae --- /dev/null +++ b/src/app/gui/zeml.cc @@ -0,0 +1,297 @@ + +#include "app/gui/zeml.h" + +#include + +#include +#include +#include +#include +#include + +namespace yaze { +namespace app { +namespace gui { +namespace zeml { + +std::vector Tokenize(const std::string& input) { + std::vector tokens; + std::istringstream stream(input); + char ch; + + while (stream.get(ch)) { + if (isspace(ch)) continue; + + if (ch == '{') { + tokens.push_back({TokenType::OpenBrace, "{"}); + } else if (ch == '}') { + tokens.push_back({TokenType::CloseBrace, "}"}); + } else if (ch == ',') { + tokens.push_back({TokenType::Comma, ","}); + } else if (std::isalnum(ch) || ch == '_') { + std::string ident(1, ch); + while (stream.get(ch) && (std::isalnum(ch) || ch == '_')) { + ident += ch; + } + stream.unget(); + tokens.push_back({TokenType::Identifier, ident}); + } else if (ch == '"' || ch == '\'') { + std::string str; + char quoteType = ch; + while (stream.get(ch) && ch != quoteType) { + str += ch; + } + tokens.push_back({TokenType::String, str}); + } + } + + tokens.push_back({TokenType::EndOfStream, ""}); + return tokens; +} + +WidgetType MapType(const std::string& type) { + static std::map typeMap = { + {"Window", WidgetType::Window}, + {"Button", WidgetType::Button}, + {"Slider", WidgetType::Slider}, + {"Text", WidgetType::Text}, + {"CollapsingHeader", WidgetType::CollapsingHeader}, + {"Columns", WidgetType::Columns}, + {"HexInputByte", WidgetType::HexInputByte}, + {"HexInputWord", WidgetType::HexInputWord}, + {"Table", WidgetType::Table}, + {"Selectable", WidgetType::Selectable}, + {"TableSetupColumn", WidgetType::TableSetupColumn}, + {"TableHeadersRow", WidgetType::TableHeadersRow}, + {"TableNextColumn", WidgetType::TableNextColumn}, + {"Function", WidgetType::Function}, + {"BeginChild", WidgetType::BeginChild}, + {"BeginMenu", WidgetType::BeginMenu}, + {"MenuItem", WidgetType::MenuItem}, + {"BeginMenuBar", WidgetType::BeginMenuBar}, + {"Separator", WidgetType::Separator}, + }; + return typeMap[type]; +} + +Node ParseNode(const std::vector& tokens, size_t& index, + const std::map& data_bindings) { + Node node; + if (index >= tokens.size() || tokens[index].type == TokenType::EndOfStream) { + return node; + } + + while (index < tokens.size() && + tokens[index].type != TokenType::EndOfStream) { + Token token = tokens[index]; + if (token.type == TokenType::Identifier) { + node.type = MapType(token.value); + index++; // Move to the next token for attributes + node.attributes = + ParseAttributes(tokens, index, node.type, data_bindings); + } + + // Handle the opening brace indicating the start of child nodes + if (index < tokens.size() && tokens[index].type == TokenType::OpenBrace) { + index++; // Skip the opening brace + + while (index < tokens.size() && + tokens[index].type != TokenType::CloseBrace) { + if (tokens[index].type == TokenType::Comma) { + index++; // Skip commas + } else { + node.children.push_back(ParseNode(tokens, index, data_bindings)); + } + } + + if (index < tokens.size() && + tokens[index].type == TokenType::CloseBrace) { + index++; // Ensure closing brace is skipped before returning + } + } + + break; // Exit after processing one complete node + } + return node; +} + +WidgetAttributes ParseAttributes( + const std::vector& tokens, size_t& index, const WidgetType& type, + const std::map& data_bindings) { + WidgetAttributes attributes; + + while (index < tokens.size() && tokens[index].type != TokenType::CloseBrace) { + if (tokens[index].type == TokenType::Identifier) { + Token keyToken = tokens[index]; + index++; // Move to the value token. + if (index < tokens.size() && tokens[index].type == TokenType::String) { + std::string value = tokens[index].value; + index++; // Move past the value. + + if (keyToken.value == "id") + attributes.id = value; + else if (keyToken.value == "title") + attributes.title = value; + else if (keyToken.value == "min") + attributes.min = std::stod(value); + else if (keyToken.value == "max") + attributes.max = std::stod(value); + else if (keyToken.value == "value") + attributes.value = std::stod(value); + else if (keyToken.value == "text") + attributes.text = value; + else if (keyToken.value == "data" && + data_bindings.find(value) != data_bindings.end()) { + attributes.data = data_bindings.at(value); + } else if (keyToken.value == "count") { + attributes.count = std::stoi(value); + } else if (keyToken.value == "flags") { + attributes.flags = nullptr; // Placeholder for future use + } else if (keyToken.value == "size") { + attributes.size = ImVec2(0, 0); // Placeholder for future use + } + } + } else { + // If it's not an identifier or we encounter an open brace, break out. + break; + } + } + return attributes; +} + +void Render(Node& node) { + switch (node.type) { + case WidgetType::Window: + if (ImGui::Begin(node.attributes.title.c_str())) { + for (auto& child : node.children) { + Render(child); + } + ImGui::End(); + } + break; + case WidgetType::Button: + if (ImGui::Button(node.attributes.text.c_str())) { + ExecuteActions(node.actions, ActionType::Click); + } + break; + case WidgetType::CollapsingHeader: + if (ImGui::CollapsingHeader(node.attributes.title.c_str())) { + for (auto& child : node.children) { + Render(child); + } + } + break; + case WidgetType::Columns: + ImGui::Columns(node.attributes.count, node.attributes.title.c_str()); + ImGui::Separator(); + for (auto& child : node.children) { + Render(child); + ImGui::NextColumn(); + } + ImGui::Columns(1); + ImGui::Separator(); + break; + case WidgetType::Table: + ImGui::BeginTable(node.attributes.id.c_str(), node.attributes.count); + for (auto& child : node.children) { + Render(child); + } + ImGui::EndTable(); + break; + case WidgetType::TableSetupColumn: + ImGui::TableSetupColumn(node.attributes.title.c_str()); + break; + case WidgetType::TableHeadersRow: + ImGui::TableHeadersRow(); + break; + case WidgetType::TableNextColumn: + ImGui::TableNextColumn(); + for (auto& child : node.children) { + Render(child); + } + break; + case WidgetType::Text: + if (node.attributes.data) { + // Assuming all data-bound Text widgets use string formatting + char formattedText[256]; + snprintf(formattedText, sizeof(formattedText), + node.attributes.text.c_str(), *(int*)node.attributes.data); + ImGui::Text("%s", formattedText); + } else { + ImGui::Text("%s", node.attributes.text.c_str()); + } + break; + case WidgetType::Function: { + node.actions[0].callback(); + break; + } + case WidgetType::BeginChild: + if (ImGui::BeginChild(node.attributes.id.c_str(), node.attributes.size)) { + for (auto& child : node.children) { + Render(child); + } + ImGui::EndChild(); + } + break; + case WidgetType::BeginMenuBar: + if (ImGui::BeginMenuBar()) { + for (auto& child : node.children) { + Render(child); + } + ImGui::EndMenuBar(); + } + break; + case WidgetType::BeginMenu: { + if (ImGui::BeginMenu(node.attributes.title.c_str())) { + for (auto& child : node.children) { + Render(child); + } + ImGui::EndMenu(); + } + break; + } + case WidgetType::MenuItem: { + if (ImGui::MenuItem(node.attributes.title.c_str())) { + ExecuteActions(node.actions, ActionType::Click); + } + break; + } + case WidgetType::Separator: + ImGui::Separator(); + break; + default: + break; + } +} + +Node Parse(const std::string& yazon_input, + const std::map& data_bindings) { + size_t index = 0; + auto tokens = Tokenize(yazon_input); + return ParseNode(tokens, index, data_bindings); +} + +void ExecuteActions(const std::vector& actions, ActionType type) { + for (const auto& action : actions) { + if (action.type == type) { + action.callback(); // Execute the callback associated with the action + } + } +} + +void Bind(Node* node, std::function callback) { + if (node) { + node->actions.push_back({ActionType::Click, callback}); + } +} + +void BindAction(Node* node, ActionType type, std::function callback) { + if (node) { + node->actions.push_back({type, callback}); + } +} + +} // namespace zeml +} // namespace gui +} // namespace app +} // namespace yaze diff --git a/src/app/gui/zeml.h b/src/app/gui/zeml.h new file mode 100644 index 00000000..ef8ba610 --- /dev/null +++ b/src/app/gui/zeml.h @@ -0,0 +1,190 @@ +#ifndef YAZE_APP_GUI_ZEML_H +#define YAZE_APP_GUI_ZEML_H + +#include + +#include +#include +#include +#include +#include + +namespace yaze { +namespace app { +namespace gui { + +/** + * @namespace yaze::app::gui::zeml + * @brief Zelda Editor Markup Language Functions + */ +namespace zeml { + +/** + * @enum TokenType + */ +enum class TokenType { + Identifier, + String, + OpenBrace, + CloseBrace, + Comma, + EndOfStream +}; + +/** + * @struct Token + */ +struct Token { + TokenType type; + std::string value; +} typedef Token; + +/** + * @enum WidgetType + */ +enum class WidgetType { + Window, + Button, + Slider, + Text, + Table, + TableSetupColumn, + TableHeadersRow, + TableNextColumn, + CollapsingHeader, + Columns, + Selectable, + Function, + BeginChild, + BeginMenuBar, + BeginMenu, + MenuItem, + Separator, + HexInputByte, + HexInputWord, +}; + +/** + * @struct WidgetAttributes + * @brief Attributes for a widget + * @details id, title, min, max, value, text, count, size, flags, data + * @details id: unique identifier for the widget + * @details title: title for the widget + * @details text: text for the widget + * @details min: minimum value for the widget + * @details max: maximum value for the widget + * @details value: value for the widget + * @details count: number of columns + * @details size: size of the widget + * @details flags: flags for the widget + * @details data: data to be binded using the data_binding map + */ +struct WidgetAttributes { + std::string id; + std::string title; // For Window, Button + double min; // For Slider + double max; // For Slider + double value; // For Slidecar + std::string text; // For Text, Button + int count = 0; // For Columns + ImVec2 size = ImVec2(0, 0); + void* flags = nullptr; + + void* data = nullptr; +}; + +/** + * @enum ActionType + */ +enum class ActionType { Click, Change, Run }; + +/** + * @struct Action + */ +struct Action { + ActionType type; + std::function callback; // Using std::function to hold lambda + // expressions or function pointers +}; + +/** + * @brief Tokenize a zeml string + */ +std::vector Tokenize(const std::string& input); + +/** + * @struct Node + * @brief Node for a zeml tree + */ +struct Node { + WidgetType type; + WidgetAttributes attributes; + std::vector actions; + std::vector children; + + Node* parent = nullptr; + + Node* GetNode(const std::string& searchId) { + if (attributes.id == searchId) { + return this; + } + for (Node& child : children) { + Node* found = child.GetNode(searchId); + if (found != nullptr) { + return found; + } + } + return nullptr; + } +}; + +/** + * @brief Bind a callback to a node + */ +void Bind(Node* node, std::function callback); + +/** + * @brief Bind an action to a node + */ +void BindAction(Node* node, ActionType type, std::function callback); + +/** + * @brief Map a string to a widget type + */ +WidgetType MapType(const std::string& type); + +/** + * @brief ParseNode attributes for a widget + */ +WidgetAttributes ParseAttributes( + const std::vector& tokens, size_t& index, const WidgetType& type, + const std::map& data_bindings = {}); + +/** + * @brief ParseNode a zeml node + */ +Node ParseNode(const std::vector& tokens, size_t& index, + const std::map& data_bindings = {}); + +/** + * @brief ParseNode a zeml string + */ +Node Parse(const std::string& yazon_input, + const std::map& data_bindings = {}); + +/** + * @brief Render a zeml tree + */ +void Render(Node& node); + +/** + * @brief Execute actions for a node + */ +void ExecuteActions(const std::vector& actions, ActionType type); + +} // namespace zeml +} // namespace gui +} // namespace app +} // namespace yaze + +#endif // YAZE_APP_GUI_YAZON_H_