From babf859fb594db03ac955b0d233ca6902042a1e1 Mon Sep 17 00:00:00 2001 From: GHOST11111100 Date: Wed, 30 Jul 2025 00:09:54 +0300 Subject: [PATCH 01/11] Working: Refactor GUI and Container classes; add styling support and element properties --- src/graphics/ui/GUI.cpp | 17 +- src/graphics/ui/elements/Container.cpp | 283 ++++++++++++++++++--- src/graphics/ui/elements/UINode.cpp | 16 ++ src/graphics/ui/elements/UINode.hpp | 12 +- src/graphics/ui/gui_xml.cpp | 5 + src/graphics/ui/style/StylesheetParser.cpp | 85 +++++++ src/graphics/ui/style/StylesheetParser.h | 25 ++ src/graphics/ui/style/value.cpp | 245 ++++++++++++++++++ src/graphics/ui/style/value.h | 103 ++++++++ 9 files changed, 749 insertions(+), 42 deletions(-) create mode 100644 src/graphics/ui/style/StylesheetParser.cpp create mode 100644 src/graphics/ui/style/StylesheetParser.h create mode 100644 src/graphics/ui/style/value.cpp create mode 100644 src/graphics/ui/style/value.h diff --git a/src/graphics/ui/GUI.cpp b/src/graphics/ui/GUI.cpp index ceb2f4a0d..e9b35a22a 100644 --- a/src/graphics/ui/GUI.cpp +++ b/src/graphics/ui/GUI.cpp @@ -13,18 +13,16 @@ #include "frontend/UiDocument.hpp" #include "frontend/locale.hpp" #include "graphics/core/Batch2D.hpp" -#include "graphics/core/LineBatch.hpp" -#include "graphics/core/Shader.hpp" -#include "graphics/core/Font.hpp" #include "graphics/core/DrawContext.hpp" +#include "graphics/core/Font.hpp" +#include "graphics/core/LineBatch.hpp" #include "graphics/core/Shader.hpp" #include "gui_util.hpp" #include "window/Camera.hpp" #include "window/Window.hpp" #include "window/input.hpp" -#include -#include +#include "style/StylesheetParser.h" using namespace gui; @@ -50,7 +48,8 @@ GUI::GUI(Engine& engine) tooltip = guiutil::create( *this, "" - "" + "" "" ); store("tooltip", tooltip); @@ -282,14 +281,16 @@ void GUI::draw(const DrawContext& pctx, const Assets& assets) { auto size = node->getSize(); batch2D->setColor(0, 255, 255); - batch2D->lineRect(parentPos.x+1, parentPos.y, size.x-2, size.y-1); + batch2D->lineRect( + parentPos.x + 1, parentPos.y, size.x - 2, size.y - 1 + ); node = node->getParent(); } // debug draw auto size = hover->getSize(); batch2D->setColor(0, 255, 0); - batch2D->lineRect(pos.x, pos.y, size.x-1, size.y-1); + batch2D->lineRect(pos.x, pos.y, size.x - 1, size.y - 1); } } diff --git a/src/graphics/ui/elements/Container.cpp b/src/graphics/ui/elements/Container.cpp index b930d76a0..7e85d9b88 100644 --- a/src/graphics/ui/elements/Container.cpp +++ b/src/graphics/ui/elements/Container.cpp @@ -1,11 +1,13 @@ #include "Container.hpp" -#include "graphics/core/DrawContext.hpp" -#include "graphics/core/Batch2D.hpp" - #include +#include #include +#include "../style/StylesheetParser.h" +#include "graphics/core/Batch2D.hpp" +#include "graphics/core/DrawContext.hpp" + using namespace gui; Container::Container(GUI& gui, glm::vec2 size) : UINode(gui, size) { @@ -24,15 +26,15 @@ std::shared_ptr Container::getAt(const glm::vec2& pos) { if (!isInside(pos)) { return nullptr; } - int diff = (actualLength-size.y); - if (scrollable && diff > 0 && pos.x > calcPos().x + getSize().x - scrollBarWidth) { + int diff = (actualLength - size.y); + if (scrollable && diff > 0 && + pos.x > calcPos().x + getSize().x - scrollBarWidth) { return UINode::getAt(pos); } - for (int i = nodes.size()-1; i >= 0; i--) { + for (int i = nodes.size() - 1; i >= 0; i--) { auto& node = nodes[i]; - if (!node->isVisible()) - continue; + if (!node->isVisible()) continue; auto hover = node->getAt(pos); if (hover != nullptr) { return hover; @@ -55,7 +57,7 @@ void Container::mouseMove(int x, int y) { } return; } - int diff = (actualLength-size.y); + int diff = (actualLength - size.y); if (diff > 0) { scroll -= (y - prevScrollY) / static_cast(size.y) * actualLength; scroll = -glm::min( @@ -87,23 +89,24 @@ void Container::act(float delta) { } } } - intervalEvents.erase(std::remove_if( - intervalEvents.begin(), intervalEvents.end(), - [](const IntervalEvent& event) { - return event.repeat == 0; - } - ), intervalEvents.end()); + intervalEvents.erase( + std::remove_if( + intervalEvents.begin(), + intervalEvents.end(), + [](const IntervalEvent& event) { return event.repeat == 0; } + ), + intervalEvents.end() + ); } void Container::scrolled(int value) { - int diff = (actualLength-getSize().y); + int diff = (actualLength - getSize().y); if (scroll < 0 && diff <= 0) { scroll = 0; } if (diff > 0 && scrollable) { scroll += value * scrollStep; - if (scroll > 0) - scroll = 0; + if (scroll > 0) scroll = 0; if (-scroll > diff) { scroll = -diff; } @@ -116,6 +119,190 @@ void Container::setScrollable(bool flag) { scrollable = flag; } +static std::string trim(const std::string& s) { + if (s.empty()) return s; + + size_t start = s.find_first_not_of(" \t\n\r"); + size_t end = s.find_last_not_of(" \t\n\r"); + + if (start == std::string::npos) return ""; + return s.substr(start, end - start + 1); +} + +std::unordered_map calculateStylesheet( + const UINode& node, + const std::vector& rules +) { + struct AppliedRule { + const StylesheetRule* rule; + int specificity; + size_t index; + }; + + std::vector appliedRules; + appliedRules.reserve(rules.size()); + + for (size_t i = 0; i < rules.size(); ++i) { + const auto& rule = rules[i]; + + // Разбиваем составные селекторы (button, .menu) + std::vector selectors; + size_t start = 0; + size_t comma; + do { + comma = rule.selector.find(',', start); + size_t end = (comma == std::string::npos) ? rule.selector.length() : comma; + std::string sel = trim(rule.selector.substr(start, end - start)); + if (!sel.empty()) { + selectors.push_back(sel); + } + start = comma + 1; + } while (comma != std::string::npos); + + bool selectorMatches = false; + + for (const auto& selector : selectors) { + // Проверяем, является ли селектор универсальным * + if (selector == "*") { + // Универсальный селектор применяется ко всем элементам + selectorMatches = true; + + // Специфичность для * = 0 + appliedRules.push_back({&rule, 0, i}); + break; // Другие селекторы в этом правиле не проверяем + } + + // Парсим обычный селектор + std::string tag, id, classname; + std::vector pseudoClasses; + + size_t pos = 0; + while (pos < selector.length()) { + if (std::isspace(selector[pos])) { + pos++; + continue; + } + + if (selector[pos] == '#') { + pos++; + size_t end = selector.find_first_of(".:#[] ", pos); + if (end == std::string::npos) end = selector.length(); + id = selector.substr(pos, end - pos); + pos = end; + } + else if (selector[pos] == '.') { + pos++; + size_t end = selector.find_first_of(".:#[] ", pos); + if (end == std::string::npos) end = selector.length(); + classname = selector.substr(pos, end - pos); + pos = end; + } + else if (selector[pos] == ':') { + pos++; + size_t end = selector.find_first_of(".:#[] ", pos); + if (end == std::string::npos) end = selector.length(); + pseudoClasses.push_back(selector.substr(pos, end - pos)); + pos = end; + } + else { + size_t end = selector.find_first_of(".:#[] ", pos); + if (end == std::string::npos) end = selector.length(); + tag = selector.substr(pos, end - pos); + pos = end; + } + } + + // Проверяем соответствие узлу + bool matches = true; + + // Проверяем тег (если указан и не *) + if (!tag.empty() && tag != "*" && node.getName() != tag) { + matches = false; + } + + // Проверяем ID (если указан) + if (!id.empty() && node.getId() != id) { + matches = false; + } + + // Проверяем класс (если указан) + if (!classname.empty() && node.getClassname() != classname) { + matches = false; + } + + // Проверяем все псевдоклассы + for (const auto& pseudo : pseudoClasses) { + if (pseudo == "hover" && !node.isHover()) { + matches = false; + break; + } + else if (pseudo == "active" && !node.isPressed()) { + matches = false; + break; + } + else if (pseudo == "focus" && !node.isFocused()) { + matches = false; + break; + } + else if (pseudo == "disabled" && node.isEnabled()) { + matches = false; + break; + } + } + + if (matches) { + selectorMatches = true; + + // Вычисляем специфичность селектора + int specificity = 0; + + if (!id.empty()) specificity += 100; + if (!classname.empty()) specificity += 10; + if (!tag.empty() && tag != "*") specificity += 1; + specificity += pseudoClasses.size() * 10; + + appliedRules.push_back({&rule, specificity, i}); + break; // Если один из составных селекторов подходит, остальные не проверяем + } + } + } + + // Сортируем правила по специфичности + std::sort(appliedRules.begin(), appliedRules.end(), + [](const AppliedRule& a, const AppliedRule& b) { + if (a.specificity != b.specificity) { + return a.specificity > b.specificity; + } + return a.index < b.index; // Ранние правила имеют приоритет при одинаковой специфичности + }); + + // Применяем стили + std::unordered_map styles; + + for (const auto& applied : appliedRules) { + for (const auto& [property, value] : applied.rule->declarations) { + // Более специфичные правила переопределяют менее специфичные + if (styles.find(property) == styles.end()) { + styles[property] = value; + } + } + } + + return styles; +} + +std::string stylesheet_src = R"( + button:hover { + color: #575757ff; + } + button { + color: #000000; + } + * { + color: #00ff00; + } +)"; + void Container::draw(const DrawContext& pctx, const Assets& assets) { glm::vec2 pos = calcPos(); glm::vec2 size = getSize(); @@ -126,21 +313,45 @@ void Container::draw(const DrawContext& pctx, const Assets& assets) { if (!nodes.empty()) { batch->flush(); DrawContext ctx = pctx.sub(); - ctx.setScissors(glm::vec4(pos.x, pos.y, glm::ceil(size.x), glm::ceil(size.y))); + ctx.setScissors( + glm::vec4(pos.x, pos.y, glm::ceil(size.x), glm::ceil(size.y)) + ); for (const auto& node : nodes) { - if (node->isVisible()) + if (node->isVisible()) { + if (node->getName() != "") { + std::vector styles = StylesheetParser::parse(stylesheet_src); + std::unordered_map node_styles = calculateStylesheet(*node, styles); + for (const auto& [property, value] : node_styles) { + switch (getStylePropertyType(property)) + { + case StyleProperty::COLOR: + node->setColor( value.asColor() ); + break; + case StyleProperty::MARGIN: + node->setMargin( value.asVec4() ); + break; + + default: + break; + } + } + } + node->draw(pctx, assets); + } } - int diff = (actualLength-size.y); + int diff = (actualLength - size.y); if (scrollable && diff > 0) { - int h = glm::max(size.y / actualLength * size.y, scrollBarWidth / 2.0f); + int h = + glm::max(size.y / actualLength * size.y, scrollBarWidth / 2.0f); batch->untexture(); batch->setColor(glm::vec4(1, 1, 1, 0.3f)); batch->rect( pos.x + size.x - scrollBarWidth, pos.y - scroll / static_cast(diff) * (size.y - h), - scrollBarWidth, h + scrollBarWidth, + h ); } batch->flush(); @@ -149,8 +360,7 @@ void Container::draw(const DrawContext& pctx, const Assets& assets) { void Container::drawBackground(const DrawContext& pctx, const Assets&) { glm::vec4 color = calcColor(); - if (color.a <= 0.001f) - return; + if (color.a <= 0.001f) return; glm::vec2 pos = calcPos(); auto batch = pctx.getBatch2D(); @@ -179,11 +389,16 @@ void Container::add(const std::shared_ptr& node, glm::vec2 pos) { void Container::remove(UINode* selected) { selected->setParent(nullptr); - nodes.erase(std::remove_if(nodes.begin(), nodes.end(), - [selected](const std::shared_ptr& node) { - return node.get() == selected; - } - ), nodes.end()); + nodes.erase( + std::remove_if( + nodes.begin(), + nodes.end(), + [selected](const std::shared_ptr& node) { + return node.get() == selected; + } + ), + nodes.end() + ); refresh(); } @@ -227,9 +442,11 @@ void Container::setScrollStep(int step) { } void Container::refresh() { - std::stable_sort(nodes.begin(), nodes.end(), [](const auto& a, const auto& b) { - return a->getZIndex() < b->getZIndex(); - }); + std::stable_sort( + nodes.begin(), nodes.end(), [](const auto& a, const auto& b) { + return a->getZIndex() < b->getZIndex(); + } + ); } void Container::setScroll(int scroll) { diff --git a/src/graphics/ui/elements/UINode.cpp b/src/graphics/ui/elements/UINode.cpp index 1e8f48d6e..1394248a3 100644 --- a/src/graphics/ui/elements/UINode.cpp +++ b/src/graphics/ui/elements/UINode.cpp @@ -421,3 +421,19 @@ std::shared_ptr UINode::find( } return nullptr; } + +void UINode::setClassname(std::string _classname) { + classname = _classname; +} + +std::string UINode::getClassname() const { + return classname; +} + +void UINode::setName(std::string _name) { + name = _name; +} + +std::string UINode::getName() const { + return name; +} \ No newline at end of file diff --git a/src/graphics/ui/elements/UINode.hpp b/src/graphics/ui/elements/UINode.hpp index fc64527fd..e226fe7a1 100644 --- a/src/graphics/ui/elements/UINode.hpp +++ b/src/graphics/ui/elements/UINode.hpp @@ -78,6 +78,10 @@ namespace gui { private: /// @brief element identifier used for direct access in UiDocument std::string id = ""; + /// @brief element name + std::string name = ""; + /// @brief element name + std::string classname = ""; /// @brief element enabled state bool enabled = true; protected: @@ -202,7 +206,13 @@ namespace gui { bool isPressed() const; void defocus(); - bool isFocused() const; + bool isFocused() const; + + void setClassname(std::string _classname); + std::string getClassname() const; + + void setName(std::string name); + std::string getName() const; /// @brief Check if element catches all user input when focused virtual bool isFocuskeeper() const {return false;} diff --git a/src/graphics/ui/gui_xml.cpp b/src/graphics/ui/gui_xml.cpp index 49adcc6aa..762ef9625 100644 --- a/src/graphics/ui/gui_xml.cpp +++ b/src/graphics/ui/gui_xml.cpp @@ -94,6 +94,9 @@ static void read_uinode( if (element.has("id")) { node.setId(element.attr("id").getText()); } + if (element.has("class")) { + node.setClassname(element.attr("class").getText()); + } if (element.has("pos")) { node.setPos(element.attr("pos").asVec2()); } @@ -193,6 +196,8 @@ static void read_uinode( if (auto ondoubleclick = create_action(reader, element, "ondoubleclick")) { node.listenDoubleClick(ondoubleclick); } + + node.setName(element.getTag()); } static void read_container_impl( diff --git a/src/graphics/ui/style/StylesheetParser.cpp b/src/graphics/ui/style/StylesheetParser.cpp new file mode 100644 index 000000000..20505f99b --- /dev/null +++ b/src/graphics/ui/style/StylesheetParser.cpp @@ -0,0 +1,85 @@ +#include "StylesheetParser.h" +#include +#include +#include + +// Утилита: trim +static std::string trim(const std::string& s) { + size_t start = s.find_first_not_of(" \t\n\r"); + size_t end = s.find_last_not_of(" \t\n\r"); + return (start == std::string::npos) ? "" : s.substr(start, end - start + 1); +} + +// Разделение составных селекторов: `button, .menu` +static std::vector splitSelectors(const std::string& sel) { + std::vector result; + std::stringstream ss(sel); + std::string part; + while (std::getline(ss, part, ',')) { + auto t = trim(part); + if (!t.empty()) result.push_back(t); + } + return result; +} + +StyleProperty getStylePropertyType(const std::string& property) { + if (property == "color") return StyleProperty::COLOR; + if (property == "margin") return StyleProperty::MARGIN; + if (property == "z-index") return StyleProperty::Z_INDEX; + return StyleProperty::UNKNOWN; +} + +std::vector StylesheetParser::parse(const std::string& source) { + std::vector rules; + size_t pos = 0; + const size_t len = source.length(); + + while (pos < len) { + if (std::isspace(source[pos])) { + ++pos; + continue; + } + if (pos + 1 < len && source.substr(pos, 2) == "/*") { + auto end = source.find("*/", pos); + if (end == std::string::npos) break; + pos = end + 2; + continue; + } + + auto open_brace = source.find('{', pos); + if (open_brace == std::string::npos) break; + + auto selector_str = trim(source.substr(pos, open_brace - pos)); + pos = open_brace + 1; + + auto close_brace = source.find('}', pos); + if (close_brace == std::string::npos) break; + + auto body = source.substr(pos, close_brace - pos); + pos = close_brace + 1; + + std::unordered_map decl; + std::stringstream sb(body); + std::string line; + while (std::getline(sb, line, ';')) { + auto colon = line.find(':'); + if (colon != std::string::npos) { + std::string prop = trim(line.substr(0, colon)); + std::string value_str = trim(line.substr(colon + 1)); + if (!prop.empty() && !value_str.empty()) { + // Автоматическое определение типа + style::value parsed_value = value_str; + + decl[prop] = parsed_value; + } + } + } + + auto selectors = splitSelectors(selector_str); + for (const auto& sel : selectors) { + rules.push_back({ sel, decl }); + } + } + + return rules; +} diff --git a/src/graphics/ui/style/StylesheetParser.h b/src/graphics/ui/style/StylesheetParser.h new file mode 100644 index 000000000..58dd10f5d --- /dev/null +++ b/src/graphics/ui/style/StylesheetParser.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#include +#include +#include "value.h" + +struct StylesheetRule { + std::string selector; + std::unordered_map declarations; +}; + +enum class StyleProperty { + COLOR, + MARGIN, + Z_INDEX, + UNKNOWN +}; + +StyleProperty getStylePropertyType(const std::string& property); + +class StylesheetParser { +public: + static std::vector parse(const std::string& source); +}; \ No newline at end of file diff --git a/src/graphics/ui/style/value.cpp b/src/graphics/ui/style/value.cpp new file mode 100644 index 000000000..2c89cb3f3 --- /dev/null +++ b/src/graphics/ui/style/value.cpp @@ -0,0 +1,245 @@ +#include "value.h" +#include +#include + +namespace style { + +// Убираем пробелы +static std::string trim(const std::string& s) { + auto wsfront = std::find_if(s.begin(), s.end(), [](int c){return !std::isspace(c);}); + auto wsback = std::find_if(s.rbegin(), s.rend(), [](int c){return !std::isspace(c);}).base(); + return (wsback <= wsfront ? std::string() : std::string(wsfront, wsback)); +} + +// Проверка, что строка — число +static bool is_number(const std::string& s) { + if (s.empty()) return false; + size_t i = s[0] == '-' ? 1 : 0; + return i < s.size() && std::all_of(s.begin() + i, s.end(), ::isdigit); +} + +// Парсинг цвета: #RGB, #RGBA, #RRGGBB, #RRGGBBAA, rgba() +static std::optional parse_color(const std::string& str) { + auto s = trim(str); + if (s.empty()) return std::nullopt; + + if (s[0] == '#') { + std::string hex = s.substr(1); + if (hex.length() == 3) { + int r = (hex[0] - '0') * 16 + (hex[0] - '0'); + int g = (hex[1] - '0') * 16 + (hex[1] - '0'); + int b = (hex[2] - '0') * 16 + (hex[2] - '0'); + return glm::vec4(r/255.0f, g/255.0f, b/255.0f, 1.0f); + } else if (hex.length() == 4) { + int r = (hex[0] - '0') * 16 + (hex[0] - '0'); + int g = (hex[1] - '0') * 16 + (hex[1] - '0'); + int b = (hex[2] - '0') * 16 + (hex[2] - '0'); + int a = (hex[3] - '0') * 16 + (hex[3] - '0'); + return glm::vec4(r/255.0f, g/255.0f, b/255.0f, a/255.0f); + } else if (hex.length() == 6) { + int r = std::stoi(hex.substr(0, 2), nullptr, 16); + int g = std::stoi(hex.substr(2, 2), nullptr, 16); + int b = std::stoi(hex.substr(4, 2), nullptr, 16); + return glm::vec4(r/255.0f, g/255.0f, b/255.0f, 1.0f); + } else if (hex.length() == 8) { + int r = std::stoi(hex.substr(0, 2), nullptr, 16); + int g = std::stoi(hex.substr(2, 2), nullptr, 16); + int b = std::stoi(hex.substr(4, 2), nullptr, 16); + int a = std::stoi(hex.substr(6, 2), nullptr, 16); + return glm::vec4(r/255.0f, g/255.0f, b/255.0f, a/255.0f); + } + } + + if (s.rfind("rgba(", 0) == 0) { + float r, g, b, a; + if (std::sscanf(s.c_str(), "rgba(%f,%f,%f,%f)", &r, &g, &b, &a) == 4) { + return glm::vec4(r/255.0f, g/255.0f, b/255.0f, a); + } + } + + // Именованные цвета (минимально) + if (s == "red") return glm::vec4(1,0,0,1); + if (s == "green") return glm::vec4(0,1,0,1); + if (s == "blue") return glm::vec4(0,0,1,1); + if (s == "white") return glm::vec4(1,1,1,1); + if (s == "black") return glm::vec4(0,0,0,1); + if (s == "transparent") return glm::vec4(0,0,0,0); + + return std::nullopt; +} + +// === Реализация методов === + +int64_t style::value::asInt(int64_t def) const { + if (std::holds_alternative(data)) { + return std::get(data); + } + if (std::holds_alternative(data)) { + return static_cast(std::get(data)); + } + if (std::holds_alternative(data)) { + return std::get(data) ? 1 : 0; + } + if (std::holds_alternative(data)) { + if (auto res = tryParse(std::get(data))) { + return *res; + } + } + return def; +} + +double style::value::asFloat(double def) const { + if (std::holds_alternative(data)) { + return std::get(data); + } + if (std::holds_alternative(data)) { + return static_cast(std::get(data)); + } + if (std::holds_alternative(data)) { + return std::get(data) ? 1.0 : 0.0; + } + if (std::holds_alternative(data)) { + if (auto res = tryParse(std::get(data))) { + return *res; + } + } + return def; +} + +float style::value::asFloat(float def) const { + return static_cast(asFloat(static_cast(def))); +} + +bool style::value::asBool(bool def) const { + if (std::holds_alternative(data)) { + return std::get(data); + } + if (std::holds_alternative(data)) { + return std::get(data) != 0; + } + if (std::holds_alternative(data)) { + return std::get(data) != 0.0; + } + if (std::holds_alternative(data)) { + auto& s = std::get(data); + return s == "true" || s == "1" || !s.empty(); + } + return def; +} + +std::string style::value::asString(const std::string& def) const { + if (std::holds_alternative(data)) { + return std::get(data); + } + if (std::holds_alternative(data)) { + return std::to_string(std::get(data)); + } + if (std::holds_alternative(data)) { + return std::to_string(std::get(data)); + } + if (std::holds_alternative(data)) { + return std::get(data) ? "true" : "false"; + } + if (std::holds_alternative(data)) { + auto v = std::get(data); + return "vec4(" + std::to_string(v.x) + "," + std::to_string(v.y) + "," + std::to_string(v.z) + "," + std::to_string(v.w) + ")"; + } + if (std::holds_alternative(data)) { + auto v = std::get(data); + return "vec3(" + std::to_string(v.x) + "," + std::to_string(v.y) + "," + std::to_string(v.z) + ")"; + } + if (std::holds_alternative(data)) { + auto v = std::get(data); + return "vec2(" + std::to_string(v.x) + "," + std::to_string(v.y) + ")"; + } + return def; +} + +glm::vec2 style::value::asVec2(const glm::vec2& def) const { + if (std::holds_alternative(data)) { + return std::get(data); + } + if (std::holds_alternative(data)) { + std::string s = std::get(data); + float x, y; + if (std::sscanf(s.c_str(), "%f,%f", &x, &y) == 2) { + return glm::vec2(x, y); + } + } + return def; +} + +glm::vec3 style::value::asVec3(const glm::vec3& def) const { + if (std::holds_alternative(data)) { + return std::get(data); + } + if (std::holds_alternative(data)) { + std::string s = std::get(data); + float x, y, z; + if (std::sscanf(s.c_str(), "%f,%f,%f", &x, &y, &z) == 3) { + return glm::vec3(x, y, z); + } + } + return def; +} + +glm::vec4 style::value::asVec4(const glm::vec4& def) const { + if (std::holds_alternative(data)) { + return std::get(data); + } + if (std::holds_alternative(data)) { + std::string s = std::get(data); + float x, y, z, w; + if (std::sscanf(s.c_str(), "%f,%f,%f,%f", &x, &y, &z, &w) == 4) { + return glm::vec4(x, y, z, w); + } + } + return def; +} + +glm::vec4 style::value::asColor(const glm::vec4& def) const { + if (std::holds_alternative(data)) { + auto v = std::get(data); + return glm::vec4(v.x/255.0f, v.y/255.0f, v.z/255.0f, v.w/255.0f); + } + if (std::holds_alternative(data)) { + if (auto col = parse_color(std::get(data))) { + return *col; + } + } + return def; +} + +std::string style::value::type_name() const { + if (std::holds_alternative(data)) return "null"; + if (std::holds_alternative(data)) return "string"; + if (std::holds_alternative(data)) return "int"; + if (std::holds_alternative(data)) return "float"; + if (std::holds_alternative(data)) return "bool"; + if (std::holds_alternative(data)) return "vec2"; + if (std::holds_alternative(data)) return "vec3"; + if (std::holds_alternative(data)) return "vec4"; + return "unknown"; +} + +std::string style::value::toString() const { + if (std::holds_alternative(data)) return std::get(data); + if (std::holds_alternative(data)) return std::to_string(std::get(data)); + if (std::holds_alternative(data)) return std::to_string(std::get(data)); + if (std::holds_alternative(data)) return std::get(data) ? "true" : "false"; + if (std::holds_alternative(data)) { + auto v = std::get(data); + return std::to_string(v.x) + "," + std::to_string(v.y); + } + if (std::holds_alternative(data)) { + auto v = std::get(data); + return std::to_string(v.x) + "," + std::to_string(v.y) + "," + std::to_string(v.z); + } + if (std::holds_alternative(data)) { + auto v = std::get(data); + return std::to_string(v.x) + "," + std::to_string(v.y) + "," + std::to_string(v.z) + "," + std::to_string(v.w); + } + return "null"; +} + +} \ No newline at end of file diff --git a/src/graphics/ui/style/value.h b/src/graphics/ui/style/value.h new file mode 100644 index 000000000..68b28fd70 --- /dev/null +++ b/src/graphics/ui/style/value.h @@ -0,0 +1,103 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace style { + +/** + * Универсальный тип для хранения динамических значений. + * Поддерживает: строки, числа, bool, vec2/3/4, цвета. + */ +class value { +private: + std::variant< + std::monostate, // null + std::string, + int64_t, + double, + bool, + glm::vec2, + glm::vec3, + glm::vec4 + > data; + + // Вспомогательная функция для парсинга строки в число + template + static std::optional tryParse(const std::string& str); + +public: + // Конструкторы + value() = default; + value(std::monostate) {} + value(const char* v) : data(std::string(v)) {} + value(std::string v) : data(std::move(v)) {} + value(int v) : data(static_cast(v)) {} + value(unsigned int v) : data(static_cast(v)) {} + value(int64_t v) : data(v) {} + value(double v) : data(v) {} + value(float v) : data(static_cast(v)) {} + value(bool v) : data(v) {} + value(glm::vec2 v) : data(v) {} + value(glm::vec3 v) : data(v) {} + value(glm::vec4 v) : data(v) {} + + // Явные преобразования + bool isNull() const { return std::holds_alternative(data); } + bool isString() const { return std::holds_alternative(data); } + bool isNumber() const { return isInteger() || isFloat(); } + bool isInteger() const { return std::holds_alternative(data); } + bool isFloat() const { return std::holds_alternative(data); } + bool isBool() const { return std::holds_alternative(data); } + bool isVec2() const { return std::holds_alternative(data); } + bool isVec3() const { return std::holds_alternative(data); } + bool isVec4() const { return std::holds_alternative(data); } + + // Получение значений с преобразованием + int64_t asInt(int64_t def = 0) const; + double asFloat(double def = 0.0) const; + float asFloat(float def = 0.0f) const; + bool asBool(bool def = false) const; + std::string asString(const std::string& def = "") const; + + glm::vec2 asVec2(const glm::vec2& def = {}) const; + glm::vec3 asVec3(const glm::vec3& def = {}) const; + glm::vec4 asVec4(const glm::vec4& def = {}) const; + + // Специальный: цвет (поддерживает #RGB, #RGBA, #RRGGBB, rgba()) + glm::vec4 asColor(const glm::vec4& def = {0,0,0,0}) const; + + // Для отладки + std::string type_name() const; + std::string toString() const; +}; + +// Реализация шаблонов +template +std::optional value::tryParse(const std::string& str) { + if (str.empty()) return std::nullopt; + try { + size_t pos; + if constexpr (std::is_same_v) { + int64_t val = std::stoll(str, &pos); + return pos == str.length() ? std::make_optional(val) : std::nullopt; + } else if constexpr (std::is_same_v) { + double val = std::stod(str, &pos); + return pos == str.length() ? std::make_optional(val) : std::nullopt; + } else if constexpr (std::is_same_v) { + if (str == "true" || str == "1") return true; + if (str == "false" || str == "0") return false; + return std::nullopt; + } + } catch (...) { + return std::nullopt; + } + return std::nullopt; +} + +} \ No newline at end of file From 25616d597478daa49e4072ae27a416ba948c3bb1 Mon Sep 17 00:00:00 2001 From: GHOST11111100 Date: Wed, 30 Jul 2025 01:59:47 +0300 Subject: [PATCH 02/11] Refactor stylesheet handling: consolidate stylesheet parsing and apply styles to UINodes --- src/graphics/ui/GUI.cpp | 2 - src/graphics/ui/elements/Container.cpp | 211 +----------------- src/graphics/ui/elements/UINode.cpp | 173 ++++++++++++++ src/graphics/ui/elements/UINode.hpp | 3 + src/graphics/ui/gui_xml.cpp | 73 ++---- src/graphics/ui/style/Stylesheet.cpp | 12 + .../{StylesheetParser.h => Stylesheet.hpp} | 17 +- src/graphics/ui/style/StylesheetParser.cpp | 11 +- src/graphics/ui/style/StylesheetParser.hpp | 21 ++ 9 files changed, 247 insertions(+), 276 deletions(-) create mode 100644 src/graphics/ui/style/Stylesheet.cpp rename src/graphics/ui/style/{StylesheetParser.h => Stylesheet.hpp} (56%) create mode 100644 src/graphics/ui/style/StylesheetParser.hpp diff --git a/src/graphics/ui/GUI.cpp b/src/graphics/ui/GUI.cpp index e9b35a22a..ecf558727 100644 --- a/src/graphics/ui/GUI.cpp +++ b/src/graphics/ui/GUI.cpp @@ -22,8 +22,6 @@ #include "window/Window.hpp" #include "window/input.hpp" -#include "style/StylesheetParser.h" - using namespace gui; GUI::GUI(Engine& engine) diff --git a/src/graphics/ui/elements/Container.cpp b/src/graphics/ui/elements/Container.cpp index 7e85d9b88..9c7006152 100644 --- a/src/graphics/ui/elements/Container.cpp +++ b/src/graphics/ui/elements/Container.cpp @@ -4,7 +4,6 @@ #include #include -#include "../style/StylesheetParser.h" #include "graphics/core/Batch2D.hpp" #include "graphics/core/DrawContext.hpp" @@ -119,189 +118,19 @@ void Container::setScrollable(bool flag) { scrollable = flag; } -static std::string trim(const std::string& s) { - if (s.empty()) return s; - - size_t start = s.find_first_not_of(" \t\n\r"); - size_t end = s.find_last_not_of(" \t\n\r"); - - if (start == std::string::npos) return ""; - return s.substr(start, end - start + 1); -} - -std::unordered_map calculateStylesheet( - const UINode& node, - const std::vector& rules -) { - struct AppliedRule { - const StylesheetRule* rule; - int specificity; - size_t index; - }; - - std::vector appliedRules; - appliedRules.reserve(rules.size()); - - for (size_t i = 0; i < rules.size(); ++i) { - const auto& rule = rules[i]; - - // Разбиваем составные селекторы (button, .menu) - std::vector selectors; - size_t start = 0; - size_t comma; - do { - comma = rule.selector.find(',', start); - size_t end = (comma == std::string::npos) ? rule.selector.length() : comma; - std::string sel = trim(rule.selector.substr(start, end - start)); - if (!sel.empty()) { - selectors.push_back(sel); - } - start = comma + 1; - } while (comma != std::string::npos); - - bool selectorMatches = false; - - for (const auto& selector : selectors) { - // Проверяем, является ли селектор универсальным * - if (selector == "*") { - // Универсальный селектор применяется ко всем элементам - selectorMatches = true; - - // Специфичность для * = 0 - appliedRules.push_back({&rule, 0, i}); - break; // Другие селекторы в этом правиле не проверяем - } - - // Парсим обычный селектор - std::string tag, id, classname; - std::vector pseudoClasses; - - size_t pos = 0; - while (pos < selector.length()) { - if (std::isspace(selector[pos])) { - pos++; - continue; - } - - if (selector[pos] == '#') { - pos++; - size_t end = selector.find_first_of(".:#[] ", pos); - if (end == std::string::npos) end = selector.length(); - id = selector.substr(pos, end - pos); - pos = end; - } - else if (selector[pos] == '.') { - pos++; - size_t end = selector.find_first_of(".:#[] ", pos); - if (end == std::string::npos) end = selector.length(); - classname = selector.substr(pos, end - pos); - pos = end; - } - else if (selector[pos] == ':') { - pos++; - size_t end = selector.find_first_of(".:#[] ", pos); - if (end == std::string::npos) end = selector.length(); - pseudoClasses.push_back(selector.substr(pos, end - pos)); - pos = end; - } - else { - size_t end = selector.find_first_of(".:#[] ", pos); - if (end == std::string::npos) end = selector.length(); - tag = selector.substr(pos, end - pos); - pos = end; - } - } - - // Проверяем соответствие узлу - bool matches = true; - - // Проверяем тег (если указан и не *) - if (!tag.empty() && tag != "*" && node.getName() != tag) { - matches = false; - } - - // Проверяем ID (если указан) - if (!id.empty() && node.getId() != id) { - matches = false; - } - - // Проверяем класс (если указан) - if (!classname.empty() && node.getClassname() != classname) { - matches = false; - } - - // Проверяем все псевдоклассы - for (const auto& pseudo : pseudoClasses) { - if (pseudo == "hover" && !node.isHover()) { - matches = false; - break; - } - else if (pseudo == "active" && !node.isPressed()) { - matches = false; - break; - } - else if (pseudo == "focus" && !node.isFocused()) { - matches = false; - break; - } - else if (pseudo == "disabled" && node.isEnabled()) { - matches = false; - break; - } - } - - if (matches) { - selectorMatches = true; - - // Вычисляем специфичность селектора - int specificity = 0; - - if (!id.empty()) specificity += 100; - if (!classname.empty()) specificity += 10; - if (!tag.empty() && tag != "*") specificity += 1; - specificity += pseudoClasses.size() * 10; - - appliedRules.push_back({&rule, specificity, i}); - break; // Если один из составных селекторов подходит, остальные не проверяем - } +std::string stylesheet = R"( + label { + color: #ff0000; } - } - - // Сортируем правила по специфичности - std::sort(appliedRules.begin(), appliedRules.end(), - [](const AppliedRule& a, const AppliedRule& b) { - if (a.specificity != b.specificity) { - return a.specificity > b.specificity; - } - return a.index < b.index; // Ранние правила имеют приоритет при одинаковой специфичности - }); - - // Применяем стили - std::unordered_map styles; - - for (const auto& applied : appliedRules) { - for (const auto& [property, value] : applied.rule->declarations) { - // Более специфичные правила переопределяют менее специфичные - if (styles.find(property) == styles.end()) { - styles[property] = value; - } + button:hover { + color: #22222288; } - } - - return styles; -} + button { + color: #11111188; + } + )"; -std::string stylesheet_src = R"( - button:hover { - color: #575757ff; - } - button { - color: #000000; - } - * { - color: #00ff00; - } -)"; +std::vector rules = StylesheetParser::parse(stylesheet); void Container::draw(const DrawContext& pctx, const Assets& assets) { glm::vec2 pos = calcPos(); @@ -318,25 +147,7 @@ void Container::draw(const DrawContext& pctx, const Assets& assets) { ); for (const auto& node : nodes) { if (node->isVisible()) { - if (node->getName() != "") { - std::vector styles = StylesheetParser::parse(stylesheet_src); - std::unordered_map node_styles = calculateStylesheet(*node, styles); - for (const auto& [property, value] : node_styles) { - switch (getStylePropertyType(property)) - { - case StyleProperty::COLOR: - node->setColor( value.asColor() ); - break; - case StyleProperty::MARGIN: - node->setMargin( value.asVec4() ); - break; - - default: - break; - } - } - } - + node->applyStylesheet(rules); node->draw(pctx, assets); } } diff --git a/src/graphics/ui/elements/UINode.cpp b/src/graphics/ui/elements/UINode.cpp index 1394248a3..a417b3ab4 100644 --- a/src/graphics/ui/elements/UINode.cpp +++ b/src/graphics/ui/elements/UINode.cpp @@ -436,4 +436,177 @@ void UINode::setName(std::string _name) { std::string UINode::getName() const { return name; +} + +void UINode::applyStylesheet(const std::vector& rules) { + std::unordered_map styles = calculateStylesheet(rules); + for (const auto& [property, value] : styles) { + switch (getStylePropertyType(property)) + { + case StyleProperty::COLOR: + setColor( value.asColor() ); + break; + case StyleProperty::MARGIN: + setMargin( value.asVec4() ); + break; + case StyleProperty::Z_INDEX: + setZIndex( value.asInt() ); + break; + default: + break; + } + } +} + +std::unordered_map UINode::calculateStylesheet(const std::vector& rules) { + std::vector appliedRules; + appliedRules.reserve(rules.size()); + + for (size_t i = 0; i < rules.size(); ++i) { + const auto& rule = rules[i]; + + // Разбиваем составные селекторы (button, .menu) + std::vector selectors; + size_t start = 0; + size_t comma; + do { + comma = rule.selector.find(',', start); + size_t end = (comma == std::string::npos) ? rule.selector.length() : comma; + std::string sel = trim(rule.selector.substr(start, end - start)); + if (!sel.empty()) { + selectors.push_back(sel); + } + start = comma + 1; + } while (comma != std::string::npos); + + bool selectorMatches = false; + + for (const auto& selector : selectors) { + // Проверяем, является ли селектор универсальным * + if (selector == "*") { + // Универсальный селектор применяется ко всем элементам + selectorMatches = true; + + // Специфичность для * = 0 + appliedRules.push_back({&rule, 0, i}); + break; // Другие селекторы в этом правиле не проверяем + } + + // Парсим обычный селектор + std::string tag, id, classname; + std::vector pseudoClasses; + + size_t pos = 0; + while (pos < selector.length()) { + if (std::isspace(selector[pos])) { + pos++; + continue; + } + + if (selector[pos] == '#') { + pos++; + size_t end = selector.find_first_of(".:#[] ", pos); + if (end == std::string::npos) end = selector.length(); + id = selector.substr(pos, end - pos); + pos = end; + } + else if (selector[pos] == '.') { + pos++; + size_t end = selector.find_first_of(".:#[] ", pos); + if (end == std::string::npos) end = selector.length(); + classname = selector.substr(pos, end - pos); + pos = end; + } + else if (selector[pos] == ':') { + pos++; + size_t end = selector.find_first_of(".:#[] ", pos); + if (end == std::string::npos) end = selector.length(); + pseudoClasses.push_back(selector.substr(pos, end - pos)); + pos = end; + } + else { + size_t end = selector.find_first_of(".:#[] ", pos); + if (end == std::string::npos) end = selector.length(); + tag = selector.substr(pos, end - pos); + pos = end; + } + } + + // Проверяем соответствие узлу + bool matches = true; + + // Проверяем тег (если указан и не *) + if (!tag.empty() && tag != "*" && this->getName() != tag) { + matches = false; + } + + // Проверяем ID (если указан) + if (!id.empty() && this->getId() != id) { + matches = false; + } + + // Проверяем класс (если указан) + if (!classname.empty() && this->getClassname() != classname) { + matches = false; + } + + // Проверяем все псевдоклассы + for (const auto& pseudo : pseudoClasses) { + if (pseudo == "hover" && !this->isHover()) { + matches = false; + break; + } + else if (pseudo == "active" && !this->isPressed()) { + matches = false; + break; + } + else if (pseudo == "focus" && !this->isFocused()) { + matches = false; + break; + } + else if (pseudo == "disabled" && this->isEnabled()) { + matches = false; + break; + } + } + + if (matches) { + selectorMatches = true; + + // Вычисляем специфичность селектора + int specificity = 0; + + if (!id.empty()) specificity += 100; + if (!classname.empty()) specificity += 10; + if (!tag.empty() && tag != "*") specificity += 1; + specificity += pseudoClasses.size() * 10; + + appliedRules.push_back({&rule, specificity, i}); + break; // Если один из составных селекторов подходит, остальные не проверяем + } + } + } + + // Сортируем правила по специфичности + std::sort(appliedRules.begin(), appliedRules.end(), + [](const AppliedRule& a, const AppliedRule& b) { + if (a.specificity != b.specificity) { + return a.specificity > b.specificity; + } + return a.index < b.index; // Ранние правила имеют приоритет при одинаковой специфичности + }); + + // Применяем стили + std::unordered_map styles; + + for (const auto& applied : appliedRules) { + for (const auto& [property, value] : applied.rule->declarations) { + // Более специфичные правила переопределяют менее специфичные + if (styles.find(property) == styles.end()) { + styles[property] = value; + } + } + } + + return styles; } \ No newline at end of file diff --git a/src/graphics/ui/elements/UINode.hpp b/src/graphics/ui/elements/UINode.hpp index e226fe7a1..24508e51e 100644 --- a/src/graphics/ui/elements/UINode.hpp +++ b/src/graphics/ui/elements/UINode.hpp @@ -3,6 +3,7 @@ #include "delegates.hpp" #include "graphics/core/commons.hpp" #include "window/input.hpp" +#include "graphics/ui/style/StylesheetParser.hpp" #include #include @@ -303,5 +304,7 @@ namespace gui { void setMustRefresh() { mustRefresh = true; } + void UINode::applyStylesheet(const std::vector& rules); + std::unordered_map calculateStylesheet(const std::vector& rules); }; } diff --git a/src/graphics/ui/gui_xml.cpp b/src/graphics/ui/gui_xml.cpp index 762ef9625..807d4b944 100644 --- a/src/graphics/ui/gui_xml.cpp +++ b/src/graphics/ui/gui_xml.cpp @@ -91,50 +91,28 @@ static onaction create_action( static void read_uinode( const UiXmlReader& reader, const xml::xmlelement& element, UINode& node ) { - if (element.has("id")) { - node.setId(element.attr("id").getText()); - } - if (element.has("class")) { - node.setClassname(element.attr("class").getText()); - } - if (element.has("pos")) { - node.setPos(element.attr("pos").asVec2()); - } - if (element.has("min-size")) { - node.setMinSize(element.attr("min-size").asVec2()); - } - if (element.has("size")) { - node.setSize(element.attr("size").asVec2()); - } + node.setName(element.getTag()); + + if (element.has("id")) node.setId(element.attr("id").getText()); + if (element.has("class")) node.setClassname(element.attr("class").getText()); + if (element.has("pos")) node.setPos(element.attr("pos").asVec2()); + if (element.has("min-size")) node.setMinSize(element.attr("min-size").asVec2()); + if (element.has("size")) node.setSize(element.attr("size").asVec2()); + if (element.has("color")) { glm::vec4 color = element.attr("color").asColor(); - glm::vec4 hoverColor = color; - glm::vec4 pressedColor = color; - if (element.has("hover-color")) { - hoverColor = node.getHoverColor(); - } - if (element.has("pressed-color")) { - pressedColor = node.getPressedColor(); - } + node.setColor(color); - node.setHoverColor(hoverColor); - node.setPressedColor(pressedColor); - } - if (element.has("margin")) { - node.setMargin(element.attr("margin").asVec4()); - } - if (element.has("z-index")) { - node.setZIndex(element.attr("z-index").asInt()); - } - if (element.has("interactive")) { - node.setInteractive(element.attr("interactive").asBool()); - } - if (element.has("visible")) { - node.setVisible(element.attr("visible").asBool()); - } - if (element.has("enabled")) { - node.setEnabled(element.attr("enabled").asBool()); + node.setHoverColor(color); + node.setPressedColor(color); } + + if (element.has("margin")) node.setMargin(element.attr("margin").asVec4()); + if (element.has("z-index")) node.setZIndex(element.attr("z-index").asInt()); + if (element.has("interactive")) node.setInteractive(element.attr("interactive").asBool()); + if (element.has("visible")) node.setVisible(element.attr("visible").asBool()); + if (element.has("enabled")) node.setEnabled(element.attr("enabled").asBool()); + if (element.has("position-func")) { node.setPositionFunc(scripting::create_vec2_supplier( reader.getEnvironment(), @@ -149,18 +127,13 @@ static void read_uinode( reader.getFilename() )); } - if (element.has("hover-color")) { - node.setHoverColor(element.attr("hover-color").asColor()); - } - if (element.has("pressed-color")) { - node.setPressedColor(element.attr("pressed-color").asColor()); - } + if (element.has("hover-color")) node.setHoverColor(element.attr("hover-color").asColor()); + if (element.has("pressed-color")) node.setPressedColor(element.attr("pressed-color").asColor()); + const auto& alignName = element.attr("align", "").getText(); node.setAlign(align_from_string(alignName, node.getAlign())); - if (element.has("gravity")) { - node.setGravity(gravity_from_string(element.attr("gravity").getText())); - } + if (element.has("gravity")) node.setGravity(gravity_from_string(element.attr("gravity").getText())); if (element.has("tooltip")) { auto tooltip = util::str2wstr_utf8(element.attr("tooltip").getText()); @@ -196,8 +169,6 @@ static void read_uinode( if (auto ondoubleclick = create_action(reader, element, "ondoubleclick")) { node.listenDoubleClick(ondoubleclick); } - - node.setName(element.getTag()); } static void read_container_impl( diff --git a/src/graphics/ui/style/Stylesheet.cpp b/src/graphics/ui/style/Stylesheet.cpp new file mode 100644 index 000000000..b4cf8aa52 --- /dev/null +++ b/src/graphics/ui/style/Stylesheet.cpp @@ -0,0 +1,12 @@ +#include +#include +#include + +#include "Stylesheet.hpp" + +StyleProperty getStylePropertyType(const std::string& property) { + if (property == "color") return StyleProperty::COLOR; + if (property == "margin") return StyleProperty::MARGIN; + if (property == "z-index") return StyleProperty::Z_INDEX; + return StyleProperty::UNKNOWN; +} \ No newline at end of file diff --git a/src/graphics/ui/style/StylesheetParser.h b/src/graphics/ui/style/Stylesheet.hpp similarity index 56% rename from src/graphics/ui/style/StylesheetParser.h rename to src/graphics/ui/style/Stylesheet.hpp index 58dd10f5d..604e4776b 100644 --- a/src/graphics/ui/style/StylesheetParser.h +++ b/src/graphics/ui/style/Stylesheet.hpp @@ -1,8 +1,7 @@ -#pragma once - #include #include #include + #include "value.h" struct StylesheetRule { @@ -10,16 +9,6 @@ struct StylesheetRule { std::unordered_map declarations; }; -enum class StyleProperty { - COLOR, - MARGIN, - Z_INDEX, - UNKNOWN -}; - -StyleProperty getStylePropertyType(const std::string& property); +enum class StyleProperty { COLOR, MARGIN, Z_INDEX, UNKNOWN }; -class StylesheetParser { -public: - static std::vector parse(const std::string& source); -}; \ No newline at end of file +StyleProperty getStylePropertyType(const std::string& property); \ No newline at end of file diff --git a/src/graphics/ui/style/StylesheetParser.cpp b/src/graphics/ui/style/StylesheetParser.cpp index 20505f99b..dd4df5221 100644 --- a/src/graphics/ui/style/StylesheetParser.cpp +++ b/src/graphics/ui/style/StylesheetParser.cpp @@ -1,10 +1,10 @@ -#include "StylesheetParser.h" +#include "StylesheetParser.hpp" #include #include #include // Утилита: trim -static std::string trim(const std::string& s) { +std::string trim(const std::string& s) { size_t start = s.find_first_not_of(" \t\n\r"); size_t end = s.find_last_not_of(" \t\n\r"); return (start == std::string::npos) ? "" : s.substr(start, end - start + 1); @@ -22,13 +22,6 @@ static std::vector splitSelectors(const std::string& sel) { return result; } -StyleProperty getStylePropertyType(const std::string& property) { - if (property == "color") return StyleProperty::COLOR; - if (property == "margin") return StyleProperty::MARGIN; - if (property == "z-index") return StyleProperty::Z_INDEX; - return StyleProperty::UNKNOWN; -} - std::vector StylesheetParser::parse(const std::string& source) { std::vector rules; size_t pos = 0; diff --git a/src/graphics/ui/style/StylesheetParser.hpp b/src/graphics/ui/style/StylesheetParser.hpp new file mode 100644 index 000000000..66d6aa03d --- /dev/null +++ b/src/graphics/ui/style/StylesheetParser.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include +#include +#include + +#include "Stylesheet.hpp" +#include "value.h" + +struct AppliedRule { + const StylesheetRule* rule; + int specificity; + size_t index; +}; + +std::string trim(const std::string& s); + +class StylesheetParser { +public: + static std::vector parse(const std::string& source); +}; \ No newline at end of file From f9936dc80c4e498429fd0bde8855aab06d3ef550 Mon Sep 17 00:00:00 2001 From: GHOST11111100 Date: Wed, 30 Jul 2025 02:19:40 +0300 Subject: [PATCH 03/11] Fix UINode::applyStylesheet method declaration by removing class qualifier --- src/graphics/ui/elements/UINode.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graphics/ui/elements/UINode.hpp b/src/graphics/ui/elements/UINode.hpp index 24508e51e..029ef77f8 100644 --- a/src/graphics/ui/elements/UINode.hpp +++ b/src/graphics/ui/elements/UINode.hpp @@ -304,7 +304,7 @@ namespace gui { void setMustRefresh() { mustRefresh = true; } - void UINode::applyStylesheet(const std::vector& rules); + void applyStylesheet(const std::vector& rules); std::unordered_map calculateStylesheet(const std::vector& rules); }; } From 3a53adbade37ea314f2eb3a2b71a13d39c7d612d Mon Sep 17 00:00:00 2001 From: GHOST11111100 Date: Sun, 3 Aug 2025 18:38:05 +0300 Subject: [PATCH 04/11] Refactor style handling and add StyleContext and Stylesheet classes - Introduced `StyleContext` class to manage UI element styles, including tag, ID, classes, and states. - Implemented methods for adding, removing, and checking classes and states within `StyleContext`. - Added functionality to synchronize `StyleContext` with associated `UINode`. - Created `Stylesheet` class to manage style rules and selectors, including parsing CSS-like rules. - Enhanced `value` class to support various data types and added factory methods for creating values from strings. - Implemented `StylesheetParser` for parsing CSS strings into style rules and properties. - Improved type safety and organization of style-related code with enums and structured classes. --- src/graphics/ui/elements/Container.cpp | 14 - src/graphics/ui/elements/UINode.cpp | 173 ------- src/graphics/ui/elements/UINode.hpp | 3 - src/graphics/ui/style/StyleContext.cpp | 226 +++++++++ src/graphics/ui/style/StyleContext.h | 128 +++++ src/graphics/ui/style/Stylesheet.cpp | 295 ++++++++++- src/graphics/ui/style/Stylesheet.h | 174 +++++++ src/graphics/ui/style/Stylesheet.hpp | 14 - src/graphics/ui/style/StylesheetParser.cpp | 496 ++++++++++++++++--- src/graphics/ui/style/StylesheetParser.h | 83 ++++ src/graphics/ui/style/StylesheetParser.hpp | 21 - src/graphics/ui/style/value.cpp | 538 ++++++++++++++------- src/graphics/ui/style/value.h | 141 +++--- 13 files changed, 1784 insertions(+), 522 deletions(-) create mode 100644 src/graphics/ui/style/StyleContext.cpp create mode 100644 src/graphics/ui/style/StyleContext.h create mode 100644 src/graphics/ui/style/Stylesheet.h delete mode 100644 src/graphics/ui/style/Stylesheet.hpp create mode 100644 src/graphics/ui/style/StylesheetParser.h delete mode 100644 src/graphics/ui/style/StylesheetParser.hpp diff --git a/src/graphics/ui/elements/Container.cpp b/src/graphics/ui/elements/Container.cpp index 9c7006152..b38bb8796 100644 --- a/src/graphics/ui/elements/Container.cpp +++ b/src/graphics/ui/elements/Container.cpp @@ -118,19 +118,6 @@ void Container::setScrollable(bool flag) { scrollable = flag; } -std::string stylesheet = R"( - label { - color: #ff0000; - } - button:hover { - color: #22222288; - } - button { - color: #11111188; - } - )"; - -std::vector rules = StylesheetParser::parse(stylesheet); void Container::draw(const DrawContext& pctx, const Assets& assets) { glm::vec2 pos = calcPos(); @@ -147,7 +134,6 @@ void Container::draw(const DrawContext& pctx, const Assets& assets) { ); for (const auto& node : nodes) { if (node->isVisible()) { - node->applyStylesheet(rules); node->draw(pctx, assets); } } diff --git a/src/graphics/ui/elements/UINode.cpp b/src/graphics/ui/elements/UINode.cpp index a417b3ab4..1ce5e52f0 100644 --- a/src/graphics/ui/elements/UINode.cpp +++ b/src/graphics/ui/elements/UINode.cpp @@ -437,176 +437,3 @@ void UINode::setName(std::string _name) { std::string UINode::getName() const { return name; } - -void UINode::applyStylesheet(const std::vector& rules) { - std::unordered_map styles = calculateStylesheet(rules); - for (const auto& [property, value] : styles) { - switch (getStylePropertyType(property)) - { - case StyleProperty::COLOR: - setColor( value.asColor() ); - break; - case StyleProperty::MARGIN: - setMargin( value.asVec4() ); - break; - case StyleProperty::Z_INDEX: - setZIndex( value.asInt() ); - break; - default: - break; - } - } -} - -std::unordered_map UINode::calculateStylesheet(const std::vector& rules) { - std::vector appliedRules; - appliedRules.reserve(rules.size()); - - for (size_t i = 0; i < rules.size(); ++i) { - const auto& rule = rules[i]; - - // Разбиваем составные селекторы (button, .menu) - std::vector selectors; - size_t start = 0; - size_t comma; - do { - comma = rule.selector.find(',', start); - size_t end = (comma == std::string::npos) ? rule.selector.length() : comma; - std::string sel = trim(rule.selector.substr(start, end - start)); - if (!sel.empty()) { - selectors.push_back(sel); - } - start = comma + 1; - } while (comma != std::string::npos); - - bool selectorMatches = false; - - for (const auto& selector : selectors) { - // Проверяем, является ли селектор универсальным * - if (selector == "*") { - // Универсальный селектор применяется ко всем элементам - selectorMatches = true; - - // Специфичность для * = 0 - appliedRules.push_back({&rule, 0, i}); - break; // Другие селекторы в этом правиле не проверяем - } - - // Парсим обычный селектор - std::string tag, id, classname; - std::vector pseudoClasses; - - size_t pos = 0; - while (pos < selector.length()) { - if (std::isspace(selector[pos])) { - pos++; - continue; - } - - if (selector[pos] == '#') { - pos++; - size_t end = selector.find_first_of(".:#[] ", pos); - if (end == std::string::npos) end = selector.length(); - id = selector.substr(pos, end - pos); - pos = end; - } - else if (selector[pos] == '.') { - pos++; - size_t end = selector.find_first_of(".:#[] ", pos); - if (end == std::string::npos) end = selector.length(); - classname = selector.substr(pos, end - pos); - pos = end; - } - else if (selector[pos] == ':') { - pos++; - size_t end = selector.find_first_of(".:#[] ", pos); - if (end == std::string::npos) end = selector.length(); - pseudoClasses.push_back(selector.substr(pos, end - pos)); - pos = end; - } - else { - size_t end = selector.find_first_of(".:#[] ", pos); - if (end == std::string::npos) end = selector.length(); - tag = selector.substr(pos, end - pos); - pos = end; - } - } - - // Проверяем соответствие узлу - bool matches = true; - - // Проверяем тег (если указан и не *) - if (!tag.empty() && tag != "*" && this->getName() != tag) { - matches = false; - } - - // Проверяем ID (если указан) - if (!id.empty() && this->getId() != id) { - matches = false; - } - - // Проверяем класс (если указан) - if (!classname.empty() && this->getClassname() != classname) { - matches = false; - } - - // Проверяем все псевдоклассы - for (const auto& pseudo : pseudoClasses) { - if (pseudo == "hover" && !this->isHover()) { - matches = false; - break; - } - else if (pseudo == "active" && !this->isPressed()) { - matches = false; - break; - } - else if (pseudo == "focus" && !this->isFocused()) { - matches = false; - break; - } - else if (pseudo == "disabled" && this->isEnabled()) { - matches = false; - break; - } - } - - if (matches) { - selectorMatches = true; - - // Вычисляем специфичность селектора - int specificity = 0; - - if (!id.empty()) specificity += 100; - if (!classname.empty()) specificity += 10; - if (!tag.empty() && tag != "*") specificity += 1; - specificity += pseudoClasses.size() * 10; - - appliedRules.push_back({&rule, specificity, i}); - break; // Если один из составных селекторов подходит, остальные не проверяем - } - } - } - - // Сортируем правила по специфичности - std::sort(appliedRules.begin(), appliedRules.end(), - [](const AppliedRule& a, const AppliedRule& b) { - if (a.specificity != b.specificity) { - return a.specificity > b.specificity; - } - return a.index < b.index; // Ранние правила имеют приоритет при одинаковой специфичности - }); - - // Применяем стили - std::unordered_map styles; - - for (const auto& applied : appliedRules) { - for (const auto& [property, value] : applied.rule->declarations) { - // Более специфичные правила переопределяют менее специфичные - if (styles.find(property) == styles.end()) { - styles[property] = value; - } - } - } - - return styles; -} \ No newline at end of file diff --git a/src/graphics/ui/elements/UINode.hpp b/src/graphics/ui/elements/UINode.hpp index 029ef77f8..e226fe7a1 100644 --- a/src/graphics/ui/elements/UINode.hpp +++ b/src/graphics/ui/elements/UINode.hpp @@ -3,7 +3,6 @@ #include "delegates.hpp" #include "graphics/core/commons.hpp" #include "window/input.hpp" -#include "graphics/ui/style/StylesheetParser.hpp" #include #include @@ -304,7 +303,5 @@ namespace gui { void setMustRefresh() { mustRefresh = true; } - void applyStylesheet(const std::vector& rules); - std::unordered_map calculateStylesheet(const std::vector& rules); }; } diff --git a/src/graphics/ui/style/StyleContext.cpp b/src/graphics/ui/style/StyleContext.cpp new file mode 100644 index 000000000..80e011e60 --- /dev/null +++ b/src/graphics/ui/style/StyleContext.cpp @@ -0,0 +1,226 @@ +#include "StyleContext.h" +#include "graphics/ui/elements/UINode.hpp" +#include +#include + +namespace style { + +// Реализация конструкторов +StyleContext::StyleContext() : tag_("node") {} + +StyleContext::StyleContext(const std::string& tag) + : tag_(tag) {} + +StyleContext::StyleContext(const std::string& tag, const std::string& id) + : tag_(tag), id_(id) {} + +StyleContext::StyleContext(const std::string& tag, const std::vector& classes) + : tag_(tag) { + for (const auto& cls : classes) { + classes_.insert(cls); + } +} + +StyleContext::StyleContext(const std::string& tag, const std::string& id, const std::vector& classes) + : tag_(tag), id_(id) { + for (const auto& cls : classes) { + classes_.insert(cls); + } +} + +StyleContext::StyleContext(gui::UINode& node) + : tag_("node"), ui_node_(&node) { + updateFromUINode(); +} + +// Сеттеры +void StyleContext::setTag(const std::string& tag) { + tag_ = tag; + selector_cache_valid_ = false; +} + +void StyleContext::setID(const std::string& id) { + id_ = id; + selector_cache_valid_ = false; +} + +// Работа с классами +void StyleContext::addClass(const std::string& className) { + classes_.insert(className); + selector_cache_valid_ = false; +} + +void StyleContext::removeClass(const std::string& className) { + classes_.erase(className); + selector_cache_valid_ = false; +} + +bool StyleContext::hasClass(const std::string& className) const { + return classes_.find(className) != classes_.end(); +} + +void StyleContext::setClasses(const std::vector& classes) { + classes_.clear(); + for (const auto& cls : classes) { + classes_.insert(cls); + } + selector_cache_valid_ = false; +} + +void StyleContext::clearClasses() { + classes_.clear(); + selector_cache_valid_ = false; +} + +// Работа с состояниями +void StyleContext::setState(State state, bool enabled) { + if (enabled) { + states_.insert(state); + } else { + states_.erase(state); + } +} + +bool StyleContext::hasState(State state) const { + return states_.find(state) != states_.end(); +} + +void StyleContext::clearStates() { + states_.clear(); +} + +// Синхронизация с UINode +void StyleContext::syncWithUINode() { + if (ui_node_) { + updateFromUINode(); + } +} + +void StyleContext::updateFromUINode() { + if (!ui_node_) return; + + // Обновляем ID из UINode + id_ = ui_node_->getId(); + + // Обновляем классы из classname + std::string classname = ui_node_->getClassname(); + if (!classname.empty()) { + // Разделяем classname на отдельные классы (предполагаем разделение пробелами) + std::istringstream iss(classname); + std::string cls; + classes_.clear(); + while (iss >> cls) { + if (!cls.empty()) { + classes_.insert(cls); + } + } + } + + // Обновляем состояния на основе свойств UINode + states_.clear(); + if (!ui_node_->isEnabled()) { + states_.insert(State::Disabled); + } + if (ui_node_->isHover()) { + states_.insert(State::Hover); + } + if (ui_node_->isPressed()) { + states_.insert(State::Active); + } + if (ui_node_->isFocused()) { + states_.insert(State::Focused); + } + + selector_cache_valid_ = false; +} + +// Утилиты +std::string StyleContext::getSelectorString() const { + if (selector_cache_valid_) { + return selector_cache_; + } + + std::ostringstream oss; + + // Добавляем тег + if (!tag_.empty()) { + oss << tag_; + } + + // Добавляем ID + if (!id_.empty()) { + oss << "#" << id_; + } + + // Добавляем классы + for (const auto& cls : classes_) { + oss << "." << cls; + } + + selector_cache_ = oss.str(); + selector_cache_valid_ = true; + + return selector_cache_; +} + +bool StyleContext::hasID(const std::string& id) const { + return id == id; +} + +std::unique_ptr StyleContext::clone() const { + auto clone = std::make_unique(tag_, id_); + clone->classes_ = classes_; + clone->states_ = states_; + clone->parent_ = parent_; + clone->ui_node_ = ui_node_; + clone->selector_cache_ = selector_cache_; + clone->selector_cache_valid_ = selector_cache_valid_; + return clone; +} + +bool StyleContext::operator==(const StyleContext& other) const { + return tag_ == other.tag_ && + id_ == other.id_ && + classes_ == other.classes_ && + states_ == other.states_; +} + +std::size_t StyleContext::Hash::operator()(const StyleContext& ctx) const { + std::size_t h1 = std::hash{}(ctx.tag_); + std::size_t h2 = std::hash{}(ctx.id_); + // Простая комбинация хэшей + return h1 ^ (h2 << 1); +} + +void StyleContext::updateSelectorCache() const { + // Реализация кэширования селектора + selector_cache_valid_ = false; +} + +// Фабричные методы +namespace context { + std::unique_ptr create(const std::string& tag) { + return std::make_unique(tag); + } + + std::unique_ptr create(const std::string& tag, const std::string& id) { + return std::make_unique(tag, id); + } + + std::unique_ptr create(const std::string& tag, + const std::vector& classes) { + return std::make_unique(tag, classes); + } + + std::unique_ptr create(const std::string& tag, + const std::string& id, + const std::vector& classes) { + return std::make_unique(tag, id, classes); + } + + std::unique_ptr fromUINode(gui::UINode& node) { + return std::make_unique(node); + } +} + +} // namespace style \ No newline at end of file diff --git a/src/graphics/ui/style/StyleContext.h b/src/graphics/ui/style/StyleContext.h new file mode 100644 index 000000000..636cd739e --- /dev/null +++ b/src/graphics/ui/style/StyleContext.h @@ -0,0 +1,128 @@ +// StyleContext.h +#pragma once + +#include +#include +#include +#include +#include + +// Предварительные объявления +namespace style { + class Stylesheet; + class ComputedStyle; +} + +namespace gui { + class UINode; // Из предоставленного кода +} + +namespace style { + +/** + * Контекст для вычисления стилей. + * Представляет элемент UI с его характеристиками для применения стилей. + */ +class StyleContext { +public: + // Типы состояний элемента + enum class State { + Normal, + Hover, + Active, + Disabled, + Focused + }; + +private: + // Базовые свойства элемента для сопоставления с селекторами + std::string tag_; // Тег элемента (например, "button", "panel") + std::unordered_set classes_; // CSS-классы + std::string id_; // Уникальный идентификатор + std::unordered_set states_; // Текущие состояния + + // Ссылка на родительский контекст (для наследования) + const StyleContext* parent_ = nullptr; + + // Указатель на соответствующий UINode (для интеграции) + gui::UINode* ui_node_ = nullptr; + +public: + // Конструкторы + StyleContext(); + explicit StyleContext(const std::string& tag); + StyleContext(const std::string& tag, const std::string& id); + StyleContext(const std::string& tag, const std::vector& classes); + StyleContext(const std::string& tag, const std::string& id, const std::vector& classes); + + // Конструктор для интеграции с UINode + explicit StyleContext(gui::UINode& node); + + // Геттеры + const std::string& getTag() const { return tag_; } + const std::string& getID() const { return id_; } + const std::unordered_set& getClasses() const { return classes_; } + const std::unordered_set& getStates() const { return states_; } + const StyleContext* getParent() const { return parent_; } + gui::UINode* getUINode() const { return ui_node_; } + + // Сеттеры + void setTag(const std::string& tag); + void setID(const std::string& id); + void setParent(const StyleContext* parent) { parent_ = parent; } + void setUINode(gui::UINode* node) { ui_node_ = node; } + + // Работа с классами + void addClass(const std::string& className); + void removeClass(const std::string& className); + bool hasClass(const std::string& className) const; + void setClasses(const std::vector& classes); + void clearClasses(); + + // Работа с состояниями + void setState(State state, bool enabled = true); + bool hasState(State state) const; + void clearStates(); + + // Методы для синхронизации с UINode + void syncWithUINode(); + void updateFromUINode(); + + // Утилиты + std::string getSelectorString() const; + bool hasID(const std::string& id) const; + + // Создание копии контекста + std::unique_ptr clone() const; + + // Операторы сравнения + bool operator==(const StyleContext& other) const; + bool operator!=(const StyleContext& other) const { return !(*this == other); } + + // Хэш для использования в контейнерах + struct Hash { + std::size_t operator()(const StyleContext& ctx) const; + }; + +private: + // Вспомогательные методы + void updateSelectorCache() const; + mutable std::string selector_cache_; + mutable bool selector_cache_valid_ = false; +}; + +// Фабричные методы для удобного создания контекстов +namespace context { + std::unique_ptr create(const std::string& tag); + std::unique_ptr create(const std::string& tag, const std::string& id); + std::unique_ptr create(const std::string& tag, + const std::vector& classes); + std::unique_ptr create(const std::string& tag, + const std::string& id, + const std::vector& classes); + + // Для интеграции с UINode + std::unique_ptr fromUINode(gui::UINode& node); +} + +} // namespace style \ No newline at end of file diff --git a/src/graphics/ui/style/Stylesheet.cpp b/src/graphics/ui/style/Stylesheet.cpp index b4cf8aa52..3e26f247b 100644 --- a/src/graphics/ui/style/Stylesheet.cpp +++ b/src/graphics/ui/style/Stylesheet.cpp @@ -1,12 +1,289 @@ -#include +// Stylesheet.cpp +#include "Stylesheet.h" +#include "StyleContext.h" #include -#include +#include +#include -#include "Stylesheet.hpp" +namespace style { -StyleProperty getStylePropertyType(const std::string& property) { - if (property == "color") return StyleProperty::COLOR; - if (property == "margin") return StyleProperty::MARGIN; - if (property == "z-index") return StyleProperty::Z_INDEX; - return StyleProperty::UNKNOWN; -} \ No newline at end of file +// Реализация PropertyID функций +PropertyID getPropertyID(const std::string& name) { + static const std::unordered_map property_map = { + {"color", PropertyID::Color}, + {"background-color", PropertyID::BackgroundColor}, + {"margin", PropertyID::Margin}, + {"margin-top", PropertyID::MarginTop}, + {"margin-right", PropertyID::MarginRight}, + {"margin-bottom", PropertyID::MarginBottom}, + {"margin-left", PropertyID::MarginLeft}, + {"padding", PropertyID::Padding}, + {"padding-top", PropertyID::PaddingTop}, + {"padding-right", PropertyID::PaddingRight}, + {"padding-bottom", PropertyID::PaddingBottom}, + {"padding-left", PropertyID::PaddingLeft}, + {"width", PropertyID::Width}, + {"height", PropertyID::Height}, + {"min-width", PropertyID::MinWidth}, + {"min-height", PropertyID::MinHeight}, + {"max-width", PropertyID::MaxWidth}, + {"max-height", PropertyID::MaxHeight}, + {"z-index", PropertyID::ZIndex}, + {"display", PropertyID::Display}, + {"position", PropertyID::Position}, + {"top", PropertyID::Top}, + {"right", PropertyID::Right}, + {"bottom", PropertyID::Bottom}, + {"left", PropertyID::Left}, + {"opacity", PropertyID::Opacity}, + {"border", PropertyID::Border}, + {"border-radius", PropertyID::BorderRadius}, + {"font-size", PropertyID::FontSize}, + {"font-weight", PropertyID::FontWeight}, + {"text-align", PropertyID::TextAlign} + }; + + auto it = property_map.find(name); + return (it != property_map.end()) ? it->second : PropertyID::Unknown; +} + +std::string getPropertyName(PropertyID id) { + static const std::unordered_map property_names = { + {PropertyID::Color, "color"}, + {PropertyID::BackgroundColor, "background-color"}, + {PropertyID::Margin, "margin"}, + {PropertyID::MarginTop, "margin-top"}, + {PropertyID::MarginRight, "margin-right"}, + {PropertyID::MarginBottom, "margin-bottom"}, + {PropertyID::MarginLeft, "margin-left"}, + {PropertyID::Padding, "padding"}, + {PropertyID::PaddingTop, "padding-top"}, + {PropertyID::PaddingRight, "padding-right"}, + {PropertyID::PaddingBottom, "padding-bottom"}, + {PropertyID::PaddingLeft, "padding-left"}, + {PropertyID::Width, "width"}, + {PropertyID::Height, "height"}, + {PropertyID::MinWidth, "min-width"}, + {PropertyID::MinHeight, "min-height"}, + {PropertyID::MaxWidth, "max-width"}, + {PropertyID::MaxHeight, "max-height"}, + {PropertyID::ZIndex, "z-index"}, + {PropertyID::Display, "display"}, + {PropertyID::Position, "position"}, + {PropertyID::Top, "top"}, + {PropertyID::Right, "right"}, + {PropertyID::Bottom, "bottom"}, + {PropertyID::Left, "left"}, + {PropertyID::Opacity, "opacity"}, + {PropertyID::Border, "border"}, + {PropertyID::BorderRadius, "border-radius"}, + {PropertyID::FontSize, "font-size"}, + {PropertyID::FontWeight, "font-weight"}, + {PropertyID::TextAlign, "text-align"} + }; + + auto it = property_names.find(id); + return (it != property_names.end()) ? it->second : "unknown"; +} + +// Реализация Selector +Selector::Selector(Type type, std::string value) + : type_(type), value_(std::move(value)) { + // Вычисляем специфичность (упрощенная версия CSS-специфичности) + switch (type_) { + case Type::Universal: specificity_ = 0; break; + case Type::Tag: specificity_ = 1; break; + case Type::Class: specificity_ = 10; break; + case Type::ID: specificity_ = 100; break; + } +} + +bool Selector::matches(const StyleContext& context) const { + switch (type_) { + case Type::Universal: + return true; + case Type::Tag: + return context.getTag() == value_; + case Type::Class: + return context.hasClass(value_); + case Type::ID: + return context.hasID(value_); + } + return false; +} + +bool Selector::operator==(const Selector& other) const { + return type_ == other.type_ && value_ == other.value_; +} + +bool Selector::operator<(const Selector& other) const { + if (type_ != other.type_) { + return static_cast(type_) < static_cast(other.type_); + } + return value_ < other.value_; +} + +std::size_t Selector::Hash::operator()(const Selector& s) const { + return std::hash{}(static_cast(s.type_)) ^ + std::hash{}(s.value_); +} + +// Реализация StyleRule +StyleRule::StyleRule(std::vector selectors_, + std::unordered_map declarations_) + : selectors(std::move(selectors_)), declarations(std::move(declarations_)) { + priority = 0; +} + +int StyleRule::getSpecificity() const { + int total = 0; + for (const auto& selector : selectors) { + total += selector.getSpecificity(); + } + return total; +} + +bool StyleRule::matches(const StyleContext& context) const { + // Пока поддерживаем только простые селекторы (без комбинаторов) + for (const auto& selector : selectors) { + if (selector.matches(context)) { + return true; + } + } + return false; +} + +// Реализация Stylesheet +void Stylesheet::addRule(const StyleRule& rule) { + rules_.push_back(rule); +} + +void Stylesheet::addRule(const std::vector& selectors, + const std::unordered_map& declarations) { + rules_.emplace_back(selectors, declarations); +} + +std::vector Stylesheet::getMatchingRules(const StyleContext& context) const { + std::vector matching_rules; + + // Если есть индекс, используем его для ускорения + if (!tag_index_.empty() || !class_index_.empty() || !id_index_.empty()) { + // Используем индекс для быстрого поиска + std::unordered_set candidate_indices; + + // Проверяем по тегу + auto tag_it = tag_index_.find(context.getTag()); + if (tag_it != tag_index_.end()) { + candidate_indices.insert(tag_it->second.begin(), tag_it->second.end()); + } + + // Проверяем по классам + for (const auto& cls : context.getClasses()) { + auto class_it = class_index_.find(cls); + if (class_it != class_index_.end()) { + candidate_indices.insert(class_it->second.begin(), class_it->second.end()); + } + } + + // Проверяем по ID + auto id_it = id_index_.find(context.getID()); + if (id_it != id_index_.end()) { + candidate_indices.insert(id_it->second.begin(), id_it->second.end()); + } + + // Проверяем кандидатов + for (size_t index : candidate_indices) { + if (index < rules_.size() && rules_[index].matches(context)) { + matching_rules.push_back(&rules_[index]); + } + } + } else { + // Линейный поиск (если индекс не построен) + for (const auto& rule : rules_) { + if (rule.matches(context)) { + matching_rules.push_back(&rule); + } + } + } + + return matching_rules; +} + +void Stylesheet::sortRulesBySpecificity() { + std::sort(rules_.begin(), rules_.end(), + [](const StyleRule& a, const StyleRule& b) { + return a.getSpecificity() < b.getSpecificity(); + }); +} + +void Stylesheet::clear() { + rules_.clear(); + tag_index_.clear(); + class_index_.clear(); + id_index_.clear(); + selector_cache_.clear(); +} + +void Stylesheet::buildIndex() { + tag_index_.clear(); + class_index_.clear(); + id_index_.clear(); + + for (size_t i = 0; i < rules_.size(); ++i) { + const auto& rule = rules_[i]; + for (const auto& selector : rule.selectors) { + switch (selector.getType()) { + case Selector::Type::Tag: + tag_index_[selector.getValue()].push_back(i); + break; + case Selector::Type::Class: + class_index_[selector.getValue()].push_back(i); + break; + case Selector::Type::ID: + id_index_[selector.getValue()].push_back(i); + break; + case Selector::Type::Universal: + // Универсальный селектор не индексируем + break; + } + } + } +} + +// Фабричные методы для селекторов +Selector Stylesheet::createTagSelector(const std::string& tag) { + return Selector(Selector::Type::Tag, tag); +} + +Selector Stylesheet::createClassSelector(const std::string& className) { + return Selector(Selector::Type::Class, className); +} + +Selector Stylesheet::createIDSelector(const std::string& id) { + return Selector(Selector::Type::ID, id); +} + +Selector Stylesheet::createUniversalSelector() { + return Selector(Selector::Type::Universal, "*"); +} + +// Утилиты для селекторов +namespace selectors { + Selector tag(const std::string& name) { + return Selector(Selector::Type::Tag, name); + } + + Selector cls(const std::string& name) { + return Selector(Selector::Type::Class, name); + } + + Selector id(const std::string& name) { + return Selector(Selector::Type::ID, name); + } + + Selector universal() { + return Selector(Selector::Type::Universal, "*"); + } +} + +} // namespace style \ No newline at end of file diff --git a/src/graphics/ui/style/Stylesheet.h b/src/graphics/ui/style/Stylesheet.h new file mode 100644 index 000000000..cee9ac0e0 --- /dev/null +++ b/src/graphics/ui/style/Stylesheet.h @@ -0,0 +1,174 @@ +#pragma once + +#include +#include +#include +#include +#include +#include "value.h" + +namespace style { + +// Предварительные объявления +class StyleContext; +class ComputedStyle; +class Selector; + +// Идентификаторы свойств для оптимизации +enum class PropertyID : uint32_t { + Unknown = 0, + Color, + BackgroundColor, + Margin, + MarginTop, + MarginRight, + MarginBottom, + MarginLeft, + Padding, + PaddingTop, + PaddingRight, + PaddingBottom, + PaddingLeft, + Width, + Height, + MinWidth, + MinHeight, + MaxWidth, + MaxHeight, + ZIndex, + Display, + Position, + Top, + Right, + Bottom, + Left, + Opacity, + Border, + BorderRadius, + FontSize, + FontWeight, + TextAlign, + // ... добавь больше по мере необходимости +}; + +// Получение PropertyID по строке +PropertyID getPropertyID(const std::string& name); +std::string getPropertyName(PropertyID id); + +// Селектор стиля +class Selector { +public: + enum class Type { + Tag, // block + Class, // .highlight + ID, // #main + Universal // * + }; + +private: + Type type_; + std::string value_; + int specificity_ = 0; // CSS-специфичность для каскада + +public: + Selector(Type type, std::string value); + + Type getType() const { return type_; } + const std::string& getValue() const { return value_; } + int getSpecificity() const { return specificity_; } + + // Проверяет, соответствует ли селектор контексту + bool matches(const StyleContext& context) const; + + // Операторы сравнения + bool operator==(const Selector& other) const; + bool operator<(const Selector& other) const; + + struct Hash { + std::size_t operator()(const Selector& s) const; + }; +}; + +// Правило стиля +struct StyleRule { + std::vector selectors; + std::unordered_map declarations; + int priority = 0; // для !important и других приоритетов + + StyleRule() = default; + StyleRule(std::vector selectors_, + std::unordered_map declarations_); + + // Вычисляет специфичность правила + int getSpecificity() const; + + // Проверяет, подходит ли правило для контекста + bool matches(const StyleContext& context) const; +}; + +// Основной класс таблицы стилей +class Stylesheet { +private: + std::vector rules_; + + // Индексы для быстрого поиска + std::unordered_map> tag_index_; + std::unordered_map> class_index_; + std::unordered_map> id_index_; + + // Кэш для часто используемых селекторов + mutable std::unordered_map> selector_cache_; + +public: + Stylesheet() = default; + + // Добавление правила + void addRule(const StyleRule& rule); + void addRule(const std::vector& selectors, + const std::unordered_map& declarations); + + // Парсинг CSS-подобного правила из строки + bool parseAndAddRule(const std::string& css_string); + + // Получение всех подходящих правил для контекста + std::vector getMatchingRules(const StyleContext& context) const; + + // Сортировка правил по специфичности (для каскада) + void sortRulesBySpecificity(); + + // Очистка таблицы стилей + void clear(); + + // Получение количества правил + size_t getRuleCount() const { return rules_.size(); } + + // Индексация для ускорения поиска + void buildIndex(); + + // Вспомогательные методы для создания селекторов + static Selector createTagSelector(const std::string& tag); + static Selector createClassSelector(const std::string& className); + static Selector createIDSelector(const std::string& id); + static Selector createUniversalSelector(); + +private: + // Вспомогательные методы для парсинга + std::vector parseSelectors(const std::string& selector_string); + std::unordered_map parseDeclarations(const std::string& declarations_string); + + // Получение или создание селектора с кэшированием + std::shared_ptr getOrCreateSelector(Selector::Type type, const std::string& value) const; +}; + +// Утилиты для работы с селекторами +namespace selectors { + Selector tag(const std::string& name); + Selector cls(const std::string& name); // class + Selector id(const std::string& name); + Selector universal(); + + // Комбинирование селекторов + std::vector combine(const std::vector& a, const std::vector& b); +} + +} // namespace style \ No newline at end of file diff --git a/src/graphics/ui/style/Stylesheet.hpp b/src/graphics/ui/style/Stylesheet.hpp deleted file mode 100644 index 604e4776b..000000000 --- a/src/graphics/ui/style/Stylesheet.hpp +++ /dev/null @@ -1,14 +0,0 @@ -#include -#include -#include - -#include "value.h" - -struct StylesheetRule { - std::string selector; - std::unordered_map declarations; -}; - -enum class StyleProperty { COLOR, MARGIN, Z_INDEX, UNKNOWN }; - -StyleProperty getStylePropertyType(const std::string& property); \ No newline at end of file diff --git a/src/graphics/ui/style/StylesheetParser.cpp b/src/graphics/ui/style/StylesheetParser.cpp index dd4df5221..9172b53ce 100644 --- a/src/graphics/ui/style/StylesheetParser.cpp +++ b/src/graphics/ui/style/StylesheetParser.cpp @@ -1,78 +1,464 @@ -#include "StylesheetParser.hpp" -#include +#include "StylesheetParser.h" #include -#include +#include +#include +#include -// Утилита: trim -std::string trim(const std::string& s) { - size_t start = s.find_first_not_of(" \t\n\r"); - size_t end = s.find_last_not_of(" \t\n\r"); - return (start == std::string::npos) ? "" : s.substr(start, end - start + 1); -} +namespace style { -// Разделение составных селекторов: `button, .menu` -static std::vector splitSelectors(const std::string& sel) { - std::vector result; - std::stringstream ss(sel); - std::string part; - while (std::getline(ss, part, ',')) { - auto t = trim(part); - if (!t.empty()) result.push_back(t); +StylesheetParser::ParseResult StylesheetParser::parse(const std::string& css_source) { + source_ = css_source; + position_ = 0; + line_ = 1; + column_ = 1; + + ParseResult result; + result.rules.reserve(16); // Резервируем место для оптимизации + + try { + while (position_ < source_.length()) { + skipWhitespace(); + skipComments(); + + if (position_ >= source_.length()) break; + + // Парсим селекторы + auto selectors = parseSelectorGroup(); + if (selectors.empty()) { + return makeError("Expected selector"); + } + + skipWhitespace(); + + // Ожидаем начало блока деклараций + if (!match('{')) { + return makeError("Expected '{' after selector"); + } + + // Парсим блок деклараций + auto declarations = parseDeclarationBlock(); + + // Ожидаем конец блока + if (!match('}')) { + return makeError("Expected '}' after declarations"); + } + // ВАЖНО: нужно переместить позицию после найденной скобки + position_++; + + // Создаем правило + if (!declarations.empty()) { + result.rules.emplace_back(selectors, declarations); + } + + skipWhitespace(); + skipComments(); + } + + result.success = true; + } catch (const std::exception& e) { + result.success = false; + result.error_message = "Exception during parsing: " + std::string(e.what()); } + return result; } -std::vector StylesheetParser::parse(const std::string& source) { - std::vector rules; - size_t pos = 0; - const size_t len = source.length(); - - while (pos < len) { - if (std::isspace(source[pos])) { - ++pos; - continue; +std::vector StylesheetParser::parseSelectorGroup() { + std::vector selectors; + + while (position_ < source_.length()) { + skipWhitespace(); + + if (position_ >= source_.length()) break; + + // Проверяем, не достигли ли мы конца селектора или блока + if (peek() == '{' || peek() == '}' || peek() == ';') { + break; } - if (pos + 1 < len && source.substr(pos, 2) == "/*") { - auto end = source.find("*/", pos); - if (end == std::string::npos) break; - pos = end + 2; - continue; + + // Парсим простой селектор + Selector selector = parseSimpleSelector(); + // Добавляем ВСЕ селекторы, включая универсальные * + selectors.push_back(selector); + + skipWhitespace(); + + // Если следующий символ не ',', значит группа закончилась + if (position_ >= source_.length() || source_[position_] != ',') { + break; } + + consume(); // Пропускаем ',' + skipWhitespace(); + } + + return selectors; +} - auto open_brace = source.find('{', pos); - if (open_brace == std::string::npos) break; +Selector StylesheetParser::parseSimpleSelector() { + if (position_ >= source_.length()) { + return Selector(Selector::Type::Universal, "*"); + } + + char first_char = source_[position_]; + + switch (first_char) { + case '*': + consume(); + return Selector(Selector::Type::Universal, "*"); + + case '#': { + consume(); // Пропускаем '#' + std::string id = consumeIdentifier(); + if (id.empty()) { + return Selector(Selector::Type::Universal, "*"); // fallback + } + return Selector(Selector::Type::ID, id); + } + + case '.': { + consume(); // Пропускаем '.' + std::string className = consumeIdentifier(); + if (className.empty()) { + return Selector(Selector::Type::Universal, "*"); // fallback + } + return Selector(Selector::Type::Class, className); + } + + default: { + if (isIdentifierStart(first_char)) { + std::string tag = consumeIdentifier(); + return Selector(Selector::Type::Tag, tag); + } + // Если символ не подходит ни под один тип, возвращаем универсальный селектор + return Selector(Selector::Type::Universal, "*"); + } + } +} - auto selector_str = trim(source.substr(pos, open_brace - pos)); - pos = open_brace + 1; +std::unordered_map StylesheetParser::parseDeclarationBlock() { + std::unordered_map declarations; + + while (position_ < source_.length() && peek() != '}') { + skipWhitespace(); + + if (position_ >= source_.length() || peek() == '}') break; + + // Парсим одну декларацию + auto declaration = parseDeclaration(); + if (declaration.first != PropertyID::Unknown) { + declarations[declaration.first] = declaration.second; + } + + skipWhitespace(); + + // Пропускаем ';' + if (position_ < source_.length() && peek() == ';') { + consume(); + } + + skipWhitespace(); + } + + return declarations; +} - auto close_brace = source.find('}', pos); - if (close_brace == std::string::npos) break; +std::pair StylesheetParser::parseDeclaration() { + skipWhitespace(); + + // Парсим имя свойства + std::string property_name = consumeIdentifier(); + if (property_name.empty()) { + return {PropertyID::Unknown, value()}; + } + + skipWhitespace(); + + // Ожидаем ':' + if (!match(':')) { + skipUntil(';'); // Пропускаем до конца декларации + return {PropertyID::Unknown, value()}; + } + + skipWhitespace(); + + // Парсим значение + std::string value_str; + while (position_ < source_.length() && + peek() != ';' && peek() != '}') { + if (!isWhitespace(peek())) { + value_str += consume(); + } else { + // Сохраняем пробелы в значении, если они важны + value_str += consume(); + } + } + + // Преобразуем в PropertyID + PropertyID prop_id = getPropertyID(property_name); + if (prop_id == PropertyID::Unknown) { + return {PropertyID::Unknown, value()}; + } + + // Преобразуем строку значения в value + value val = value::fromString(value_str); + + return {prop_id, val}; +} - auto body = source.substr(pos, close_brace - pos); - pos = close_brace + 1; +// Добавьте этот вспомогательный метод +void StylesheetParser::skipUntil(char delimiter) { + while (position_ < source_.length() && peek() != delimiter && peek() != '}') { + consume(); + } + if (position_ < source_.length() && peek() == delimiter) { + consume(); + } +} - std::unordered_map decl; - std::stringstream sb(body); - std::string line; - while (std::getline(sb, line, ';')) { - auto colon = line.find(':'); - if (colon != std::string::npos) { - std::string prop = trim(line.substr(0, colon)); - std::string value_str = trim(line.substr(colon + 1)); - if (!prop.empty() && !value_str.empty()) { - // Автоматическое определение типа - style::value parsed_value = value_str; +// Вспомогательные методы +void StylesheetParser::skipWhitespace() { + while (position_ < source_.length() && isWhitespace(peek())) { + if (peek() == '\n') { + line_++; + column_ = 1; + } else { + column_++; + } + position_++; + } +} - decl[prop] = parsed_value; +void StylesheetParser::skipComments() { + while (position_ < source_.length() - 1) { + if (source_[position_] == '/' && source_[position_ + 1] == '*') { + // Найден комментарий /* */ + position_ += 2; + column_ += 2; + + while (position_ < source_.length() - 1) { + if (source_[position_] == '*' && source_[position_ + 1] == '/') { + position_ += 2; + column_ += 2; + break; + } + if (source_[position_] == '\n') { // Исправлено: source_[position_] вместо peek() + line_++; + column_ = 1; + } else { + column_++; } + position_++; // column_ должен обновляться до position_++ } + } else if (source_[position_] == '/' && source_[position_ + 1] == '/') { + // Найден комментарий // + position_ += 2; + column_ += 2; + + while (position_ < source_.length() && source_[position_] != '\n') { // Исправлено + position_++; + column_++; + } + // Пропускаем символ новой строки, если он есть + if (position_ < source_.length() && source_[position_] == '\n') { + position_++; + line_++; + column_ = 1; + } + } else { + break; } + } +} + +char StylesheetParser::peek() const { + if (position_ >= source_.length()) return '\0'; + return source_[position_]; +} - auto selectors = splitSelectors(selector_str); - for (const auto& sel : selectors) { - rules.push_back({ sel, decl }); +char StylesheetParser::peek(size_t offset) const { + if (position_ + offset >= source_.length()) return '\0'; + return source_[position_ + offset]; +} + +char StylesheetParser::consume() { + if (position_ >= source_.length()) return '\0'; + + char c = source_[position_++]; + if (c == '\n') { + line_++; + column_ = 1; + } else { + column_++; + } + return c; +} + +bool StylesheetParser::match(char c) { + if (position_ < source_.length() && source_[position_] == c) { + consume(); + return true; + } + return false; +} + +bool StylesheetParser::match(const std::string& str) { + if (position_ + str.length() <= source_.length()) { + if (source_.substr(position_, str.length()) == str) { + for (size_t i = 0; i < str.length(); ++i) { + consume(); + } + return true; + } + } + return false; +} + +std::string StylesheetParser::consumeWhile(bool (*predicate)(char)) { + std::string result; + while (position_ < source_.length() && predicate(peek())) { + result += consume(); + } + return result; +} + +std::string StylesheetParser::consumeIdentifier() { + return consumeWhile([](char c) { + return isIdentifierChar(c); + }); +} + +std::string StylesheetParser::consumeString() { + if (position_ >= source_.length()) return ""; + + char quote = peek(); + if (quote != '"' && quote != '\'') return ""; + + consume(); // Пропускаем открывающую кавычку + + std::string result; + while (position_ < source_.length() && peek() != quote) { + if (peek() == '\\') { + consume(); // Пропускаем '\' + if (position_ < source_.length()) { + result += consume(); + } + } else { + result += consume(); + } + } + + if (position_ < source_.length() && peek() == quote) { + consume(); // Пропускаем закрывающую кавычку + } + + return result; +} + +std::string StylesheetParser::consumeNumber() { + return consumeWhile([](char c) { + return isNumberChar(c); + }); +} + +// Статические утилиты +std::vector StylesheetParser::splitSelectors(const std::string& selector_string) { + std::vector selectors; + std::string current; + int bracket_count = 0; + + for (char c : selector_string) { + if (c == ',' && bracket_count == 0) { + if (!current.empty()) { + selectors.push_back(current); + current.clear(); + } + } else { + if (c == '(') bracket_count++; + else if (c == ')') bracket_count--; + current += c; + } + } + + if (!current.empty()) { + selectors.push_back(current); + } + + return selectors; +} + +std::pair StylesheetParser::splitDeclaration(const std::string& declaration) { + size_t colon_pos = declaration.find(':'); + if (colon_pos == std::string::npos) { + return {"", ""}; + } + + std::string property = declaration.substr(0, colon_pos); + std::string value = declaration.substr(colon_pos + 1); + + // Убираем пробелы + property.erase(0, property.find_first_not_of(" \t\r\n")); + property.erase(property.find_last_not_of(" \t\r\n") + 1); + + value.erase(0, value.find_first_not_of(" \t\r\n")); + value.erase(value.find_last_not_of(" \t\r\n") + 1); + + return {property, value}; +} + +// Вспомогательные функции +bool StylesheetParser::isWhitespace(char c) { + return c == ' ' || c == '\t' || c == '\n' || c == '\r'; +} + +bool StylesheetParser::isIdentifierStart(char c) { + return std::isalpha(c) || c == '_' || c == '-'; +} + +bool StylesheetParser::isIdentifierChar(char c) { + return std::isalnum(c) || c == '_' || c == '-'; +} + +bool StylesheetParser::isNumberChar(char c) { + return std::isdigit(c) || c == '.' || c == '-' || c == '+'; +} + +// Обработка ошибок +StylesheetParser::ParseResult StylesheetParser::makeError(const std::string& message) const { + ParseResult result; + result.success = false; + result.error_message = message + " at line " + std::to_string(line_) + + ", column " + std::to_string(column_); + return result; +} + +std::string StylesheetParser::getCurrentPosition() const { + return "line " + std::to_string(line_) + ", column " + std::to_string(column_); +} + +// Утилиты для внешнего использования +namespace parser { + StylesheetParser::ParseResult parseCSS(const std::string& css_string) { + StylesheetParser parser; + return parser.parse(css_string); + } + + bool parseAndAddToStylesheet(Stylesheet& stylesheet, const std::string& css_string) { + auto result = parseCSS(css_string); + if (result.success) { + for (const auto& rule : result.rules) { + stylesheet.addRule(rule); + } + return true; } + std::cerr << "CSS Parse Error: " << result.error_message << std::endl; + return false; } +} - return rules; +// Добавим метод в Stylesheet для удобства +bool Stylesheet::parseAndAddRule(const std::string& css_string) { + return parser::parseAndAddToStylesheet(*this, css_string); } + +} // namespace style \ No newline at end of file diff --git a/src/graphics/ui/style/StylesheetParser.h b/src/graphics/ui/style/StylesheetParser.h new file mode 100644 index 000000000..96d2e3f32 --- /dev/null +++ b/src/graphics/ui/style/StylesheetParser.h @@ -0,0 +1,83 @@ +#pragma once + +#include +#include +#include +#include + +#include "Stylesheet.h" + +namespace style { + +/** + * Парсер CSS-подобных стилей + */ +class StylesheetParser { +public: + struct ParseResult { + bool success = false; + std::string error_message; + std::vector rules; + }; + +private: + std::string source_; + size_t position_ = 0; + size_t line_ = 1; + size_t column_ = 1; + +public: + StylesheetParser() = default; + + // Основной метод парсинга + ParseResult parse(const std::string& css_source); + + // Парсинг отдельных компонентов + std::vector parseSelector(const std::string& selector_string); + std::unordered_map parseDeclarations(const std::string& declarations_string); + + // Утилиты для парсинга + static std::vector splitSelectors(const std::string& selector_string); + static std::pair splitDeclaration(const std::string& declaration); + +private: + // Вспомогательные методы для парсинга + void skipUntil(char delimiter); + void skipWhitespace(); + void skipComments(); + char peek() const; + char peek(size_t offset) const; + char consume(); + bool match(char c); + bool match(const std::string& str); + std::string consumeWhile(bool (*predicate)(char)); + std::string consumeIdentifier(); + std::string consumeString(); + std::string consumeNumber(); + + // Парсинг конкретных элементов + std::vector parseSelectorGroup(); + Selector parseSimpleSelector(); + std::unordered_map parseDeclarationBlock(); + std::pair parseDeclaration(); + + // Обработка ошибок + ParseResult makeError(const std::string& message) const; + std::string getCurrentPosition() const; + + // Утилиты + static bool isWhitespace(char c); + static bool isIdentifierStart(char c); + static bool isIdentifierChar(char c); + static bool isNumberChar(char c); +}; + +// Утилиты для работы с парсером +namespace parser { + StylesheetParser::ParseResult parseCSS(const std::string& css_string); + + // Быстрое добавление CSS в Stylesheet + bool parseAndAddToStylesheet(Stylesheet& stylesheet, const std::string& css_string); +} + +} // namespace style \ No newline at end of file diff --git a/src/graphics/ui/style/StylesheetParser.hpp b/src/graphics/ui/style/StylesheetParser.hpp deleted file mode 100644 index 66d6aa03d..000000000 --- a/src/graphics/ui/style/StylesheetParser.hpp +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once - -#include -#include -#include - -#include "Stylesheet.hpp" -#include "value.h" - -struct AppliedRule { - const StylesheetRule* rule; - int specificity; - size_t index; -}; - -std::string trim(const std::string& s); - -class StylesheetParser { -public: - static std::vector parse(const std::string& source); -}; \ No newline at end of file diff --git a/src/graphics/ui/style/value.cpp b/src/graphics/ui/style/value.cpp index 2c89cb3f3..e93916613 100644 --- a/src/graphics/ui/style/value.cpp +++ b/src/graphics/ui/style/value.cpp @@ -1,245 +1,443 @@ + #include "value.h" -#include +#include #include +#include +#include +#include namespace style { -// Убираем пробелы -static std::string trim(const std::string& s) { - auto wsfront = std::find_if(s.begin(), s.end(), [](int c){return !std::isspace(c);}); - auto wsback = std::find_if(s.rbegin(), s.rend(), [](int c){return !std::isspace(c);}).base(); - return (wsback <= wsfront ? std::string() : std::string(wsfront, wsback)); -} +// --- Конструкторы --- -// Проверка, что строка — число -static bool is_number(const std::string& s) { - if (s.empty()) return false; - size_t i = s[0] == '-' ? 1 : 0; - return i < s.size() && std::all_of(s.begin() + i, s.end(), ::isdigit); -} +// Конструктор по умолчанию +value::value() = default; // Определение, если в .h только декларация -// Парсинг цвета: #RGB, #RGBA, #RRGGBB, #RRGGBBAA, rgba() -static std::optional parse_color(const std::string& str) { - auto s = trim(str); - if (s.empty()) return std::nullopt; +// Конструктор копирования +value::value(const value& other) : data(other.data) { + // Копируем кэш, если он существует + // (Предполагается, что glm::vec* поддерживают копирование) + if (other.color_cache) { + color_cache = std::make_unique(*other.color_cache); + } + color_cache_valid = other.color_cache_valid; + cached_string_value = other.cached_string_value; + // std::cout << "value copy constructor called" << std::endl; // Отладка +} - if (s[0] == '#') { - std::string hex = s.substr(1); - if (hex.length() == 3) { - int r = (hex[0] - '0') * 16 + (hex[0] - '0'); - int g = (hex[1] - '0') * 16 + (hex[1] - '0'); - int b = (hex[2] - '0') * 16 + (hex[2] - '0'); - return glm::vec4(r/255.0f, g/255.0f, b/255.0f, 1.0f); - } else if (hex.length() == 4) { - int r = (hex[0] - '0') * 16 + (hex[0] - '0'); - int g = (hex[1] - '0') * 16 + (hex[1] - '0'); - int b = (hex[2] - '0') * 16 + (hex[2] - '0'); - int a = (hex[3] - '0') * 16 + (hex[3] - '0'); - return glm::vec4(r/255.0f, g/255.0f, b/255.0f, a/255.0f); - } else if (hex.length() == 6) { - int r = std::stoi(hex.substr(0, 2), nullptr, 16); - int g = std::stoi(hex.substr(2, 2), nullptr, 16); - int b = std::stoi(hex.substr(4, 2), nullptr, 16); - return glm::vec4(r/255.0f, g/255.0f, b/255.0f, 1.0f); - } else if (hex.length() == 8) { - int r = std::stoi(hex.substr(0, 2), nullptr, 16); - int g = std::stoi(hex.substr(2, 2), nullptr, 16); - int b = std::stoi(hex.substr(4, 2), nullptr, 16); - int a = std::stoi(hex.substr(6, 2), nullptr, 16); - return glm::vec4(r/255.0f, g/255.0f, b/255.0f, a/255.0f); +// Оператор присваивания копированием +value& value::operator=(const value& other) { + // std::cout << "value copy assignment called" << std::endl; // Отладка + if (this != &other) { + data = other.data; + if (other.color_cache) { + color_cache = std::make_unique(*other.color_cache); + } else { + color_cache.reset(); } + color_cache_valid = other.color_cache_valid; + cached_string_value = other.cached_string_value; } + return *this; +} - if (s.rfind("rgba(", 0) == 0) { - float r, g, b, a; - if (std::sscanf(s.c_str(), "rgba(%f,%f,%f,%f)", &r, &g, &b, &a) == 4) { - return glm::vec4(r/255.0f, g/255.0f, b/255.0f, a); - } - } +// Move конструктор +value::value(value&& other) noexcept + : data(std::move(other.data)) + , color_cache(std::move(other.color_cache)) + , color_cache_valid(other.color_cache_valid) + , cached_string_value(std::move(other.cached_string_value)) { + // std::cout << "value move constructor called" << std::endl; // Отладка + // other.color_cache_valid = false; // Не обязательно, other будет разрушен +} - // Именованные цвета (минимально) - if (s == "red") return glm::vec4(1,0,0,1); - if (s == "green") return glm::vec4(0,1,0,1); - if (s == "blue") return glm::vec4(0,0,1,1); - if (s == "white") return glm::vec4(1,1,1,1); - if (s == "black") return glm::vec4(0,0,0,1); - if (s == "transparent") return glm::vec4(0,0,0,0); +// Move оператор присваивания +value& value::operator=(value&& other) noexcept { + // std::cout << "value move assignment called" << std::endl; // Отладка + if (this != &other) { + data = std::move(other.data); + color_cache = std::move(other.color_cache); + color_cache_valid = other.color_cache_valid; + cached_string_value = std::move(other.cached_string_value); + // other.color_cache_valid = false; // Не обязательно + } + return *this; +} - return std::nullopt; +// Конструкторы от конкретных типов +value::value(std::monostate) : data(std::monostate{}) {} +value::value(const char* v) : data(std::string(v)) {} +value::value(std::string v) : data(std::move(v)) {} +value::value(int v) : data(static_cast(v)) {} +value::value(unsigned int v) : data(static_cast(v)) {} +value::value(int64_t v) : data(v) {} +value::value(double v) : data(v) {} +value::value(float v) : data(static_cast(v)) {} +value::value(bool v) : data(v) {} +value::value(glm::vec2 v) : data(v) {} +value::value(glm::vec3 v) : data(v) {} +value::value(glm::vec4 v) : data(v) {} + + +// --- Методы проверки типа --- + +bool value::isNull() const { + return std::holds_alternative(data); +} + +bool value::isString() const { + return std::holds_alternative(data); +} + +bool value::isNumber() const { + return isInteger() || isFloat(); +} + +bool value::isInteger() const { + return std::holds_alternative(data); } -// === Реализация методов === +bool value::isFloat() const { + return std::holds_alternative(data); +} -int64_t style::value::asInt(int64_t def) const { - if (std::holds_alternative(data)) { +bool value::isBool() const { + return std::holds_alternative(data); +} + +bool value::isVec2() const { + return std::holds_alternative(data); +} + +bool value::isVec3() const { + return std::holds_alternative(data); +} + +bool value::isVec4() const { + return std::holds_alternative(data); +} + +value::Type value::getType() const { + if (isNull()) return Type::Null; + if (isString()) return Type::String; + if (isInteger()) return Type::Integer; + if (isFloat()) return Type::Float; + if (isBool()) return Type::Boolean; + if (isVec2()) return Type::Vec2; + if (isVec3()) return Type::Vec3; + if (isVec4()) return Type::Vec4; + return Type::Null; // На всякий случай +} + + +// --- Методы получения значений (примеры) --- +// (У вас, вероятно, уже есть полные реализации этих методов) + +int64_t value::asInt(int64_t def) const { + if (isInteger()) { return std::get(data); } - if (std::holds_alternative(data)) { + if (isFloat()) { return static_cast(std::get(data)); } - if (std::holds_alternative(data)) { - return std::get(data) ? 1 : 0; - } - if (std::holds_alternative(data)) { - if (auto res = tryParse(std::get(data))) { - return *res; + if (isString()) { + if (auto parsed = parseInt(std::get(data))) { + return *parsed; } } return def; } -double style::value::asFloat(double def) const { - if (std::holds_alternative(data)) { +double value::asFloat(double def) const { + if (isFloat()) { return std::get(data); } - if (std::holds_alternative(data)) { + if (isInteger()) { return static_cast(std::get(data)); } - if (std::holds_alternative(data)) { - return std::get(data) ? 1.0 : 0.0; - } - if (std::holds_alternative(data)) { - if (auto res = tryParse(std::get(data))) { - return *res; + if (isString()) { + if (auto parsed = parseFloat(std::get(data))) { + return *parsed; } } return def; } -float style::value::asFloat(float def) const { - return static_cast(asFloat(static_cast(def))); +// ... (остальные методы as... и вспомогательные функции) + + +// --- Фабричные методы --- + +value value::fromString(const std::string& str) { + if (auto result = tryFromString(str)) { + // Теперь это должно работать, так как конструктор копирования определен + return *result; + } + return value(str); // Возврат как строка, если парсинг не удался } -bool style::value::asBool(bool def) const { - if (std::holds_alternative(data)) { - return std::get(data); +std::optional value::tryFromString(const std::string& str) { + if (str.empty()) return std::nullopt; + + // Попробуем разные типы по порядку + if (auto val = parseInt(str)) { + return value(*val); } - if (std::holds_alternative(data)) { - return std::get(data) != 0; + + if (auto val = parseFloat(str)) { + return value(*val); } - if (std::holds_alternative(data)) { - return std::get(data) != 0.0; + + if (auto val = parseBool(str)) { + return value(*val); } - if (std::holds_alternative(data)) { - auto& s = std::get(data); - return s == "true" || s == "1" || !s.empty(); + + // Проверим, может это цвет? + if (auto color = parseColor(str)) { + return value(*color); } - return def; + + // Если ничего не подошло, возвращаем nullopt + // или можно вернуть как строку: return value(str); + return std::nullopt; } -std::string style::value::asString(const std::string& def) const { - if (std::holds_alternative(data)) { - return std::get(data); - } - if (std::holds_alternative(data)) { - return std::to_string(std::get(data)); - } - if (std::holds_alternative(data)) { - return std::to_string(std::get(data)); + +// --- Вспомогательные функции для парсинга (примеры) --- +// (У вас, вероятно, уже есть реализации) + +std::optional value::parseInt(const std::string& str) { + return parsers::parseInt(str); +} + +std::optional value::parseFloat(const std::string& str) { + return parsers::parseFloat(str); +} + +std::optional value::parseBool(const std::string& str) { + return parsers::parseBool(str); +} + +std::optional value::parseColor(const std::string& str) { + return parsers::parseColor(str); +} + +// Реализации утилит из namespace parsers (если они в value.cpp) +namespace parsers { + std::optional parseInt(const std::string& str) { + if (str.empty()) return std::nullopt; + try { + size_t pos; + int64_t val = std::stoll(str, &pos); + return pos == str.length() ? std::make_optional(val) : std::nullopt; + } catch (...) { + return std::nullopt; + } } - if (std::holds_alternative(data)) { - return std::get(data) ? "true" : "false"; + + std::optional parseFloat(const std::string& str) { + if (str.empty()) return std::nullopt; + try { + size_t pos; + double val = std::stod(str, &pos); + return pos == str.length() ? std::make_optional(val) : std::nullopt; + } catch (...) { + return std::nullopt; + } } - if (std::holds_alternative(data)) { - auto v = std::get(data); - return "vec4(" + std::to_string(v.x) + "," + std::to_string(v.y) + "," + std::to_string(v.z) + "," + std::to_string(v.w) + ")"; + + std::optional parseBool(const std::string& str) { + if (str == "true" || str == "1") return true; + if (str == "false" || str == "0") return false; + return std::nullopt; } - if (std::holds_alternative(data)) { - auto v = std::get(data); - return "vec3(" + std::to_string(v.x) + "," + std::to_string(v.y) + "," + std::to_string(v.z) + ")"; + + std::optional parseColor(const std::string& str) { + if (str.empty() || str[0] != '#') return std::nullopt; + + std::string hex = str.substr(1); + + // Убираем пробелы (на всякий случай) + hex.erase(std::remove(hex.begin(), hex.end(), ' '), hex.end()); + + if (hex.length() == 3) { + // #RGB -> #RRGGBB + std::string expanded; + for (char c : hex) { + expanded += c; + expanded += c; + } + hex = expanded; + } + + if (hex.length() == 4) { + // #RGBA -> #RRGGBBAA + std::string expanded; + for (size_t i = 0; i < 4; ++i) { + expanded += hex[i]; + expanded += hex[i]; + } + hex = expanded; + } + + if (hex.length() == 6) { + // #RRGGBB + try { + unsigned int r = std::stoi(hex.substr(0, 2), nullptr, 16); + unsigned int g = std::stoi(hex.substr(2, 2), nullptr, 16); + unsigned int b = std::stoi(hex.substr(4, 2), nullptr, 16); + return glm::vec4(r / 255.0f, g / 255.0f, b / 255.0f, 1.0f); + } catch (...) { + return std::nullopt; + } + } + + if (hex.length() == 8) { + // #RRGGBBAA + try { + unsigned int r = std::stoi(hex.substr(0, 2), nullptr, 16); + unsigned int g = std::stoi(hex.substr(2, 2), nullptr, 16); + unsigned int b = std::stoi(hex.substr(4, 2), nullptr, 16); + unsigned int a = std::stoi(hex.substr(6, 2), nullptr, 16); + return glm::vec4(r / 255.0f, g / 255.0f, b / 255.0f, a / 255.0f); + } catch (...) { + return std::nullopt; + } + } + + return std::nullopt; } - if (std::holds_alternative(data)) { - auto v = std::get(data); - return "vec2(" + std::to_string(v.x) + "," + std::to_string(v.y) + ")"; +} // namespace parsers + + +// --- Остальные методы --- +// ... (toString, operator==, Hash и т.д. - реализации должны быть у вас) + +bool value::operator==(const value& other) const { + // Простая реализация сравнения + if (this->getType() != other.getType()) return false; + + // Сравнение конкретных значений + switch (this->getType()) { + case Type::Null: return other.isNull(); + case Type::String: return std::get(this->data) == std::get(other.data); + case Type::Integer: return std::get(this->data) == std::get(other.data); + case Type::Float: return std::abs(std::get(this->data) - std::get(other.data)) < 1e-10; // Точность? + case Type::Boolean: return std::get(this->data) == std::get(other.data); + case Type::Vec2: return std::get(this->data) == std::get(other.data); + case Type::Vec3: return std::get(this->data) == std::get(other.data); + case Type::Vec4: return std::get(this->data) == std::get(other.data); + default: return false; // Для не поддерживаемых типов } - return def; } -glm::vec2 style::value::asVec2(const glm::vec2& def) const { - if (std::holds_alternative(data)) { - return std::get(data); +bool value::operator!=(const value& other) const { + return !(*this == other); +} + +std::string value::typeName() const { + switch (getType()) { + case Type::Null: return "null"; + case Type::String: return "string"; + case Type::Integer: return "integer"; + case Type::Float: return "float"; + case Type::Boolean: return "boolean"; + case Type::Vec2: return "vec2"; + case Type::Vec3: return "vec3"; + case Type::Vec4: return "vec4"; + default: return "unknown"; } - if (std::holds_alternative(data)) { - std::string s = std::get(data); - float x, y; - if (std::sscanf(s.c_str(), "%f,%f", &x, &y) == 2) { - return glm::vec2(x, y); +} + +// Реализация хэш-функции +std::size_t value::Hash::operator()(const value& v) const { + // Базовая реализация хэша + switch (v.getType()) { + case Type::String: + return std::hash{}(v.asString()); + case Type::Integer: + return std::hash{}(v.asInt()); + case Type::Float: + return std::hash{}(v.asFloat(0.0)); + case Type::Boolean: + return std::hash{}(v.asBool()); + case Type::Vec2: { + auto vec = v.asVec2(); + return std::hash{}(vec.x) ^ (std::hash{}(vec.y) << 1); + } + case Type::Vec3: { + auto vec = v.asVec3(); + return std::hash{}(vec.x) ^ (std::hash{}(vec.y) << 1) ^ (std::hash{}(vec.z) << 2); } + case Type::Vec4: { + auto vec = v.asVec4(); + return std::hash{}(vec.x) ^ (std::hash{}(vec.y) << 1) ^ (std::hash{}(vec.z) << 2) ^ (std::hash{}(vec.w) << 3); + } + default: + return 0; } - return def; + return 0; } -glm::vec3 style::value::asVec3(const glm::vec3& def) const { - if (std::holds_alternative(data)) { - return std::get(data); - } - if (std::holds_alternative(data)) { - std::string s = std::get(data); - float x, y, z; - if (std::sscanf(s.c_str(), "%f,%f,%f", &x, &y, &z) == 3) { - return glm::vec3(x, y, z); - } + +bool value::asBool(bool def) const { + if (isBool()) { + return std::get(data); } + return def; } -glm::vec4 style::value::asVec4(const glm::vec4& def) const { - if (std::holds_alternative(data)) { - return std::get(data); +std::string value::asString(const std::string& def) const { + if (isString()) { + return std::get(data); } - if (std::holds_alternative(data)) { - std::string s = std::get(data); - float x, y, z, w; - if (std::sscanf(s.c_str(), "%f,%f,%f,%f", &x, &y, &z, &w) == 4) { - return glm::vec4(x, y, z, w); - } + + if (isNull()) { + return def; } + return def; } -glm::vec4 style::value::asColor(const glm::vec4& def) const { - if (std::holds_alternative(data)) { - auto v = std::get(data); - return glm::vec4(v.x/255.0f, v.y/255.0f, v.z/255.0f, v.w/255.0f); - } - if (std::holds_alternative(data)) { - if (auto col = parse_color(std::get(data))) { - return *col; - } +glm::vec2 value::asVec2(const glm::vec2& def) const { + if (isVec2()) { + return std::get(data); } + return def; } -std::string style::value::type_name() const { - if (std::holds_alternative(data)) return "null"; - if (std::holds_alternative(data)) return "string"; - if (std::holds_alternative(data)) return "int"; - if (std::holds_alternative(data)) return "float"; - if (std::holds_alternative(data)) return "bool"; - if (std::holds_alternative(data)) return "vec2"; - if (std::holds_alternative(data)) return "vec3"; - if (std::holds_alternative(data)) return "vec4"; - return "unknown"; +glm::vec3 value::asVec3(const glm::vec3& def) const { + if (isVec3()) { + return std::get(data); + } + + if (isVec2()) { + auto v2 = std::get(data); + return glm::vec3(v2, 0.0f); + } + + return def; } -std::string style::value::toString() const { - if (std::holds_alternative(data)) return std::get(data); - if (std::holds_alternative(data)) return std::to_string(std::get(data)); - if (std::holds_alternative(data)) return std::to_string(std::get(data)); - if (std::holds_alternative(data)) return std::get(data) ? "true" : "false"; - if (std::holds_alternative(data)) { - auto v = std::get(data); - return std::to_string(v.x) + "," + std::to_string(v.y); +glm::vec4 value::asVec4(const glm::vec4& def) const { + if (isVec4()) { + return std::get(data); } - if (std::holds_alternative(data)) { - auto v = std::get(data); - return std::to_string(v.x) + "," + std::to_string(v.y) + "," + std::to_string(v.z); + // Vec3 -> Vec4 (w = 1.0) + if (isVec3()) { + auto v3 = std::get(data); + return glm::vec4(v3, 1.0f); } - if (std::holds_alternative(data)) { - auto v = std::get(data); - return std::to_string(v.x) + "," + std::to_string(v.y) + "," + std::to_string(v.z) + "," + std::to_string(v.w); + // Vec2 -> Vec4 (z=0, w=1) + if (isVec2()) { + auto v2 = std::get(data); + return glm::vec4(v2, 0.0f, 1.0f); } - return "null"; + + return def; } + } \ No newline at end of file diff --git a/src/graphics/ui/style/value.h b/src/graphics/ui/style/value.h index 68b28fd70..ae97da1dd 100644 --- a/src/graphics/ui/style/value.h +++ b/src/graphics/ui/style/value.h @@ -1,3 +1,4 @@ +// value.h (обновленная версия) #pragma once #include @@ -5,19 +6,28 @@ #include #include #include -#include -#include +#include +#include +#include namespace style { -/** - * Универсальный тип для хранения динамических значений. - * Поддерживает: строки, числа, bool, vec2/3/4, цвета. - */ class value { +public: + enum class Type { + Null, + String, + Integer, + Float, + Boolean, + Vec2, + Vec3, + Vec4 + }; + private: std::variant< - std::monostate, // null + std::monostate, std::string, int64_t, double, @@ -27,77 +37,82 @@ class value { glm::vec4 > data; - // Вспомогательная функция для парсинга строки в число - template - static std::optional tryParse(const std::string& str); + mutable std::unique_ptr color_cache; + mutable bool color_cache_valid = false; + mutable std::string cached_string_value; + + static std::optional parseInt(const std::string& str); + static std::optional parseFloat(const std::string& str); + static std::optional parseBool(const std::string& str); + static std::optional parseColor(const std::string& str); public: // Конструкторы - value() = default; - value(std::monostate) {} - value(const char* v) : data(std::string(v)) {} - value(std::string v) : data(std::move(v)) {} - value(int v) : data(static_cast(v)) {} - value(unsigned int v) : data(static_cast(v)) {} - value(int64_t v) : data(v) {} - value(double v) : data(v) {} - value(float v) : data(static_cast(v)) {} - value(bool v) : data(v) {} - value(glm::vec2 v) : data(v) {} - value(glm::vec3 v) : data(v) {} - value(glm::vec4 v) : data(v) {} - - // Явные преобразования - bool isNull() const { return std::holds_alternative(data); } - bool isString() const { return std::holds_alternative(data); } - bool isNumber() const { return isInteger() || isFloat(); } - bool isInteger() const { return std::holds_alternative(data); } - bool isFloat() const { return std::holds_alternative(data); } - bool isBool() const { return std::holds_alternative(data); } - bool isVec2() const { return std::holds_alternative(data); } - bool isVec3() const { return std::holds_alternative(data); } - bool isVec4() const { return std::holds_alternative(data); } - - // Получение значений с преобразованием + value(); + value(const value& other); + value& operator=(const value& other); + value(value&& other) noexcept; + value& operator=(value&& other) noexcept; + + value(std::monostate); + value(const char* v); + value(std::string v); + value(int v); + value(unsigned int v); + value(int64_t v); + value(double v); + value(float v); + value(bool v); + value(glm::vec2 v); + value(glm::vec3 v); + value(glm::vec4 v); + + // Фабричные методы + static value fromString(const std::string& str); + static std::optional tryFromString(const std::string& str); + + // Тип проверки + bool isNull() const; + bool isString() const; + bool isNumber() const; + bool isInteger() const; + bool isFloat() const; + bool isBool() const; + bool isVec2() const; + bool isVec3() const; + bool isVec4() const; + + Type getType() const; + + // Получение значений int64_t asInt(int64_t def = 0) const; double asFloat(double def = 0.0) const; float asFloat(float def = 0.0f) const; bool asBool(bool def = false) const; std::string asString(const std::string& def = "") const; - glm::vec2 asVec2(const glm::vec2& def = {}) const; glm::vec3 asVec3(const glm::vec3& def = {}) const; glm::vec4 asVec4(const glm::vec4& def = {}) const; - - // Специальный: цвет (поддерживает #RGB, #RGBA, #RRGGBB, rgba()) - glm::vec4 asColor(const glm::vec4& def = {0,0,0,0}) const; + glm::vec4 asColor(const glm::vec4& def = {0,0,0,1}) const; // Для отладки - std::string type_name() const; + std::string typeName() const; std::string toString() const; + + // Операторы сравнения + bool operator==(const value& other) const; + bool operator!=(const value& other) const; + + struct Hash { + std::size_t operator()(const value& v) const; + }; }; -// Реализация шаблонов -template -std::optional value::tryParse(const std::string& str) { - if (str.empty()) return std::nullopt; - try { - size_t pos; - if constexpr (std::is_same_v) { - int64_t val = std::stoll(str, &pos); - return pos == str.length() ? std::make_optional(val) : std::nullopt; - } else if constexpr (std::is_same_v) { - double val = std::stod(str, &pos); - return pos == str.length() ? std::make_optional(val) : std::nullopt; - } else if constexpr (std::is_same_v) { - if (str == "true" || str == "1") return true; - if (str == "false" || str == "0") return false; - return std::nullopt; - } - } catch (...) { - return std::nullopt; - } - return std::nullopt; +namespace parsers { + std::optional parseInt(const std::string& str); + std::optional parseFloat(const std::string& str); + std::optional parseBool(const std::string& str); + std::optional parseColor(const std::string& str); } -} \ No newline at end of file +} // namespace style \ No newline at end of file From f69384f7f6401ad5ac1447a5d304b65d2cb5f51e Mon Sep 17 00:00:00 2001 From: GHOST11111100 Date: Sun, 3 Aug 2025 18:45:17 +0300 Subject: [PATCH 05/11] cleanup comments --- src/graphics/ui/style/StyleContext.cpp | 34 ++-- src/graphics/ui/style/StyleContext.h | 219 ++++++++++----------- src/graphics/ui/style/Stylesheet.cpp | 30 ++- src/graphics/ui/style/Stylesheet.h | 19 +- src/graphics/ui/style/StylesheetParser.cpp | 88 ++++----- src/graphics/ui/style/StylesheetParser.h | 20 +- src/graphics/ui/style/value.cpp | 94 ++++----- src/graphics/ui/style/value.h | 204 +++++++++---------- 8 files changed, 340 insertions(+), 368 deletions(-) diff --git a/src/graphics/ui/style/StyleContext.cpp b/src/graphics/ui/style/StyleContext.cpp index 80e011e60..a1bf39a0c 100644 --- a/src/graphics/ui/style/StyleContext.cpp +++ b/src/graphics/ui/style/StyleContext.cpp @@ -5,7 +5,7 @@ namespace style { -// Реализация конструкторов + StyleContext::StyleContext() : tag_("node") {} StyleContext::StyleContext(const std::string& tag) @@ -33,7 +33,7 @@ StyleContext::StyleContext(gui::UINode& node) updateFromUINode(); } -// Сеттеры + void StyleContext::setTag(const std::string& tag) { tag_ = tag; selector_cache_valid_ = false; @@ -44,7 +44,7 @@ void StyleContext::setID(const std::string& id) { selector_cache_valid_ = false; } -// Работа с классами + void StyleContext::addClass(const std::string& className) { classes_.insert(className); selector_cache_valid_ = false; @@ -72,7 +72,7 @@ void StyleContext::clearClasses() { selector_cache_valid_ = false; } -// Работа с состояниями + void StyleContext::setState(State state, bool enabled) { if (enabled) { states_.insert(state); @@ -89,7 +89,7 @@ void StyleContext::clearStates() { states_.clear(); } -// Синхронизация с UINode + void StyleContext::syncWithUINode() { if (ui_node_) { updateFromUINode(); @@ -99,13 +99,13 @@ void StyleContext::syncWithUINode() { void StyleContext::updateFromUINode() { if (!ui_node_) return; - // Обновляем ID из UINode + id_ = ui_node_->getId(); - // Обновляем классы из classname + std::string classname = ui_node_->getClassname(); if (!classname.empty()) { - // Разделяем classname на отдельные классы (предполагаем разделение пробелами) + std::istringstream iss(classname); std::string cls; classes_.clear(); @@ -116,7 +116,7 @@ void StyleContext::updateFromUINode() { } } - // Обновляем состояния на основе свойств UINode + states_.clear(); if (!ui_node_->isEnabled()) { states_.insert(State::Disabled); @@ -134,7 +134,7 @@ void StyleContext::updateFromUINode() { selector_cache_valid_ = false; } -// Утилиты + std::string StyleContext::getSelectorString() const { if (selector_cache_valid_) { return selector_cache_; @@ -142,17 +142,17 @@ std::string StyleContext::getSelectorString() const { std::ostringstream oss; - // Добавляем тег + if (!tag_.empty()) { oss << tag_; } - // Добавляем ID + if (!id_.empty()) { oss << "#" << id_; } - // Добавляем классы + for (const auto& cls : classes_) { oss << "." << cls; } @@ -188,16 +188,16 @@ bool StyleContext::operator==(const StyleContext& other) const { std::size_t StyleContext::Hash::operator()(const StyleContext& ctx) const { std::size_t h1 = std::hash{}(ctx.tag_); std::size_t h2 = std::hash{}(ctx.id_); - // Простая комбинация хэшей + return h1 ^ (h2 << 1); } void StyleContext::updateSelectorCache() const { - // Реализация кэширования селектора + selector_cache_valid_ = false; } -// Фабричные методы + namespace context { std::unique_ptr create(const std::string& tag) { return std::make_unique(tag); @@ -223,4 +223,4 @@ namespace context { } } -} // namespace style \ No newline at end of file +} \ No newline at end of file diff --git a/src/graphics/ui/style/StyleContext.h b/src/graphics/ui/style/StyleContext.h index 636cd739e..02e5e1ad1 100644 --- a/src/graphics/ui/style/StyleContext.h +++ b/src/graphics/ui/style/StyleContext.h @@ -1,128 +1,125 @@ -// StyleContext.h + #pragma once +#include +#include #include -#include #include -#include -#include +#include -// Предварительные объявления namespace style { class Stylesheet; class ComputedStyle; } namespace gui { - class UINode; // Из предоставленного кода + class UINode; } namespace style { -/** - * Контекст для вычисления стилей. - * Представляет элемент UI с его характеристиками для применения стилей. - */ -class StyleContext { -public: - // Типы состояний элемента - enum class State { - Normal, - Hover, - Active, - Disabled, - Focused - }; - -private: - // Базовые свойства элемента для сопоставления с селекторами - std::string tag_; // Тег элемента (например, "button", "panel") - std::unordered_set classes_; // CSS-классы - std::string id_; // Уникальный идентификатор - std::unordered_set states_; // Текущие состояния - - // Ссылка на родительский контекст (для наследования) - const StyleContext* parent_ = nullptr; - - // Указатель на соответствующий UINode (для интеграции) - gui::UINode* ui_node_ = nullptr; - -public: - // Конструкторы - StyleContext(); - explicit StyleContext(const std::string& tag); - StyleContext(const std::string& tag, const std::string& id); - StyleContext(const std::string& tag, const std::vector& classes); - StyleContext(const std::string& tag, const std::string& id, const std::vector& classes); - - // Конструктор для интеграции с UINode - explicit StyleContext(gui::UINode& node); - - // Геттеры - const std::string& getTag() const { return tag_; } - const std::string& getID() const { return id_; } - const std::unordered_set& getClasses() const { return classes_; } - const std::unordered_set& getStates() const { return states_; } - const StyleContext* getParent() const { return parent_; } - gui::UINode* getUINode() const { return ui_node_; } - - // Сеттеры - void setTag(const std::string& tag); - void setID(const std::string& id); - void setParent(const StyleContext* parent) { parent_ = parent; } - void setUINode(gui::UINode* node) { ui_node_ = node; } - - // Работа с классами - void addClass(const std::string& className); - void removeClass(const std::string& className); - bool hasClass(const std::string& className) const; - void setClasses(const std::vector& classes); - void clearClasses(); - - // Работа с состояниями - void setState(State state, bool enabled = true); - bool hasState(State state) const; - void clearStates(); - - // Методы для синхронизации с UINode - void syncWithUINode(); - void updateFromUINode(); - - // Утилиты - std::string getSelectorString() const; - bool hasID(const std::string& id) const; - - // Создание копии контекста - std::unique_ptr clone() const; - - // Операторы сравнения - bool operator==(const StyleContext& other) const; - bool operator!=(const StyleContext& other) const { return !(*this == other); } - - // Хэш для использования в контейнерах - struct Hash { - std::size_t operator()(const StyleContext& ctx) const; + class StyleContext { + public: + enum class State { Normal, Hover, Active, Disabled, Focused }; + private: + std::string tag_; + std::unordered_set classes_; + std::string id_; + std::unordered_set states_; + + const StyleContext* parent_ = nullptr; + + gui::UINode* ui_node_ = nullptr; + public: + StyleContext(); + explicit StyleContext(const std::string& tag); + StyleContext(const std::string& tag, const std::string& id); + StyleContext( + const std::string& tag, const std::vector& classes + ); + StyleContext( + const std::string& tag, + const std::string& id, + const std::vector& classes + ); + + explicit StyleContext(gui::UINode& node); + + const std::string& getTag() const { + return tag_; + } + const std::string& getID() const { + return id_; + } + const std::unordered_set& getClasses() const { + return classes_; + } + const std::unordered_set& getStates() const { + return states_; + } + const StyleContext* getParent() const { + return parent_; + } + gui::UINode* getUINode() const { + return ui_node_; + } + + void setTag(const std::string& tag); + void setID(const std::string& id); + void setParent(const StyleContext* parent) { + parent_ = parent; + } + void setUINode(gui::UINode* node) { + ui_node_ = node; + } + + void addClass(const std::string& className); + void removeClass(const std::string& className); + bool hasClass(const std::string& className) const; + void setClasses(const std::vector& classes); + void clearClasses(); + + void setState(State state, bool enabled = true); + bool hasState(State state) const; + void clearStates(); + + void syncWithUINode(); + void updateFromUINode(); + + std::string getSelectorString() const; + bool hasID(const std::string& id) const; + + std::unique_ptr clone() const; + + bool operator==(const StyleContext& other) const; + bool operator!=(const StyleContext& other) const { + return !(*this == other); + } + + struct Hash { + std::size_t operator()(const StyleContext& ctx) const; + }; + private: + void updateSelectorCache() const; + mutable std::string selector_cache_; + mutable bool selector_cache_valid_ = false; }; -private: - // Вспомогательные методы - void updateSelectorCache() const; - mutable std::string selector_cache_; - mutable bool selector_cache_valid_ = false; -}; - -// Фабричные методы для удобного создания контекстов -namespace context { - std::unique_ptr create(const std::string& tag); - std::unique_ptr create(const std::string& tag, const std::string& id); - std::unique_ptr create(const std::string& tag, - const std::vector& classes); - std::unique_ptr create(const std::string& tag, - const std::string& id, - const std::vector& classes); - - // Для интеграции с UINode - std::unique_ptr fromUINode(gui::UINode& node); -} - -} // namespace style \ No newline at end of file + namespace context { + std::unique_ptr create(const std::string& tag); + std::unique_ptr create( + const std::string& tag, const std::string& id + ); + std::unique_ptr create( + const std::string& tag, const std::vector& classes + ); + std::unique_ptr create( + const std::string& tag, + const std::string& id, + const std::vector& classes + ); + + std::unique_ptr fromUINode(gui::UINode& node); + } + +} \ No newline at end of file diff --git a/src/graphics/ui/style/Stylesheet.cpp b/src/graphics/ui/style/Stylesheet.cpp index 3e26f247b..462b58561 100644 --- a/src/graphics/ui/style/Stylesheet.cpp +++ b/src/graphics/ui/style/Stylesheet.cpp @@ -1,4 +1,4 @@ -// Stylesheet.cpp + #include "Stylesheet.h" #include "StyleContext.h" #include @@ -7,7 +7,6 @@ namespace style { -// Реализация PropertyID функций PropertyID getPropertyID(const std::string& name) { static const std::unordered_map property_map = { {"color", PropertyID::Color}, @@ -86,10 +85,10 @@ std::string getPropertyName(PropertyID id) { return (it != property_names.end()) ? it->second : "unknown"; } -// Реализация Selector + Selector::Selector(Type type, std::string value) : type_(type), value_(std::move(value)) { - // Вычисляем специфичность (упрощенная версия CSS-специфичности) + switch (type_) { case Type::Universal: specificity_ = 0; break; case Type::Tag: specificity_ = 1; break; @@ -128,7 +127,7 @@ std::size_t Selector::Hash::operator()(const Selector& s) const { std::hash{}(s.value_); } -// Реализация StyleRule + StyleRule::StyleRule(std::vector selectors_, std::unordered_map declarations_) : selectors(std::move(selectors_)), declarations(std::move(declarations_)) { @@ -144,7 +143,7 @@ int StyleRule::getSpecificity() const { } bool StyleRule::matches(const StyleContext& context) const { - // Пока поддерживаем только простые селекторы (без комбинаторов) + for (const auto& selector : selectors) { if (selector.matches(context)) { return true; @@ -153,7 +152,7 @@ bool StyleRule::matches(const StyleContext& context) const { return false; } -// Реализация Stylesheet + void Stylesheet::addRule(const StyleRule& rule) { rules_.push_back(rule); } @@ -166,18 +165,15 @@ void Stylesheet::addRule(const std::vector& selectors, std::vector Stylesheet::getMatchingRules(const StyleContext& context) const { std::vector matching_rules; - // Если есть индекс, используем его для ускорения + if (!tag_index_.empty() || !class_index_.empty() || !id_index_.empty()) { - // Используем индекс для быстрого поиска std::unordered_set candidate_indices; - // Проверяем по тегу auto tag_it = tag_index_.find(context.getTag()); if (tag_it != tag_index_.end()) { candidate_indices.insert(tag_it->second.begin(), tag_it->second.end()); } - // Проверяем по классам for (const auto& cls : context.getClasses()) { auto class_it = class_index_.find(cls); if (class_it != class_index_.end()) { @@ -185,20 +181,18 @@ std::vector Stylesheet::getMatchingRules(const StyleContext& c } } - // Проверяем по ID auto id_it = id_index_.find(context.getID()); if (id_it != id_index_.end()) { candidate_indices.insert(id_it->second.begin(), id_it->second.end()); } - // Проверяем кандидатов for (size_t index : candidate_indices) { if (index < rules_.size() && rules_[index].matches(context)) { matching_rules.push_back(&rules_[index]); } } } else { - // Линейный поиск (если индекс не построен) + for (const auto& rule : rules_) { if (rule.matches(context)) { matching_rules.push_back(&rule); @@ -243,14 +237,14 @@ void Stylesheet::buildIndex() { id_index_[selector.getValue()].push_back(i); break; case Selector::Type::Universal: - // Универсальный селектор не индексируем + break; } } } } -// Фабричные методы для селекторов + Selector Stylesheet::createTagSelector(const std::string& tag) { return Selector(Selector::Type::Tag, tag); } @@ -267,7 +261,7 @@ Selector Stylesheet::createUniversalSelector() { return Selector(Selector::Type::Universal, "*"); } -// Утилиты для селекторов + namespace selectors { Selector tag(const std::string& name) { return Selector(Selector::Type::Tag, name); @@ -286,4 +280,4 @@ namespace selectors { } } -} // namespace style \ No newline at end of file +} \ No newline at end of file diff --git a/src/graphics/ui/style/Stylesheet.h b/src/graphics/ui/style/Stylesheet.h index cee9ac0e0..6c425cf9f 100644 --- a/src/graphics/ui/style/Stylesheet.h +++ b/src/graphics/ui/style/Stylesheet.h @@ -9,12 +9,10 @@ namespace style { -// Предварительные объявления class StyleContext; class ComputedStyle; class Selector; -// Идентификаторы свойств для оптимизации enum class PropertyID : uint32_t { Unknown = 0, Color, @@ -48,7 +46,6 @@ enum class PropertyID : uint32_t { FontSize, FontWeight, TextAlign, - // ... добавь больше по мере необходимости }; // Получение PropertyID по строке @@ -68,7 +65,7 @@ class Selector { private: Type type_; std::string value_; - int specificity_ = 0; // CSS-специфичность для каскада + int specificity_ = 0; public: Selector(Type type, std::string value); @@ -89,7 +86,6 @@ class Selector { }; }; -// Правило стиля struct StyleRule { std::vector selectors; std::unordered_map declarations; @@ -106,7 +102,6 @@ struct StyleRule { bool matches(const StyleContext& context) const; }; -// Основной класс таблицы стилей class Stylesheet { private: std::vector rules_; @@ -121,22 +116,18 @@ class Stylesheet { public: Stylesheet() = default; - - // Добавление правила + void addRule(const StyleRule& rule); void addRule(const std::vector& selectors, const std::unordered_map& declarations); - // Парсинг CSS-подобного правила из строки bool parseAndAddRule(const std::string& css_string); - // Получение всех подходящих правил для контекста std::vector getMatchingRules(const StyleContext& context) const; // Сортировка правил по специфичности (для каскада) void sortRulesBySpecificity(); - // Очистка таблицы стилей void clear(); // Получение количества правил @@ -160,15 +151,13 @@ class Stylesheet { std::shared_ptr getOrCreateSelector(Selector::Type type, const std::string& value) const; }; -// Утилиты для работы с селекторами namespace selectors { Selector tag(const std::string& name); Selector cls(const std::string& name); // class Selector id(const std::string& name); Selector universal(); - - // Комбинирование селекторов + std::vector combine(const std::vector& a, const std::vector& b); } -} // namespace style \ No newline at end of file +} \ No newline at end of file diff --git a/src/graphics/ui/style/StylesheetParser.cpp b/src/graphics/ui/style/StylesheetParser.cpp index 9172b53ce..85f315091 100644 --- a/src/graphics/ui/style/StylesheetParser.cpp +++ b/src/graphics/ui/style/StylesheetParser.cpp @@ -13,7 +13,7 @@ StylesheetParser::ParseResult StylesheetParser::parse(const std::string& css_sou column_ = 1; ParseResult result; - result.rules.reserve(16); // Резервируем место для оптимизации + result.rules.reserve(16); try { while (position_ < source_.length()) { @@ -22,7 +22,7 @@ StylesheetParser::ParseResult StylesheetParser::parse(const std::string& css_sou if (position_ >= source_.length()) break; - // Парсим селекторы + auto selectors = parseSelectorGroup(); if (selectors.empty()) { return makeError("Expected selector"); @@ -30,22 +30,22 @@ StylesheetParser::ParseResult StylesheetParser::parse(const std::string& css_sou skipWhitespace(); - // Ожидаем начало блока деклараций + if (!match('{')) { return makeError("Expected '{' after selector"); } - // Парсим блок деклараций + auto declarations = parseDeclarationBlock(); - // Ожидаем конец блока + if (!match('}')) { return makeError("Expected '}' after declarations"); } - // ВАЖНО: нужно переместить позицию после найденной скобки + position_++; - // Создаем правило + if (!declarations.empty()) { result.rules.emplace_back(selectors, declarations); } @@ -71,24 +71,24 @@ std::vector StylesheetParser::parseSelectorGroup() { if (position_ >= source_.length()) break; - // Проверяем, не достигли ли мы конца селектора или блока + if (peek() == '{' || peek() == '}' || peek() == ';') { break; } - // Парсим простой селектор + Selector selector = parseSimpleSelector(); - // Добавляем ВСЕ селекторы, включая универсальные * + selectors.push_back(selector); skipWhitespace(); - // Если следующий символ не ',', значит группа закончилась + if (position_ >= source_.length() || source_[position_] != ',') { break; } - consume(); // Пропускаем ',' + consume(); skipWhitespace(); } @@ -108,19 +108,19 @@ Selector StylesheetParser::parseSimpleSelector() { return Selector(Selector::Type::Universal, "*"); case '#': { - consume(); // Пропускаем '#' + consume(); std::string id = consumeIdentifier(); if (id.empty()) { - return Selector(Selector::Type::Universal, "*"); // fallback + return Selector(Selector::Type::Universal, "*"); } return Selector(Selector::Type::ID, id); } case '.': { - consume(); // Пропускаем '.' + consume(); std::string className = consumeIdentifier(); if (className.empty()) { - return Selector(Selector::Type::Universal, "*"); // fallback + return Selector(Selector::Type::Universal, "*"); } return Selector(Selector::Type::Class, className); } @@ -130,7 +130,7 @@ Selector StylesheetParser::parseSimpleSelector() { std::string tag = consumeIdentifier(); return Selector(Selector::Type::Tag, tag); } - // Если символ не подходит ни под один тип, возвращаем универсальный селектор + return Selector(Selector::Type::Universal, "*"); } } @@ -144,7 +144,7 @@ std::unordered_map StylesheetParser::parseDeclarationBlock() if (position_ >= source_.length() || peek() == '}') break; - // Парсим одну декларацию + auto declaration = parseDeclaration(); if (declaration.first != PropertyID::Unknown) { declarations[declaration.first] = declaration.second; @@ -152,7 +152,7 @@ std::unordered_map StylesheetParser::parseDeclarationBlock() skipWhitespace(); - // Пропускаем ';' + if (position_ < source_.length() && peek() == ';') { consume(); } @@ -166,7 +166,7 @@ std::unordered_map StylesheetParser::parseDeclarationBlock() std::pair StylesheetParser::parseDeclaration() { skipWhitespace(); - // Парсим имя свойства + std::string property_name = consumeIdentifier(); if (property_name.empty()) { return {PropertyID::Unknown, value()}; @@ -174,39 +174,39 @@ std::pair StylesheetParser::parseDeclaration() { skipWhitespace(); - // Ожидаем ':' + if (!match(':')) { - skipUntil(';'); // Пропускаем до конца декларации + skipUntil(';'); return {PropertyID::Unknown, value()}; } skipWhitespace(); - // Парсим значение + std::string value_str; while (position_ < source_.length() && peek() != ';' && peek() != '}') { if (!isWhitespace(peek())) { value_str += consume(); } else { - // Сохраняем пробелы в значении, если они важны + value_str += consume(); } } - // Преобразуем в PropertyID + PropertyID prop_id = getPropertyID(property_name); if (prop_id == PropertyID::Unknown) { return {PropertyID::Unknown, value()}; } - // Преобразуем строку значения в value + value val = value::fromString(value_str); return {prop_id, val}; } -// Добавьте этот вспомогательный метод + void StylesheetParser::skipUntil(char delimiter) { while (position_ < source_.length() && peek() != delimiter && peek() != '}') { consume(); @@ -216,7 +216,7 @@ void StylesheetParser::skipUntil(char delimiter) { } } -// Вспомогательные методы + void StylesheetParser::skipWhitespace() { while (position_ < source_.length() && isWhitespace(peek())) { if (peek() == '\n') { @@ -232,7 +232,7 @@ void StylesheetParser::skipWhitespace() { void StylesheetParser::skipComments() { while (position_ < source_.length() - 1) { if (source_[position_] == '/' && source_[position_ + 1] == '*') { - // Найден комментарий /* */ + position_ += 2; column_ += 2; @@ -242,24 +242,24 @@ void StylesheetParser::skipComments() { column_ += 2; break; } - if (source_[position_] == '\n') { // Исправлено: source_[position_] вместо peek() + if (source_[position_] == '\n') { line_++; column_ = 1; } else { column_++; } - position_++; // column_ должен обновляться до position_++ + position_++; } } else if (source_[position_] == '/' && source_[position_ + 1] == '/') { - // Найден комментарий // + position_ += 2; column_ += 2; - while (position_ < source_.length() && source_[position_] != '\n') { // Исправлено + while (position_ < source_.length() && source_[position_] != '\n') { position_++; column_++; } - // Пропускаем символ новой строки, если он есть + if (position_ < source_.length() && source_[position_] == '\n') { position_++; line_++; @@ -334,12 +334,12 @@ std::string StylesheetParser::consumeString() { char quote = peek(); if (quote != '"' && quote != '\'') return ""; - consume(); // Пропускаем открывающую кавычку + consume(); std::string result; while (position_ < source_.length() && peek() != quote) { if (peek() == '\\') { - consume(); // Пропускаем '\' + consume(); if (position_ < source_.length()) { result += consume(); } @@ -349,7 +349,7 @@ std::string StylesheetParser::consumeString() { } if (position_ < source_.length() && peek() == quote) { - consume(); // Пропускаем закрывающую кавычку + consume(); } return result; @@ -361,7 +361,7 @@ std::string StylesheetParser::consumeNumber() { }); } -// Статические утилиты + std::vector StylesheetParser::splitSelectors(const std::string& selector_string) { std::vector selectors; std::string current; @@ -396,7 +396,7 @@ std::pair StylesheetParser::splitDeclaration(const std std::string property = declaration.substr(0, colon_pos); std::string value = declaration.substr(colon_pos + 1); - // Убираем пробелы + property.erase(0, property.find_first_not_of(" \t\r\n")); property.erase(property.find_last_not_of(" \t\r\n") + 1); @@ -406,7 +406,7 @@ std::pair StylesheetParser::splitDeclaration(const std return {property, value}; } -// Вспомогательные функции + bool StylesheetParser::isWhitespace(char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r'; } @@ -423,7 +423,7 @@ bool StylesheetParser::isNumberChar(char c) { return std::isdigit(c) || c == '.' || c == '-' || c == '+'; } -// Обработка ошибок + StylesheetParser::ParseResult StylesheetParser::makeError(const std::string& message) const { ParseResult result; result.success = false; @@ -436,7 +436,7 @@ std::string StylesheetParser::getCurrentPosition() const { return "line " + std::to_string(line_) + ", column " + std::to_string(column_); } -// Утилиты для внешнего использования + namespace parser { StylesheetParser::ParseResult parseCSS(const std::string& css_string) { StylesheetParser parser; @@ -456,9 +456,9 @@ namespace parser { } } -// Добавим метод в Stylesheet для удобства + bool Stylesheet::parseAndAddRule(const std::string& css_string) { return parser::parseAndAddToStylesheet(*this, css_string); } -} // namespace style \ No newline at end of file +} \ No newline at end of file diff --git a/src/graphics/ui/style/StylesheetParser.h b/src/graphics/ui/style/StylesheetParser.h index 96d2e3f32..5782cc7a9 100644 --- a/src/graphics/ui/style/StylesheetParser.h +++ b/src/graphics/ui/style/StylesheetParser.h @@ -29,19 +29,19 @@ class StylesheetParser { public: StylesheetParser() = default; - // Основной метод парсинга + ParseResult parse(const std::string& css_source); - // Парсинг отдельных компонентов + std::vector parseSelector(const std::string& selector_string); std::unordered_map parseDeclarations(const std::string& declarations_string); - // Утилиты для парсинга + static std::vector splitSelectors(const std::string& selector_string); static std::pair splitDeclaration(const std::string& declaration); private: - // Вспомогательные методы для парсинга + void skipUntil(char delimiter); void skipWhitespace(); void skipComments(); @@ -55,29 +55,29 @@ class StylesheetParser { std::string consumeString(); std::string consumeNumber(); - // Парсинг конкретных элементов + std::vector parseSelectorGroup(); Selector parseSimpleSelector(); std::unordered_map parseDeclarationBlock(); std::pair parseDeclaration(); - // Обработка ошибок + ParseResult makeError(const std::string& message) const; std::string getCurrentPosition() const; - // Утилиты + static bool isWhitespace(char c); static bool isIdentifierStart(char c); static bool isIdentifierChar(char c); static bool isNumberChar(char c); }; -// Утилиты для работы с парсером + namespace parser { StylesheetParser::ParseResult parseCSS(const std::string& css_string); - // Быстрое добавление CSS в Stylesheet + bool parseAndAddToStylesheet(Stylesheet& stylesheet, const std::string& css_string); } -} // namespace style \ No newline at end of file +} \ No newline at end of file diff --git a/src/graphics/ui/style/value.cpp b/src/graphics/ui/style/value.cpp index e93916613..26b87261d 100644 --- a/src/graphics/ui/style/value.cpp +++ b/src/graphics/ui/style/value.cpp @@ -8,26 +8,26 @@ namespace style { -// --- Конструкторы --- -// Конструктор по умолчанию -value::value() = default; // Определение, если в .h только декларация -// Конструктор копирования + +value::value() = default; + + value::value(const value& other) : data(other.data) { - // Копируем кэш, если он существует - // (Предполагается, что glm::vec* поддерживают копирование) + + if (other.color_cache) { color_cache = std::make_unique(*other.color_cache); } color_cache_valid = other.color_cache_valid; cached_string_value = other.cached_string_value; - // std::cout << "value copy constructor called" << std::endl; // Отладка + } -// Оператор присваивания копированием + value& value::operator=(const value& other) { - // std::cout << "value copy assignment called" << std::endl; // Отладка + if (this != &other) { data = other.data; if (other.color_cache) { @@ -41,30 +41,30 @@ value& value::operator=(const value& other) { return *this; } -// Move конструктор + value::value(value&& other) noexcept : data(std::move(other.data)) , color_cache(std::move(other.color_cache)) , color_cache_valid(other.color_cache_valid) , cached_string_value(std::move(other.cached_string_value)) { - // std::cout << "value move constructor called" << std::endl; // Отладка - // other.color_cache_valid = false; // Не обязательно, other будет разрушен + + } -// Move оператор присваивания + value& value::operator=(value&& other) noexcept { - // std::cout << "value move assignment called" << std::endl; // Отладка + if (this != &other) { data = std::move(other.data); color_cache = std::move(other.color_cache); color_cache_valid = other.color_cache_valid; cached_string_value = std::move(other.cached_string_value); - // other.color_cache_valid = false; // Не обязательно + } return *this; } -// Конструкторы от конкретных типов + value::value(std::monostate) : data(std::monostate{}) {} value::value(const char* v) : data(std::string(v)) {} value::value(std::string v) : data(std::move(v)) {} @@ -79,7 +79,7 @@ value::value(glm::vec3 v) : data(v) {} value::value(glm::vec4 v) : data(v) {} -// --- Методы проверки типа --- + bool value::isNull() const { return std::holds_alternative(data); @@ -126,12 +126,12 @@ value::Type value::getType() const { if (isVec2()) return Type::Vec2; if (isVec3()) return Type::Vec3; if (isVec4()) return Type::Vec4; - return Type::Null; // На всякий случай + return Type::Null; } -// --- Методы получения значений (примеры) --- -// (У вас, вероятно, уже есть полные реализации этих методов) + + int64_t value::asInt(int64_t def) const { if (isInteger()) { @@ -163,23 +163,23 @@ double value::asFloat(double def) const { return def; } -// ... (остальные методы as... и вспомогательные функции) -// --- Фабричные методы --- + + value value::fromString(const std::string& str) { if (auto result = tryFromString(str)) { - // Теперь это должно работать, так как конструктор копирования определен + return *result; } - return value(str); // Возврат как строка, если парсинг не удался + return value(str); } std::optional value::tryFromString(const std::string& str) { if (str.empty()) return std::nullopt; - // Попробуем разные типы по порядку + if (auto val = parseInt(str)) { return value(*val); } @@ -192,19 +192,19 @@ std::optional value::tryFromString(const std::string& str) { return value(*val); } - // Проверим, может это цвет? + if (auto color = parseColor(str)) { return value(*color); } - // Если ничего не подошло, возвращаем nullopt - // или можно вернуть как строку: return value(str); + + return std::nullopt; } -// --- Вспомогательные функции для парсинга (примеры) --- -// (У вас, вероятно, уже есть реализации) + + std::optional value::parseInt(const std::string& str) { return parsers::parseInt(str); @@ -222,7 +222,7 @@ std::optional value::parseColor(const std::string& str) { return parsers::parseColor(str); } -// Реализации утилит из namespace parsers (если они в value.cpp) + namespace parsers { std::optional parseInt(const std::string& str) { if (str.empty()) return std::nullopt; @@ -257,11 +257,11 @@ namespace parsers { std::string hex = str.substr(1); - // Убираем пробелы (на всякий случай) + hex.erase(std::remove(hex.begin(), hex.end(), ' '), hex.end()); if (hex.length() == 3) { - // #RGB -> #RRGGBB + std::string expanded; for (char c : hex) { expanded += c; @@ -271,7 +271,7 @@ namespace parsers { } if (hex.length() == 4) { - // #RGBA -> #RRGGBBAA + std::string expanded; for (size_t i = 0; i < 4; ++i) { expanded += hex[i]; @@ -281,7 +281,7 @@ namespace parsers { } if (hex.length() == 6) { - // #RRGGBB + try { unsigned int r = std::stoi(hex.substr(0, 2), nullptr, 16); unsigned int g = std::stoi(hex.substr(2, 2), nullptr, 16); @@ -293,7 +293,7 @@ namespace parsers { } if (hex.length() == 8) { - // #RRGGBBAA + try { unsigned int r = std::stoi(hex.substr(0, 2), nullptr, 16); unsigned int g = std::stoi(hex.substr(2, 2), nullptr, 16); @@ -307,27 +307,27 @@ namespace parsers { return std::nullopt; } -} // namespace parsers +} + + -// --- Остальные методы --- -// ... (toString, operator==, Hash и т.д. - реализации должны быть у вас) bool value::operator==(const value& other) const { - // Простая реализация сравнения + if (this->getType() != other.getType()) return false; - // Сравнение конкретных значений + switch (this->getType()) { case Type::Null: return other.isNull(); case Type::String: return std::get(this->data) == std::get(other.data); case Type::Integer: return std::get(this->data) == std::get(other.data); - case Type::Float: return std::abs(std::get(this->data) - std::get(other.data)) < 1e-10; // Точность? + case Type::Float: return std::abs(std::get(this->data) - std::get(other.data)) < 1e-10; case Type::Boolean: return std::get(this->data) == std::get(other.data); case Type::Vec2: return std::get(this->data) == std::get(other.data); case Type::Vec3: return std::get(this->data) == std::get(other.data); case Type::Vec4: return std::get(this->data) == std::get(other.data); - default: return false; // Для не поддерживаемых типов + default: return false; } } @@ -349,9 +349,9 @@ std::string value::typeName() const { } } -// Реализация хэш-функции + std::size_t value::Hash::operator()(const value& v) const { - // Базовая реализация хэша + switch (v.getType()) { case Type::String: return std::hash{}(v.asString()); @@ -425,12 +425,12 @@ glm::vec4 value::asVec4(const glm::vec4& def) const { if (isVec4()) { return std::get(data); } - // Vec3 -> Vec4 (w = 1.0) + if (isVec3()) { auto v3 = std::get(data); return glm::vec4(v3, 1.0f); } - // Vec2 -> Vec4 (z=0, w=1) + if (isVec2()) { auto v2 = std::get(data); return glm::vec4(v2, 0.0f, 1.0f); diff --git a/src/graphics/ui/style/value.h b/src/graphics/ui/style/value.h index ae97da1dd..bae009655 100644 --- a/src/graphics/ui/style/value.h +++ b/src/graphics/ui/style/value.h @@ -1,118 +1,110 @@ -// value.h (обновленная версия) + #pragma once +#include #include #include -#include -#include +#include #include -#include +#include #include -#include +#include namespace style { -class value { -public: - enum class Type { - Null, - String, - Integer, - Float, - Boolean, - Vec2, - Vec3, - Vec4 - }; - -private: - std::variant< - std::monostate, - std::string, - int64_t, - double, - bool, - glm::vec2, - glm::vec3, - glm::vec4 - > data; - - mutable std::unique_ptr color_cache; - mutable bool color_cache_valid = false; - mutable std::string cached_string_value; - - static std::optional parseInt(const std::string& str); - static std::optional parseFloat(const std::string& str); - static std::optional parseBool(const std::string& str); - static std::optional parseColor(const std::string& str); - -public: - // Конструкторы - value(); - value(const value& other); - value& operator=(const value& other); - value(value&& other) noexcept; - value& operator=(value&& other) noexcept; - - value(std::monostate); - value(const char* v); - value(std::string v); - value(int v); - value(unsigned int v); - value(int64_t v); - value(double v); - value(float v); - value(bool v); - value(glm::vec2 v); - value(glm::vec3 v); - value(glm::vec4 v); - - // Фабричные методы - static value fromString(const std::string& str); - static std::optional tryFromString(const std::string& str); - - // Тип проверки - bool isNull() const; - bool isString() const; - bool isNumber() const; - bool isInteger() const; - bool isFloat() const; - bool isBool() const; - bool isVec2() const; - bool isVec3() const; - bool isVec4() const; - - Type getType() const; - - // Получение значений - int64_t asInt(int64_t def = 0) const; - double asFloat(double def = 0.0) const; - float asFloat(float def = 0.0f) const; - bool asBool(bool def = false) const; - std::string asString(const std::string& def = "") const; - glm::vec2 asVec2(const glm::vec2& def = {}) const; - glm::vec3 asVec3(const glm::vec3& def = {}) const; - glm::vec4 asVec4(const glm::vec4& def = {}) const; - glm::vec4 asColor(const glm::vec4& def = {0,0,0,1}) const; - - // Для отладки - std::string typeName() const; - std::string toString() const; - - // Операторы сравнения - bool operator==(const value& other) const; - bool operator!=(const value& other) const; - - struct Hash { - std::size_t operator()(const value& v) const; + class value { + public: + enum class Type { + Null, + String, + Integer, + Float, + Boolean, + Vec2, + Vec3, + Vec4 + }; + private: + std::variant< + std::monostate, + std::string, + int64_t, + double, + bool, + glm::vec2, + glm::vec3, + glm::vec4> + data; + + mutable std::unique_ptr color_cache; + mutable bool color_cache_valid = false; + mutable std::string cached_string_value; + + static std::optional parseInt(const std::string& str); + static std::optional parseFloat(const std::string& str); + static std::optional parseBool(const std::string& str); + static std::optional parseColor(const std::string& str); + public: + value(); + value(const value& other); + value& operator=(const value& other); + value(value&& other) noexcept; + value& operator=(value&& other) noexcept; + + value(std::monostate); + value(const char* v); + value(std::string v); + value(int v); + value(unsigned int v); + value(int64_t v); + value(double v); + value(float v); + value(bool v); + value(glm::vec2 v); + value(glm::vec3 v); + value(glm::vec4 v); + + static value fromString(const std::string& str); + static std::optional tryFromString(const std::string& str); + + bool isNull() const; + bool isString() const; + bool isNumber() const; + bool isInteger() const; + bool isFloat() const; + bool isBool() const; + bool isVec2() const; + bool isVec3() const; + bool isVec4() const; + + Type getType() const; + + int64_t asInt(int64_t def = 0) const; + double asFloat(double def = 0.0) const; + float asFloat(float def = 0.0f) const; + bool asBool(bool def = false) const; + std::string asString(const std::string& def = "") const; + glm::vec2 asVec2(const glm::vec2& def = {}) const; + glm::vec3 asVec3(const glm::vec3& def = {}) const; + glm::vec4 asVec4(const glm::vec4& def = {}) const; + glm::vec4 asColor(const glm::vec4& def = {0, 0, 0, 1}) const; + + std::string typeName() const; + std::string toString() const; + + bool operator==(const value& other) const; + bool operator!=(const value& other) const; + + struct Hash { + std::size_t operator()(const value& v) const; + }; }; -}; -namespace parsers { - std::optional parseInt(const std::string& str); - std::optional parseFloat(const std::string& str); - std::optional parseBool(const std::string& str); - std::optional parseColor(const std::string& str); -} + namespace parsers { + std::optional parseInt(const std::string& str); + std::optional parseFloat(const std::string& str); + std::optional parseBool(const std::string& str); + std::optional parseColor(const std::string& str); + } -} // namespace style \ No newline at end of file +} \ No newline at end of file From bacecabe23bd7f7fa0ab18d4288289b2cf68b3f0 Mon Sep 17 00:00:00 2001 From: GHOST11111100 Date: Wed, 6 Aug 2025 17:00:23 +0300 Subject: [PATCH 06/11] Refactor style parsing and node handling - Removed the StylesheetParser.h file as it was no longer needed. - Updated value.cpp to remove an unnecessary return statement in the Hash operator. - Implemented Node class in Node.cpp and Node.hpp for XML node representation, including methods for child management, attribute handling, and searching by tag/path. - Added CSSSelectorParser class in SelectorParser.cpp and SelectorParser.hpp for parsing CSS selectors, including support for combinators and pseudo-classes. - Introduced StylesheetParser class in StylesheetParser.hpp for parsing stylesheets, including rules and declarations. - Created a test CSS file (test.css) with a basic style rule. --- src/graphics/ui/GUI.cpp | 33 ++ src/graphics/ui/elements/Node.cpp | 317 +++++++++++ src/graphics/ui/elements/Node.hpp | 83 +++ src/graphics/ui/style/SelectorParser.cpp | 235 ++++++++ src/graphics/ui/style/SelectorParser.hpp | 52 ++ src/graphics/ui/style/StyleContext.cpp | 226 -------- src/graphics/ui/style/StyleContext.h | 125 ----- src/graphics/ui/style/Stylesheet.cpp | 281 +--------- src/graphics/ui/style/Stylesheet.h | 241 ++++---- src/graphics/ui/style/StylesheetParser.cpp | 606 +++++++-------------- src/graphics/ui/style/StylesheetParser.h | 83 --- src/graphics/ui/style/StylesheetParser.hpp | 29 + src/graphics/ui/style/value.cpp | 1 - test/test.css | 3 + 14 files changed, 1050 insertions(+), 1265 deletions(-) create mode 100644 src/graphics/ui/elements/Node.cpp create mode 100644 src/graphics/ui/elements/Node.hpp create mode 100644 src/graphics/ui/style/SelectorParser.cpp create mode 100644 src/graphics/ui/style/SelectorParser.hpp delete mode 100644 src/graphics/ui/style/StyleContext.cpp delete mode 100644 src/graphics/ui/style/StyleContext.h delete mode 100644 src/graphics/ui/style/StylesheetParser.h create mode 100644 src/graphics/ui/style/StylesheetParser.hpp create mode 100644 test/test.css diff --git a/src/graphics/ui/GUI.cpp b/src/graphics/ui/GUI.cpp index ecf558727..f767c225c 100644 --- a/src/graphics/ui/GUI.cpp +++ b/src/graphics/ui/GUI.cpp @@ -7,6 +7,7 @@ #include "assets/Assets.hpp" #include "elements/Label.hpp" #include "elements/Menu.hpp" +#include "elements/Node.hpp" #include "elements/Panel.hpp" #include "elements/UINode.hpp" #include "engine/Engine.hpp" @@ -18,6 +19,7 @@ #include "graphics/core/LineBatch.hpp" #include "graphics/core/Shader.hpp" #include "gui_util.hpp" +#include "style/StylesheetParser.hpp" #include "window/Camera.hpp" #include "window/Window.hpp" #include "window/input.hpp" @@ -53,8 +55,39 @@ GUI::GUI(Engine& engine) store("tooltip", tooltip); store("tooltip.label", UINode::find(tooltip, "tooltip.label")); container->add(tooltip); + + // Testing new UI Implementation + + std::string xmlString = R"( + + + +
+ 123-456-7890 + + +)"; + std::string css_code = R"( + // comment variant 1 + /* comment variant 2 */ + div.hover#open:focus + span:hover, button { + color: red; + } + + span { + background: white red; + } +)"; + + Node root = Node::from_xml_string("example.xml", xmlString); + + StylesheetParser parser("test.css", css_code); + style::Stylesheet stylesheet = parser.parse(); + } + + GUI::~GUI() = default; void GUI::setPageLoader(PageLoaderFunc pageLoader) { diff --git a/src/graphics/ui/elements/Node.cpp b/src/graphics/ui/elements/Node.cpp new file mode 100644 index 000000000..ec383dea2 --- /dev/null +++ b/src/graphics/ui/elements/Node.cpp @@ -0,0 +1,317 @@ +#pragma once + +#include "Node.hpp" + +Node Node::from_xml_node(const xml::Node& xml_node) { + + if (xml_node.isText()) { + return Node(xml_node.getInnerText()); + } + + AttributesMap attrs; + const auto& xml_attrs = xml_node.getAttributes(); + for (const auto& [name, attr] : xml_attrs) { + attrs[name] = attr.getText(); + } + + Node dom_node(xml_node.getTag(), std::move(attrs)); + + for (size_t i = 0; i < xml_node.size(); ++i) { + const xml::Node& child = xml_node.sub(i); + dom_node.append_child(from_xml_node(child)); + } + + return dom_node; +} + +Node Node::from_xml_document(const xml::Document& xml_doc) { + if (!xml_doc.getRoot()) { + throw std::runtime_error("XML document has no root element"); + } + + return from_xml_node(*xml_doc.getRoot()); +} + +Node Node::from_xml_string(std::string_view filename, std::string_view source) { + auto xml_doc = xml::parse(filename, source); + return from_xml_document(*xml_doc); +} + +// Working with childs +// + +void Node::append_child(Node node) { + children.push_back( std::move( node ) ); +} + +void Node::prepend_child(Node node) { + children.insert( children.begin(), std::move( node ) ); +} + +void Node::append_childs(std::vector nodes) { + children.reserve( children.size() + nodes.size() ); + + for ( Node node: nodes ) { + children.push_back( std::move( node ) ); + } +} + +void Node::insert_child(size_t index, Node node) { + if (index > children.size()) { + throw std::out_of_range("Index out of range"); + } + children.insert(children.begin() + index, std::move(node)); +} + +void Node::remove_child(size_t index) { + if (index >= children.size()) { + throw std::out_of_range("Index out of range"); + } + children.erase(children.begin() + index); +} + +void Node::clear_children() { + children.clear(); +} + +const std::string& Node::get_tag() const { + return std::visit([](const auto& node) -> const std::string& { + using T = std::decay_t; + if constexpr (std::is_same_v) { + static const std::string text_tag = "#text"; + return text_tag; + } else { + return node.data.tag_name; + } + }, node_type); +} + +const std::string& Node::get_text() const { + return std::visit([](const auto& node) -> const std::string& { + using T = std::decay_t; + if constexpr (std::is_same_v) { + return node.content; + } else { + static const std::string empty_string = ""; + return empty_string; + } + }, node_type); +} + +bool Node::is_text() const { + return std::holds_alternative(node_type); +} + +bool Node::is_element() const { + return std::holds_alternative(node_type); +} + +size_t Node::child_count() const { + return children.size(); +} + +void Node::set_attribute(std::string_view name, std::string_view value) { + if (auto* element = std::get_if(&node_type)) { + element->data.attributes[std::string(name)] = std::string(value); + } else { + throw std::runtime_error("Cannot set attribute on text node"); + } +} + +void Node::remove_attribute(std::string_view name) { + if (auto* element = std::get_if(&node_type)) { + element->data.attributes.erase(std::string(name)); + } +} + +bool Node::has_attribute(std::string_view name) const { + if (const auto* element = std::get_if(&node_type)) { + return element->data.attributes.find(std::string(name)) != element->data.attributes.end(); + } + return false; +} + +std::optional Node::get_attribute(std::string_view name) const { + if (const auto* element = std::get_if(&node_type)) { + auto it = element->data.attributes.find(std::string(name)); + if (it != element->data.attributes.end()) { + return it->second; + } + } + return std::nullopt; +} + +const AttributesMap& Node::get_attributes() const { + static const AttributesMap empty_attrs = {}; + + if (const auto* element = std::get_if(&node_type)) { + return element->data.attributes; + } + return empty_attrs; +} + +std::vector Node::find_by_tag(std::string_view tag) { + std::vector result; + + if (is_element() && get_tag() == tag) { + result.push_back(this); + } + + for (auto& child : children) { + auto child_results = child.find_by_tag(tag); + result.insert(result.end(), child_results.begin(), child_results.end()); + } + + return result; +} + +std::vector Node::find_by_tag(std::string_view tag) const { + std::vector result; + + if (is_element() && get_tag() == tag) { + result.push_back(this); + } + + for (const auto& child : children) { + auto child_results = child.find_by_tag(tag); + result.insert(result.end(), child_results.begin(), child_results.end()); + } + + return result; +} + +Node* Node::find_first_by_tag(std::string_view tag) { + if (is_element() && get_tag() == tag) { + return this; + } + + for (auto& child : children) { + if (auto* found = child.find_first_by_tag(tag)) { + return found; + } + } + + return nullptr; +} + +const Node* Node::find_first_by_tag(std::string_view tag) const { + if (is_element() && get_tag() == tag) { + return this; + } + + for (const auto& child : children) { + if (const auto* found = child.find_first_by_tag(tag)) { + return found; + } + } + + return nullptr; +} + +Node* Node::find_by_path(std::string_view path) { + if (path.empty()) { + return nullptr; + } + + std::string path_str(path); + std::vector parts; + size_t start = 0; + size_t pos = 0; + + while ((pos = path_str.find('>', start)) != std::string::npos) { + std::string part = path_str.substr(start, pos - start); + if (!part.empty()) { + parts.push_back(std::move(part)); + } + start = pos + 1; + } + + std::string last_part = path_str.substr(start); + if (!last_part.empty()) { + parts.push_back(std::move(last_part)); + } + + if (parts.empty()) { + return nullptr; + } + + return find_by_path_impl(parts, 0); +} + +const Node* Node::find_by_path(std::string_view path) const { + if (path.empty()) { + return nullptr; + } + + std::string path_str(path); + std::vector parts; + size_t start = 0; + size_t pos = 0; + + while ((pos = path_str.find('>', start)) != std::string::npos) { + std::string part = path_str.substr(start, pos - start); + if (!part.empty()) { + parts.push_back(std::move(part)); + } + start = pos + 1; + } + + std::string last_part = path_str.substr(start); + if (!last_part.empty()) { + parts.push_back(std::move(last_part)); + } + + if (parts.empty()) { + return nullptr; + } + + return find_by_path_impl(parts, 0); +} + +Node* Node::find_by_path_impl(const std::vector& parts, size_t index) { + if (index >= parts.size()) { + return nullptr; + } + + const std::string& expected_tag = parts[index]; + + if (is_element() && get_tag() == expected_tag) { + if (index == parts.size() - 1) { + return this; + } + + for (auto& child : children) { + if (auto* found = child.find_by_path_impl(parts, index + 1)) { + return found; + } + } + } + + return nullptr; +} + +const Node* Node::find_by_path_impl(const std::vector& parts, size_t index) const { + if (index >= parts.size()) { + return nullptr; + } + + const std::string& expected_tag = parts[index]; + + if (is_element() && get_tag() == expected_tag) { + if (index == parts.size() - 1) { + return this; + } + + for (const auto& child : children) { + if (const auto* found = child.find_by_path_impl(parts, index + 1)) { + return found; + } + } + } + + return nullptr; +} + +const std::vector& Node::get_children() const { + return children; +} \ No newline at end of file diff --git a/src/graphics/ui/elements/Node.hpp b/src/graphics/ui/elements/Node.hpp new file mode 100644 index 000000000..3783fad2d --- /dev/null +++ b/src/graphics/ui/elements/Node.hpp @@ -0,0 +1,83 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "coders/xml.hpp" + +using AttributesMap = std::unordered_map; + +struct ElementData { + std::string tag_name; + AttributesMap attributes; + + ElementData(std::string_view tag, AttributesMap attrs): tag_name(tag), attributes(std::move(attrs)) {} +}; + +struct Text { + std::string content; + + Text(std::string_view text_content) : content(text_content) {} +}; + +struct Element { + ElementData data; + + Element(std::string_view tag, AttributesMap attrs): data(tag, std::move(attrs)) {} +}; + +using NodeType = std::variant; + +struct Node { + std::vector children; + NodeType node_type; + + Node(std::string_view text): node_type(Text(text)) {} + Node(std::string_view tag, AttributesMap attrs): node_type(Element(tag, std::move(attrs))) {} + Node(std::string_view tag, AttributesMap attrs, std::vector childs): children(std::move(childs)), node_type(Element(tag, std::move(attrs))) {} + + // Working with childs + void append_child(Node node); + void append_childs(std::vector nodes); + void prepend_child(Node node); + void insert_child(size_t index, Node node); + void remove_child(size_t index); + void clear_children(); + + // Getters + const std::string& get_tag() const; + const std::string& get_text() const; + bool is_text() const; + bool is_element() const; + size_t child_count() const; + const std::vector& get_children() const; + + // Working with attributes + void set_attribute(std::string_view name, std::string_view value); + void remove_attribute(std::string_view name); + bool has_attribute(std::string_view name) const; + std::optional get_attribute(std::string_view name) const; + const AttributesMap& get_attributes() const; + + // Search + std::vector find_by_tag(std::string_view tag); + std::vector find_by_tag(std::string_view tag) const; + Node* find_first_by_tag(std::string_view tag); + const Node* find_first_by_tag(std::string_view tag) const; + Node* find_by_path(std::string_view path); // "div>p>span" + const Node* find_by_path(std::string_view path) const; + + // Parse from XML + static Node from_xml_string(std::string_view filename, std::string_view source); + static Node from_xml_document(const xml::Document& xml_doc); + + private: + static Node from_xml_node(const xml::Node& xml_node); + + Node* find_by_path_impl(const std::vector& parts, size_t index); + const Node* find_by_path_impl(const std::vector& parts, size_t index) const; +}; diff --git a/src/graphics/ui/style/SelectorParser.cpp b/src/graphics/ui/style/SelectorParser.cpp new file mode 100644 index 000000000..1bb358c24 --- /dev/null +++ b/src/graphics/ui/style/SelectorParser.cpp @@ -0,0 +1,235 @@ +#include "SelectorParser.hpp" +using namespace style; + +Selector CSSSelectorParser::parse() { + try { + skipWhitespace(); + + std::vector first_selectors = + parseSimpleSelectorSequence(); + + if (!hasNext()) { + if (first_selectors.size() == 1) { + return Selector(first_selectors.front()); + } else { + return Selector(ComplexSelector(first_selectors)); + } + } + + ComplexSelector complex_selector; + complex_selector.first = std::move(first_selectors); + + while (hasNext()) { + Combinator combinator = parseCombinator(); + + skipWhitespace(); + if (!hasNext()) { + break; + } + + std::vector next_selectors = + parseSimpleSelectorSequence(); + + if (next_selectors.empty()) { + break; + } + + complex_selector.parts.emplace_back( + combinator, std::move(next_selectors) + ); + } + + return Selector(std::move(complex_selector)); + } catch (const parsing_error& e) { + std::cerr << "Selector parsing error: " << e.what() << "\n"; + throw; + } +} + +std::vector CSSSelectorParser::parseSimpleSelectorSequence() { + std::vector selectors; + skipWhitespace(); + bool has_pseudo_prefix = false; + + while (hasNext()) { + char next = peekNoJump(); + if (next == ',' || next == '{' || next == '}' || next == ';' || + is_combinator(next)) { + break; + } + + if (next == ':') { + has_pseudo_prefix = true; + SimpleSelector selector; + selector.type = SimpleSelectorType::Tag; + selector.value = ""; + selectors.push_back( + std::move(selector) + ); + break; + } else { + SimpleSelector selector = parseSingleSimpleSelector(); + selectors.push_back(std::move(selector)); + } + skipWhitespace(); + } + parsePseudoClassesForSequence(selectors, has_pseudo_prefix); + return selectors; +} + +SimpleSelector CSSSelectorParser::parseSingleSimpleSelector() { + char c = peekNoJump(); + if (c == '*') { + nextChar(); + return SimpleSelector(SimpleSelectorType::Universal, "*"); + ; + } else if (c == '#') { + nextChar(); + return SimpleSelector(SimpleSelectorType::ID, parseIdentifier()); + } else if (c == '.') { + nextChar(); + return SimpleSelector(SimpleSelectorType::Class, parseIdentifier()); + ; + } else if (is_identifier_start(c)) { + return SimpleSelector(SimpleSelectorType::Tag, parseIdentifier()); + } else { + if (hasNext()) nextChar(); + return SimpleSelector(SimpleSelectorType::Tag, ""); + } +} + +std::string CSSSelectorParser::parseIdentifier() { + int start = pos; + // Первый символ + if (hasNext() && is_identifier_start(source[pos])) { + pos++; + } else { + return ""; + } + while (hasNext() && is_css_identifier_part(source[pos])) { + pos++; + } + return std::string(source.substr(start, pos - start)); +} + +bool CSSSelectorParser::is_css_identifier_part(char c) const { + + return is_identifier_part(c) || c == '-'; +} + +Combinator CSSSelectorParser::parseCombinator() { + skipWhitespace(); + if (!hasNext()) return Combinator::Descendant; + + char c = peekNoJump(); + switch (c) { + case '>': + nextChar(); + return Combinator::Child; + case '+': + nextChar(); + return Combinator::Adjacent; + case '~': + nextChar(); + return Combinator::GeneralSibling; + default: + return Combinator::Descendant; + } +} + +bool CSSSelectorParser::is_combinator(char c) const { + return c == '>' || c == '+' || c == '~'; +} + +void CSSSelectorParser::parseAndAddPseudoClass(SimpleSelector& selector) { + expect(':'); + std::string pseudo_name = parseIdentifier(); + + std::string lower_name = pseudo_name; + std::transform( + lower_name.begin(), lower_name.end(), lower_name.begin(), ::tolower + ); + + PseudoClass pclass = PseudoClass::Custom; + std::string argument = ""; + + static const std::map pseudo_map = { + {"hover", PseudoClass::Hover}, + {"focus", PseudoClass::Focus}, + {"active", PseudoClass::Active}, + {"checked", PseudoClass::Checked}, + {"disabled", PseudoClass::Disabled}, + {"nth-child", PseudoClass::NthChild}, + {"nth-of-type", PseudoClass::NthOfType} + }; + + auto it = pseudo_map.find(lower_name); + if (it != pseudo_map.end()) { + pclass = it->second; + } else { + pclass = PseudoClass::Custom; + } + + skipWhitespace(); + if (hasNext() && peekNoJump() == '(') { + nextChar(); + int start = pos; + int paren_count = 1; + while (hasNext() && paren_count > 0) { + char ch = source[pos]; + if (ch == '(') { + paren_count++; + } else if (ch == ')') { + paren_count--; + } + if (paren_count > 0) { + pos++; + } + } + if (paren_count == 0) { + argument = std::string(source.substr(start, pos - start)); + nextChar(); + } else { + argument = std::string(source.substr(start)); + } + } + + selector.pseudoClasses.emplace_back(pclass, argument); +} + +void CSSSelectorParser::parsePseudoClassesForSequence( + std::vector& selectors, bool has_pseudo_prefix +) { + if (has_pseudo_prefix) { + parseAndAddPseudoClass( + selectors.back() + ); + } + + while (hasNext() && peekNoJump() == ':') { + if (selectors.empty()) { + selectors.emplace_back(SimpleSelectorType::Tag, ""); + } + + SimpleSelector temp_selector = + selectors.front(); + temp_selector.pseudoClasses + .clear(); + parseAndAddPseudoClass(temp_selector); + + for (auto& sel : selectors) { + sel.pseudoClasses.insert( + sel.pseudoClasses.end(), + temp_selector.pseudoClasses.begin(), + temp_selector.pseudoClasses.end() + ); + } + + skipWhitespace(); + } +} + +Selector parseCSSSelector(std::string_view selector_text) { + CSSSelectorParser parser(selector_text); + return parser.parse(); +} \ No newline at end of file diff --git a/src/graphics/ui/style/SelectorParser.hpp b/src/graphics/ui/style/SelectorParser.hpp new file mode 100644 index 000000000..ca1bb5fd5 --- /dev/null +++ b/src/graphics/ui/style/SelectorParser.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "Stylesheet.h" +#include "coders/BasicParser.hpp" + +class CSSSelectorParser : public BasicParser { +public: + CSSSelectorParser( + const std::string_view source, const std::string& filename = "" + ) + : BasicParser(filename, source) { + // Включаем поддержку C-стильных комментариев, если они возможны в + // селекторах + this->clikeComment = true; + } + + style::Selector parse(); +private: + std::vector parseSimpleSelectorSequence(); + style::SimpleSelector parseSingleSimpleSelector(); + + // Парсит идентификатор (имя тега, класса, ID) + std::string parseIdentifier(); + + // Проверяет, является ли символ допустимым для продолжения CSS + // идентификатора + bool is_css_identifier_part(char c) const; + + // Парсит комбинатор и возвращает его тип + style::Combinator parseCombinator(); + + // Проверяет, является ли символ комбинатором + bool is_combinator(char c) const; + + // Парсит псевдокласс и добавляет его к селектору + void parseAndAddPseudoClass(style::SimpleSelector& selector); + + void parsePseudoClassesForSequence( + std::vector& selectors, bool has_pseudo_prefix + ); +}; + +// --- Вспомогательная функция для удобства --- +style::Selector parseCSSSelector(std::string_view selector_text); \ No newline at end of file diff --git a/src/graphics/ui/style/StyleContext.cpp b/src/graphics/ui/style/StyleContext.cpp deleted file mode 100644 index a1bf39a0c..000000000 --- a/src/graphics/ui/style/StyleContext.cpp +++ /dev/null @@ -1,226 +0,0 @@ -#include "StyleContext.h" -#include "graphics/ui/elements/UINode.hpp" -#include -#include - -namespace style { - - -StyleContext::StyleContext() : tag_("node") {} - -StyleContext::StyleContext(const std::string& tag) - : tag_(tag) {} - -StyleContext::StyleContext(const std::string& tag, const std::string& id) - : tag_(tag), id_(id) {} - -StyleContext::StyleContext(const std::string& tag, const std::vector& classes) - : tag_(tag) { - for (const auto& cls : classes) { - classes_.insert(cls); - } -} - -StyleContext::StyleContext(const std::string& tag, const std::string& id, const std::vector& classes) - : tag_(tag), id_(id) { - for (const auto& cls : classes) { - classes_.insert(cls); - } -} - -StyleContext::StyleContext(gui::UINode& node) - : tag_("node"), ui_node_(&node) { - updateFromUINode(); -} - - -void StyleContext::setTag(const std::string& tag) { - tag_ = tag; - selector_cache_valid_ = false; -} - -void StyleContext::setID(const std::string& id) { - id_ = id; - selector_cache_valid_ = false; -} - - -void StyleContext::addClass(const std::string& className) { - classes_.insert(className); - selector_cache_valid_ = false; -} - -void StyleContext::removeClass(const std::string& className) { - classes_.erase(className); - selector_cache_valid_ = false; -} - -bool StyleContext::hasClass(const std::string& className) const { - return classes_.find(className) != classes_.end(); -} - -void StyleContext::setClasses(const std::vector& classes) { - classes_.clear(); - for (const auto& cls : classes) { - classes_.insert(cls); - } - selector_cache_valid_ = false; -} - -void StyleContext::clearClasses() { - classes_.clear(); - selector_cache_valid_ = false; -} - - -void StyleContext::setState(State state, bool enabled) { - if (enabled) { - states_.insert(state); - } else { - states_.erase(state); - } -} - -bool StyleContext::hasState(State state) const { - return states_.find(state) != states_.end(); -} - -void StyleContext::clearStates() { - states_.clear(); -} - - -void StyleContext::syncWithUINode() { - if (ui_node_) { - updateFromUINode(); - } -} - -void StyleContext::updateFromUINode() { - if (!ui_node_) return; - - - id_ = ui_node_->getId(); - - - std::string classname = ui_node_->getClassname(); - if (!classname.empty()) { - - std::istringstream iss(classname); - std::string cls; - classes_.clear(); - while (iss >> cls) { - if (!cls.empty()) { - classes_.insert(cls); - } - } - } - - - states_.clear(); - if (!ui_node_->isEnabled()) { - states_.insert(State::Disabled); - } - if (ui_node_->isHover()) { - states_.insert(State::Hover); - } - if (ui_node_->isPressed()) { - states_.insert(State::Active); - } - if (ui_node_->isFocused()) { - states_.insert(State::Focused); - } - - selector_cache_valid_ = false; -} - - -std::string StyleContext::getSelectorString() const { - if (selector_cache_valid_) { - return selector_cache_; - } - - std::ostringstream oss; - - - if (!tag_.empty()) { - oss << tag_; - } - - - if (!id_.empty()) { - oss << "#" << id_; - } - - - for (const auto& cls : classes_) { - oss << "." << cls; - } - - selector_cache_ = oss.str(); - selector_cache_valid_ = true; - - return selector_cache_; -} - -bool StyleContext::hasID(const std::string& id) const { - return id == id; -} - -std::unique_ptr StyleContext::clone() const { - auto clone = std::make_unique(tag_, id_); - clone->classes_ = classes_; - clone->states_ = states_; - clone->parent_ = parent_; - clone->ui_node_ = ui_node_; - clone->selector_cache_ = selector_cache_; - clone->selector_cache_valid_ = selector_cache_valid_; - return clone; -} - -bool StyleContext::operator==(const StyleContext& other) const { - return tag_ == other.tag_ && - id_ == other.id_ && - classes_ == other.classes_ && - states_ == other.states_; -} - -std::size_t StyleContext::Hash::operator()(const StyleContext& ctx) const { - std::size_t h1 = std::hash{}(ctx.tag_); - std::size_t h2 = std::hash{}(ctx.id_); - - return h1 ^ (h2 << 1); -} - -void StyleContext::updateSelectorCache() const { - - selector_cache_valid_ = false; -} - - -namespace context { - std::unique_ptr create(const std::string& tag) { - return std::make_unique(tag); - } - - std::unique_ptr create(const std::string& tag, const std::string& id) { - return std::make_unique(tag, id); - } - - std::unique_ptr create(const std::string& tag, - const std::vector& classes) { - return std::make_unique(tag, classes); - } - - std::unique_ptr create(const std::string& tag, - const std::string& id, - const std::vector& classes) { - return std::make_unique(tag, id, classes); - } - - std::unique_ptr fromUINode(gui::UINode& node) { - return std::make_unique(node); - } -} - -} \ No newline at end of file diff --git a/src/graphics/ui/style/StyleContext.h b/src/graphics/ui/style/StyleContext.h deleted file mode 100644 index 02e5e1ad1..000000000 --- a/src/graphics/ui/style/StyleContext.h +++ /dev/null @@ -1,125 +0,0 @@ - -#pragma once - -#include -#include -#include -#include -#include - -namespace style { - class Stylesheet; - class ComputedStyle; -} - -namespace gui { - class UINode; -} - -namespace style { - - class StyleContext { - public: - enum class State { Normal, Hover, Active, Disabled, Focused }; - private: - std::string tag_; - std::unordered_set classes_; - std::string id_; - std::unordered_set states_; - - const StyleContext* parent_ = nullptr; - - gui::UINode* ui_node_ = nullptr; - public: - StyleContext(); - explicit StyleContext(const std::string& tag); - StyleContext(const std::string& tag, const std::string& id); - StyleContext( - const std::string& tag, const std::vector& classes - ); - StyleContext( - const std::string& tag, - const std::string& id, - const std::vector& classes - ); - - explicit StyleContext(gui::UINode& node); - - const std::string& getTag() const { - return tag_; - } - const std::string& getID() const { - return id_; - } - const std::unordered_set& getClasses() const { - return classes_; - } - const std::unordered_set& getStates() const { - return states_; - } - const StyleContext* getParent() const { - return parent_; - } - gui::UINode* getUINode() const { - return ui_node_; - } - - void setTag(const std::string& tag); - void setID(const std::string& id); - void setParent(const StyleContext* parent) { - parent_ = parent; - } - void setUINode(gui::UINode* node) { - ui_node_ = node; - } - - void addClass(const std::string& className); - void removeClass(const std::string& className); - bool hasClass(const std::string& className) const; - void setClasses(const std::vector& classes); - void clearClasses(); - - void setState(State state, bool enabled = true); - bool hasState(State state) const; - void clearStates(); - - void syncWithUINode(); - void updateFromUINode(); - - std::string getSelectorString() const; - bool hasID(const std::string& id) const; - - std::unique_ptr clone() const; - - bool operator==(const StyleContext& other) const; - bool operator!=(const StyleContext& other) const { - return !(*this == other); - } - - struct Hash { - std::size_t operator()(const StyleContext& ctx) const; - }; - private: - void updateSelectorCache() const; - mutable std::string selector_cache_; - mutable bool selector_cache_valid_ = false; - }; - - namespace context { - std::unique_ptr create(const std::string& tag); - std::unique_ptr create( - const std::string& tag, const std::string& id - ); - std::unique_ptr create( - const std::string& tag, const std::vector& classes - ); - std::unique_ptr create( - const std::string& tag, - const std::string& id, - const std::vector& classes - ); - - std::unique_ptr fromUINode(gui::UINode& node); - } - -} \ No newline at end of file diff --git a/src/graphics/ui/style/Stylesheet.cpp b/src/graphics/ui/style/Stylesheet.cpp index 462b58561..3a5b39143 100644 --- a/src/graphics/ui/style/Stylesheet.cpp +++ b/src/graphics/ui/style/Stylesheet.cpp @@ -1,283 +1,4 @@ - -#include "Stylesheet.h" -#include "StyleContext.h" #include #include #include - -namespace style { - -PropertyID getPropertyID(const std::string& name) { - static const std::unordered_map property_map = { - {"color", PropertyID::Color}, - {"background-color", PropertyID::BackgroundColor}, - {"margin", PropertyID::Margin}, - {"margin-top", PropertyID::MarginTop}, - {"margin-right", PropertyID::MarginRight}, - {"margin-bottom", PropertyID::MarginBottom}, - {"margin-left", PropertyID::MarginLeft}, - {"padding", PropertyID::Padding}, - {"padding-top", PropertyID::PaddingTop}, - {"padding-right", PropertyID::PaddingRight}, - {"padding-bottom", PropertyID::PaddingBottom}, - {"padding-left", PropertyID::PaddingLeft}, - {"width", PropertyID::Width}, - {"height", PropertyID::Height}, - {"min-width", PropertyID::MinWidth}, - {"min-height", PropertyID::MinHeight}, - {"max-width", PropertyID::MaxWidth}, - {"max-height", PropertyID::MaxHeight}, - {"z-index", PropertyID::ZIndex}, - {"display", PropertyID::Display}, - {"position", PropertyID::Position}, - {"top", PropertyID::Top}, - {"right", PropertyID::Right}, - {"bottom", PropertyID::Bottom}, - {"left", PropertyID::Left}, - {"opacity", PropertyID::Opacity}, - {"border", PropertyID::Border}, - {"border-radius", PropertyID::BorderRadius}, - {"font-size", PropertyID::FontSize}, - {"font-weight", PropertyID::FontWeight}, - {"text-align", PropertyID::TextAlign} - }; - - auto it = property_map.find(name); - return (it != property_map.end()) ? it->second : PropertyID::Unknown; -} - -std::string getPropertyName(PropertyID id) { - static const std::unordered_map property_names = { - {PropertyID::Color, "color"}, - {PropertyID::BackgroundColor, "background-color"}, - {PropertyID::Margin, "margin"}, - {PropertyID::MarginTop, "margin-top"}, - {PropertyID::MarginRight, "margin-right"}, - {PropertyID::MarginBottom, "margin-bottom"}, - {PropertyID::MarginLeft, "margin-left"}, - {PropertyID::Padding, "padding"}, - {PropertyID::PaddingTop, "padding-top"}, - {PropertyID::PaddingRight, "padding-right"}, - {PropertyID::PaddingBottom, "padding-bottom"}, - {PropertyID::PaddingLeft, "padding-left"}, - {PropertyID::Width, "width"}, - {PropertyID::Height, "height"}, - {PropertyID::MinWidth, "min-width"}, - {PropertyID::MinHeight, "min-height"}, - {PropertyID::MaxWidth, "max-width"}, - {PropertyID::MaxHeight, "max-height"}, - {PropertyID::ZIndex, "z-index"}, - {PropertyID::Display, "display"}, - {PropertyID::Position, "position"}, - {PropertyID::Top, "top"}, - {PropertyID::Right, "right"}, - {PropertyID::Bottom, "bottom"}, - {PropertyID::Left, "left"}, - {PropertyID::Opacity, "opacity"}, - {PropertyID::Border, "border"}, - {PropertyID::BorderRadius, "border-radius"}, - {PropertyID::FontSize, "font-size"}, - {PropertyID::FontWeight, "font-weight"}, - {PropertyID::TextAlign, "text-align"} - }; - - auto it = property_names.find(id); - return (it != property_names.end()) ? it->second : "unknown"; -} - - -Selector::Selector(Type type, std::string value) - : type_(type), value_(std::move(value)) { - - switch (type_) { - case Type::Universal: specificity_ = 0; break; - case Type::Tag: specificity_ = 1; break; - case Type::Class: specificity_ = 10; break; - case Type::ID: specificity_ = 100; break; - } -} - -bool Selector::matches(const StyleContext& context) const { - switch (type_) { - case Type::Universal: - return true; - case Type::Tag: - return context.getTag() == value_; - case Type::Class: - return context.hasClass(value_); - case Type::ID: - return context.hasID(value_); - } - return false; -} - -bool Selector::operator==(const Selector& other) const { - return type_ == other.type_ && value_ == other.value_; -} - -bool Selector::operator<(const Selector& other) const { - if (type_ != other.type_) { - return static_cast(type_) < static_cast(other.type_); - } - return value_ < other.value_; -} - -std::size_t Selector::Hash::operator()(const Selector& s) const { - return std::hash{}(static_cast(s.type_)) ^ - std::hash{}(s.value_); -} - - -StyleRule::StyleRule(std::vector selectors_, - std::unordered_map declarations_) - : selectors(std::move(selectors_)), declarations(std::move(declarations_)) { - priority = 0; -} - -int StyleRule::getSpecificity() const { - int total = 0; - for (const auto& selector : selectors) { - total += selector.getSpecificity(); - } - return total; -} - -bool StyleRule::matches(const StyleContext& context) const { - - for (const auto& selector : selectors) { - if (selector.matches(context)) { - return true; - } - } - return false; -} - - -void Stylesheet::addRule(const StyleRule& rule) { - rules_.push_back(rule); -} - -void Stylesheet::addRule(const std::vector& selectors, - const std::unordered_map& declarations) { - rules_.emplace_back(selectors, declarations); -} - -std::vector Stylesheet::getMatchingRules(const StyleContext& context) const { - std::vector matching_rules; - - - if (!tag_index_.empty() || !class_index_.empty() || !id_index_.empty()) { - std::unordered_set candidate_indices; - - auto tag_it = tag_index_.find(context.getTag()); - if (tag_it != tag_index_.end()) { - candidate_indices.insert(tag_it->second.begin(), tag_it->second.end()); - } - - for (const auto& cls : context.getClasses()) { - auto class_it = class_index_.find(cls); - if (class_it != class_index_.end()) { - candidate_indices.insert(class_it->second.begin(), class_it->second.end()); - } - } - - auto id_it = id_index_.find(context.getID()); - if (id_it != id_index_.end()) { - candidate_indices.insert(id_it->second.begin(), id_it->second.end()); - } - - for (size_t index : candidate_indices) { - if (index < rules_.size() && rules_[index].matches(context)) { - matching_rules.push_back(&rules_[index]); - } - } - } else { - - for (const auto& rule : rules_) { - if (rule.matches(context)) { - matching_rules.push_back(&rule); - } - } - } - - return matching_rules; -} - -void Stylesheet::sortRulesBySpecificity() { - std::sort(rules_.begin(), rules_.end(), - [](const StyleRule& a, const StyleRule& b) { - return a.getSpecificity() < b.getSpecificity(); - }); -} - -void Stylesheet::clear() { - rules_.clear(); - tag_index_.clear(); - class_index_.clear(); - id_index_.clear(); - selector_cache_.clear(); -} - -void Stylesheet::buildIndex() { - tag_index_.clear(); - class_index_.clear(); - id_index_.clear(); - - for (size_t i = 0; i < rules_.size(); ++i) { - const auto& rule = rules_[i]; - for (const auto& selector : rule.selectors) { - switch (selector.getType()) { - case Selector::Type::Tag: - tag_index_[selector.getValue()].push_back(i); - break; - case Selector::Type::Class: - class_index_[selector.getValue()].push_back(i); - break; - case Selector::Type::ID: - id_index_[selector.getValue()].push_back(i); - break; - case Selector::Type::Universal: - - break; - } - } - } -} - - -Selector Stylesheet::createTagSelector(const std::string& tag) { - return Selector(Selector::Type::Tag, tag); -} - -Selector Stylesheet::createClassSelector(const std::string& className) { - return Selector(Selector::Type::Class, className); -} - -Selector Stylesheet::createIDSelector(const std::string& id) { - return Selector(Selector::Type::ID, id); -} - -Selector Stylesheet::createUniversalSelector() { - return Selector(Selector::Type::Universal, "*"); -} - - -namespace selectors { - Selector tag(const std::string& name) { - return Selector(Selector::Type::Tag, name); - } - - Selector cls(const std::string& name) { - return Selector(Selector::Type::Class, name); - } - - Selector id(const std::string& name) { - return Selector(Selector::Type::ID, name); - } - - Selector universal() { - return Selector(Selector::Type::Universal, "*"); - } -} - -} \ No newline at end of file +#include "Stylesheet.h" diff --git a/src/graphics/ui/style/Stylesheet.h b/src/graphics/ui/style/Stylesheet.h index 6c425cf9f..6bc83dd72 100644 --- a/src/graphics/ui/style/Stylesheet.h +++ b/src/graphics/ui/style/Stylesheet.h @@ -1,163 +1,104 @@ #pragma once -#include +#include +#include #include #include -#include -#include +#include +#include + #include "value.h" namespace style { + enum class PseudoClass { + None, + Hover, + Focus, + Active, + Checked, + Disabled, + NthChild, // :nth-child() + NthOfType, + Custom // fallback + }; + + enum class SimpleSelectorType { + Universal, // * + Tag, // div, span + ID, // #id + Class // .class + }; -class StyleContext; -class ComputedStyle; -class Selector; - -enum class PropertyID : uint32_t { - Unknown = 0, - Color, - BackgroundColor, - Margin, - MarginTop, - MarginRight, - MarginBottom, - MarginLeft, - Padding, - PaddingTop, - PaddingRight, - PaddingBottom, - PaddingLeft, - Width, - Height, - MinWidth, - MinHeight, - MaxWidth, - MaxHeight, - ZIndex, - Display, - Position, - Top, - Right, - Bottom, - Left, - Opacity, - Border, - BorderRadius, - FontSize, - FontWeight, - TextAlign, -}; - -// Получение PropertyID по строке -PropertyID getPropertyID(const std::string& name); -std::string getPropertyName(PropertyID id); - -// Селектор стиля -class Selector { -public: - enum class Type { - Tag, // block - Class, // .highlight - ID, // #main - Universal // * + struct SimpleSelector { + SimpleSelectorType type; + std::string value; // tag, id or class + std::vector> pseudoClasses; + + SimpleSelector() = default; + SimpleSelector(SimpleSelectorType t, std::string v) : type(t), value(std::move(v)) {} + SimpleSelector(SimpleSelectorType t, std::string v, std::vector> p) : type(t), value(std::move(v)), pseudoClasses(std::move(p)) {} }; -private: - Type type_; - std::string value_; - int specificity_ = 0; - -public: - Selector(Type type, std::string value); - - Type getType() const { return type_; } - const std::string& getValue() const { return value_; } - int getSpecificity() const { return specificity_; } - - // Проверяет, соответствует ли селектор контексту - bool matches(const StyleContext& context) const; - - // Операторы сравнения - bool operator==(const Selector& other) const; - bool operator<(const Selector& other) const; - - struct Hash { - std::size_t operator()(const Selector& s) const; + enum class Combinator { + Descendant, // child or inherit. + Child, // > + Adjacent, // + + GeneralSibling // ~ }; -}; - -struct StyleRule { - std::vector selectors; - std::unordered_map declarations; - int priority = 0; // для !important и других приоритетов - - StyleRule() = default; - StyleRule(std::vector selectors_, - std::unordered_map declarations_); - - // Вычисляет специфичность правила - int getSpecificity() const; - - // Проверяет, подходит ли правило для контекста - bool matches(const StyleContext& context) const; -}; - -class Stylesheet { -private: - std::vector rules_; - - // Индексы для быстрого поиска - std::unordered_map> tag_index_; - std::unordered_map> class_index_; - std::unordered_map> id_index_; - - // Кэш для часто используемых селекторов - mutable std::unordered_map> selector_cache_; - -public: - Stylesheet() = default; - - void addRule(const StyleRule& rule); - void addRule(const std::vector& selectors, - const std::unordered_map& declarations); - - bool parseAndAddRule(const std::string& css_string); - - std::vector getMatchingRules(const StyleContext& context) const; - - // Сортировка правил по специфичности (для каскада) - void sortRulesBySpecificity(); - - void clear(); - - // Получение количества правил - size_t getRuleCount() const { return rules_.size(); } - - // Индексация для ускорения поиска - void buildIndex(); - - // Вспомогательные методы для создания селекторов - static Selector createTagSelector(const std::string& tag); - static Selector createClassSelector(const std::string& className); - static Selector createIDSelector(const std::string& id); - static Selector createUniversalSelector(); - -private: - // Вспомогательные методы для парсинга - std::vector parseSelectors(const std::string& selector_string); - std::unordered_map parseDeclarations(const std::string& declarations_string); - - // Получение или создание селектора с кэшированием - std::shared_ptr getOrCreateSelector(Selector::Type type, const std::string& value) const; -}; - -namespace selectors { - Selector tag(const std::string& name); - Selector cls(const std::string& name); // class - Selector id(const std::string& name); - Selector universal(); - - std::vector combine(const std::vector& a, const std::vector& b); -} + struct SelectorPart { + Combinator combinator; + std::vector selectors; + + SelectorPart() = default; + SelectorPart(Combinator comb, std::vector sel) + : combinator(comb), selectors(std::move(sel)) { + } + }; + + struct ComplexSelector { + std::vector first; + std::vector parts; + + ComplexSelector() = default; + ComplexSelector(std::vector f) : first(std::move(f)) { + } + ComplexSelector( + std::vector f, std::vector p + ) + : first(std::move(f)), parts(std::move(p)) { + } + }; + + struct Selector { + std::variant data; + + Selector() : data(SimpleSelector {}) { + } + Selector(SimpleSelector simple) : data(std::move(simple)) { + } + Selector(ComplexSelector complex) : data(std::move(complex)) { + } + Selector(SimpleSelectorType type, std::string value) + : data(SimpleSelector(type, std::move(value))) { + } + }; + + struct Declaration { + std::string name; + style::value value; + + Declaration() = default; + }; + + struct Rule { + std::vector selectors; + std::vector declarations; + + Rule() = default; + }; + + struct Stylesheet { + std::vector rules; + }; } \ No newline at end of file diff --git a/src/graphics/ui/style/StylesheetParser.cpp b/src/graphics/ui/style/StylesheetParser.cpp index 85f315091..988ba034e 100644 --- a/src/graphics/ui/style/StylesheetParser.cpp +++ b/src/graphics/ui/style/StylesheetParser.cpp @@ -1,464 +1,270 @@ -#include "StylesheetParser.h" +#include "StylesheetParser.hpp" +#include #include -#include -#include #include -namespace style { - -StylesheetParser::ParseResult StylesheetParser::parse(const std::string& css_source) { - source_ = css_source; - position_ = 0; - line_ = 1; - column_ = 1; - - ParseResult result; - result.rules.reserve(16); - - try { - while (position_ < source_.length()) { - skipWhitespace(); - skipComments(); - - if (position_ >= source_.length()) break; - - - auto selectors = parseSelectorGroup(); - if (selectors.empty()) { - return makeError("Expected selector"); - } - - skipWhitespace(); - - - if (!match('{')) { - return makeError("Expected '{' after selector"); - } - - - auto declarations = parseDeclarationBlock(); - - - if (!match('}')) { - return makeError("Expected '}' after declarations"); - } - - position_++; - - - if (!declarations.empty()) { - result.rules.emplace_back(selectors, declarations); - } - - skipWhitespace(); - skipComments(); - } - - result.success = true; - } catch (const std::exception& e) { - result.success = false; - result.error_message = "Exception during parsing: " + std::string(e.what()); - } - - return result; +#include "util/stringutil.hpp" + +using namespace style; + +StylesheetParser::StylesheetParser(std::string_view file, std::string_view source) + : BasicParser(file, source) { + clikeComment = true; } -std::vector StylesheetParser::parseSelectorGroup() { - std::vector selectors; - - while (position_ < source_.length()) { - skipWhitespace(); - - if (position_ >= source_.length()) break; - - - if (peek() == '{' || peek() == '}' || peek() == ';') { - break; - } - - - Selector selector = parseSimpleSelector(); - - selectors.push_back(selector); - - skipWhitespace(); - - - if (position_ >= source_.length() || source_[position_] != ',') { +Stylesheet StylesheetParser::parse() { + Stylesheet stylesheet; + + while (hasNext()) { + skipWhitespace(); // Пропускаем ведущие пробелы, комментарии + + // Проверяем, не достигли ли конца + if (!hasNext()) { break; } - - consume(); - skipWhitespace(); - } - - return selectors; -} -Selector StylesheetParser::parseSimpleSelector() { - if (position_ >= source_.length()) { - return Selector(Selector::Type::Universal, "*"); - } - - char first_char = source_[position_]; - - switch (first_char) { - case '*': - consume(); - return Selector(Selector::Type::Universal, "*"); - - case '#': { - consume(); - std::string id = consumeIdentifier(); - if (id.empty()) { - return Selector(Selector::Type::Universal, "*"); + // Пытаемся распарсить правило (селекторы { декларации }) + try { + auto rule_opt = parseRule(); + if (rule_opt.has_value()) { + stylesheet.rules.push_back(std::move(rule_opt.value())); } - return Selector(Selector::Type::ID, id); - } - - case '.': { - consume(); - std::string className = consumeIdentifier(); - if (className.empty()) { - return Selector(Selector::Type::Universal, "*"); + // Если правило не распарсилось (nullopt), parseRule уже обработал ошибку + } catch (const parsing_error& e) { + std::cerr << "Error parsing rule: " << e.what() << "\n"; + // Пытаемся восстановиться: пропустить до следующей возможной точки + // Простая стратегия: пропустить до следующего '}' + while (hasNext() && peekNoJump() != '}') { + nextChar(); } - return Selector(Selector::Type::Class, className); - } - - default: { - if (isIdentifierStart(first_char)) { - std::string tag = consumeIdentifier(); - return Selector(Selector::Type::Tag, tag); + if (hasNext() && peekNoJump() == '}') { + nextChar(); // Пропускаем '}' } - - return Selector(Selector::Type::Universal, "*"); + // Продолжаем парсинг } } -} -std::unordered_map StylesheetParser::parseDeclarationBlock() { - std::unordered_map declarations; - - while (position_ < source_.length() && peek() != '}') { - skipWhitespace(); - - if (position_ >= source_.length() || peek() == '}') break; - - - auto declaration = parseDeclaration(); - if (declaration.first != PropertyID::Unknown) { - declarations[declaration.first] = declaration.second; - } - - skipWhitespace(); - - - if (position_ < source_.length() && peek() == ';') { - consume(); - } - - skipWhitespace(); - } - - return declarations; + return stylesheet; } -std::pair StylesheetParser::parseDeclaration() { +std::optional StylesheetParser::parseRule() { skipWhitespace(); - - - std::string property_name = consumeIdentifier(); - if (property_name.empty()) { - return {PropertyID::Unknown, value()}; + if (!hasNext()) { + return std::nullopt; } - - skipWhitespace(); - - - if (!match(':')) { - skipUntil(';'); - return {PropertyID::Unknown, value()}; + + // 1. Парсим список селекторов, разделенных запятыми + auto selectors = parseSelectorList(); + if (selectors.empty()) { + // Если не удалось распарсить ни одного селектора + if (!hasNext()) return std::nullopt; + + // Попробуем пропустить непонятный символ и продолжить + std::cerr << "Warning: No selectors found, skipping character.\n"; + if (hasNext()) nextChar(); + return std::nullopt; } - + skipWhitespace(); - - - std::string value_str; - while (position_ < source_.length() && - peek() != ';' && peek() != '}') { - if (!isWhitespace(peek())) { - value_str += consume(); + // 2. Ожидаем открывающую фигурную скобку + try { + expect('{'); + } catch (const parsing_error&) { + std::cerr << "Error: Expected '{' after selectors.\n"; + // Пытаемся восстановиться + while(hasNext() && peekNoJump() != '{' && peekNoJump() != '}') { + nextChar(); + } + if (hasNext() && peekNoJump() == '{') { + nextChar(); } else { - - value_str += consume(); + return std::nullopt; } } - - - PropertyID prop_id = getPropertyID(property_name); - if (prop_id == PropertyID::Unknown) { - return {PropertyID::Unknown, value()}; - } - - - value val = value::fromString(value_str); - - return {prop_id, val}; -} + // 3. Парсим декларации + auto declarations = parseDeclarations(); -void StylesheetParser::skipUntil(char delimiter) { - while (position_ < source_.length() && peek() != delimiter && peek() != '}') { - consume(); - } - if (position_ < source_.length() && peek() == delimiter) { - consume(); + skipWhitespace(); + // 4. Ожидаем закрывающую фигурную скобку + try { + expect('}'); + } catch (const parsing_error&) { + std::cerr << "Error: Expected '}' after declarations.\n"; + // Пытаемся восстановиться + while(hasNext() && peekNoJump() != '}') { + nextChar(); + } + if (hasNext() && peekNoJump() == '}') { + nextChar(); // Пропускаем '}' + } } + + return Rule{std::move(selectors), std::move(declarations)}; } +// Парсит список селекторов, разделенных запятыми +std::vector StylesheetParser::parseSelectorList() { + std::vector selectors; + bool first = true; + do { + if (!first) { + // Пропускаем запятую, если это не первый селектор + skipWhitespace(); + if (hasNext() && peekNoJump() == ',') { + nextChar(); // Пропускаем ',' + } else { + // Если нет запятой, значит, это конец списка селекторов + break; + } + } + first = false; -void StylesheetParser::skipWhitespace() { - while (position_ < source_.length() && isWhitespace(peek())) { - if (peek() == '\n') { - line_++; - column_ = 1; + skipWhitespace(); + + // Определяем конец текущего селектора: ',' или '{' + size_t start_pos = pos; + bool found_delimiter = false; + while (hasNext() && peekNoJump() != ',' && peekNoJump() != '{') { + nextChar(); + } + // pos теперь указывает на ',' или '{' или на конец + + // Извлекаем текст селектора + std::string selector_text(source.substr(start_pos, pos - start_pos)); + + // Триммируем selector_text + size_t first_char = selector_text.find_first_not_of(" \t\n\r\f\v"); + size_t last_char = selector_text.find_last_not_of(" \t\n\r\f\v"); + if (first_char != std::string::npos) { + selector_text = selector_text.substr(first_char, (last_char - first_char + 1)); } else { - column_++; + selector_text.clear(); // Только пробелы } - position_++; - } -} -void StylesheetParser::skipComments() { - while (position_ < source_.length() - 1) { - if (source_[position_] == '/' && source_[position_ + 1] == '*') { - - position_ += 2; - column_ += 2; - - while (position_ < source_.length() - 1) { - if (source_[position_] == '*' && source_[position_ + 1] == '/') { - position_ += 2; - column_ += 2; - break; - } - if (source_[position_] == '\n') { - line_++; - column_ = 1; - } else { - column_++; - } - position_++; - } - } else if (source_[position_] == '/' && source_[position_ + 1] == '/') { - - position_ += 2; - column_ += 2; - - while (position_ < source_.length() && source_[position_] != '\n') { - position_++; - column_++; - } - - if (position_ < source_.length() && source_[position_] == '\n') { - position_++; - line_++; - column_ = 1; + if (!selector_text.empty()) { + try { + // Предполагается, что функция parseCSSSelector существует и возвращает style::Selector + selectors.push_back(parseCSSSelector(selector_text)); + } catch (const std::exception& e) { + std::cerr << "Error parsing selector '" << selector_text << "': " << e.what() << "\n"; } - } else { - break; } - } -} + + } while (hasNext() && peekNoJump() == ','); // Продолжаем, если следующий символ - запятая -char StylesheetParser::peek() const { - if (position_ >= source_.length()) return '\0'; - return source_[position_]; + return selectors; } -char StylesheetParser::peek(size_t offset) const { - if (position_ + offset >= source_.length()) return '\0'; - return source_[position_ + offset]; -} -char StylesheetParser::consume() { - if (position_ >= source_.length()) return '\0'; - - char c = source_[position_++]; - if (c == '\n') { - line_++; - column_ = 1; - } else { - column_++; - } - return c; -} +std::vector StylesheetParser::parseDeclarations() { + std::vector declarations; -bool StylesheetParser::match(char c) { - if (position_ < source_.length() && source_[position_] == c) { - consume(); - return true; - } - return false; -} + while (hasNext()) { + skipWhitespace(); + // Если встречаем '}', значит блок закончился + if (peekNoJump() == '}') { + break; + } -bool StylesheetParser::match(const std::string& str) { - if (position_ + str.length() <= source_.length()) { - if (source_.substr(position_, str.length()) == str) { - for (size_t i = 0; i < str.length(); ++i) { - consume(); + // Пытаемся распарсить одну декларацию + auto decl_opt = parseDeclaration(); + if (decl_opt.has_value()) { + declarations.push_back(std::move(decl_opt.value())); + } else { + // Если не удалось распарсить декларацию, пропускаем до ';' + // или до конца блока, чтобы избежать зависания + std::cerr << "Warning: Could not parse declaration near line " << (line + 1) << "\n"; + while(hasNext() && peekNoJump() != ';' && peekNoJump() != '}') { + nextChar(); + } + if (hasNext() && peekNoJump() == ';') { + nextChar(); // Пропускаем ';' } - return true; } } - return false; -} - -std::string StylesheetParser::consumeWhile(bool (*predicate)(char)) { - std::string result; - while (position_ < source_.length() && predicate(peek())) { - result += consume(); - } - return result; -} -std::string StylesheetParser::consumeIdentifier() { - return consumeWhile([](char c) { - return isIdentifierChar(c); - }); + return declarations; } -std::string StylesheetParser::consumeString() { - if (position_ >= source_.length()) return ""; - - char quote = peek(); - if (quote != '"' && quote != '\'') return ""; - - consume(); - - std::string result; - while (position_ < source_.length() && peek() != quote) { - if (peek() == '\\') { - consume(); - if (position_ < source_.length()) { - result += consume(); - } - } else { - result += consume(); +std::optional StylesheetParser::parseDeclaration() { + try { + skipWhitespace(); + if (!hasNext() || peekNoJump() == '}') { + return std::nullopt; // Нечего парсить } - } - - if (position_ < source_.length() && peek() == quote) { - consume(); - } - - return result; -} -std::string StylesheetParser::consumeNumber() { - return consumeWhile([](char c) { - return isNumberChar(c); - }); -} + // 1. Парсим имя свойства (property name) + std::string property_name = parseCSSIdentifier(); + if (property_name.empty()) { + // Пропускаем странный символ + if(hasNext()) nextChar(); + return std::nullopt; + } + skipWhitespace(); + // 2. Ожидаем двоеточие + expect(':'); -std::vector StylesheetParser::splitSelectors(const std::string& selector_string) { - std::vector selectors; - std::string current; - int bracket_count = 0; - - for (char c : selector_string) { - if (c == ',' && bracket_count == 0) { - if (!current.empty()) { - selectors.push_back(current); - current.clear(); - } + // 3. Парсим значение свойства (пока как строку) + // TODO: Здесь нужно будет реализовать полноценный парсер значений CSS + skipWhitespace(); + size_t start = pos; + // Читаем до ';' или '}' + while (hasNext() && peekNoJump() != ';' && peekNoJump() != '}') { + nextChar(); + } + std::string property_value_str = std::string(source.substr(start, pos - start)); + // Триммим значение + size_t first = property_value_str.find_first_not_of(" \t\n\r\f\v"); + size_t last = property_value_str.find_last_not_of(" \t\n\r\f\v"); + if (first != std::string::npos) { + property_value_str = property_value_str.substr(first, (last - first + 1)); } else { - if (c == '(') bracket_count++; - else if (c == ')') bracket_count--; - current += c; + property_value_str.clear(); // Только пробелы } - } - - if (!current.empty()) { - selectors.push_back(current); - } - - return selectors; -} - -std::pair StylesheetParser::splitDeclaration(const std::string& declaration) { - size_t colon_pos = declaration.find(':'); - if (colon_pos == std::string::npos) { - return {"", ""}; - } - - std::string property = declaration.substr(0, colon_pos); - std::string value = declaration.substr(colon_pos + 1); - - - property.erase(0, property.find_first_not_of(" \t\r\n")); - property.erase(property.find_last_not_of(" \t\r\n") + 1); - - value.erase(0, value.find_first_not_of(" \t\r\n")); - value.erase(value.find_last_not_of(" \t\r\n") + 1); - - return {property, value}; -} - -bool StylesheetParser::isWhitespace(char c) { - return c == ' ' || c == '\t' || c == '\n' || c == '\r'; -} - -bool StylesheetParser::isIdentifierStart(char c) { - return std::isalpha(c) || c == '_' || c == '-'; -} - -bool StylesheetParser::isIdentifierChar(char c) { - return std::isalnum(c) || c == '_' || c == '-'; -} - -bool StylesheetParser::isNumberChar(char c) { - return std::isdigit(c) || c == '.' || c == '-' || c == '+'; -} + // 4. Ожидаем точку с запятой + skipWhitespace(); + if (hasNext() && peekNoJump() == ';') { + nextChar(); // Пропускаем ';' + } else if (hasNext() && peekNoJump() != '}') { + // Если нет ';' и это не конец блока, это ошибка + throw error("';' expected"); + } + // TODO: Здесь нужно будет преобразовать property_value_str в style::value + // Пока что просто создаем строковое значение + style::value property_value = style::value(property_value_str); // Предполагаем конструктор из строки -StylesheetParser::ParseResult StylesheetParser::makeError(const std::string& message) const { - ParseResult result; - result.success = false; - result.error_message = message + " at line " + std::to_string(line_) + - ", column " + std::to_string(column_); - return result; -} + return Declaration{std::move(property_name), std::move(property_value)}; -std::string StylesheetParser::getCurrentPosition() const { - return "line " + std::to_string(line_) + ", column " + std::to_string(column_); + } catch (const parsing_error& e) { + std::cerr << "Declaration parsing error: " << e.what() << "\n"; + return std::nullopt; + } } - -namespace parser { - StylesheetParser::ParseResult parseCSS(const std::string& css_string) { - StylesheetParser parser; - return parser.parse(css_string); +// Парсит CSS-идентификатор +std::string StylesheetParser::parseCSSIdentifier() { + if (!hasNext() || !is_css_identifier_start(peekNoJump())) { + return ""; } - - bool parseAndAddToStylesheet(Stylesheet& stylesheet, const std::string& css_string) { - auto result = parseCSS(css_string); - if (result.success) { - for (const auto& rule : result.rules) { - stylesheet.addRule(rule); - } - return true; - } - std::cerr << "CSS Parse Error: " << result.error_message << std::endl; - return false; + size_t start = pos; + while (hasNext() && is_css_identifier_part(peekNoJump())) { + nextChar(); } + return std::string(source.substr(start, pos - start)); } - -bool Stylesheet::parseAndAddRule(const std::string& css_string) { - return parser::parseAndAddToStylesheet(*this, css_string); +// Проверяет, является ли символ допустимым для начала CSS идентификатора +bool StylesheetParser::is_css_identifier_start(char c) const { + return is_identifier_start(c) || c == '-'; // CSS разрешает '-' в начале (после которого должна быть буква) + // Более точная проверка для '-' в начале потребовала бы просмотра следующего символа + // Для простоты разрешим '-' везде, где разрешены идентификаторы } -} \ No newline at end of file +// Проверяет, является ли символ допустимым для продолжения CSS идентификатора +bool StylesheetParser::is_css_identifier_part(char c) const { + return is_identifier_part(c) || c == '-'; // CSS разрешает '-' в середине/конце +} \ No newline at end of file diff --git a/src/graphics/ui/style/StylesheetParser.h b/src/graphics/ui/style/StylesheetParser.h deleted file mode 100644 index 5782cc7a9..000000000 --- a/src/graphics/ui/style/StylesheetParser.h +++ /dev/null @@ -1,83 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include "Stylesheet.h" - -namespace style { - -/** - * Парсер CSS-подобных стилей - */ -class StylesheetParser { -public: - struct ParseResult { - bool success = false; - std::string error_message; - std::vector rules; - }; - -private: - std::string source_; - size_t position_ = 0; - size_t line_ = 1; - size_t column_ = 1; - -public: - StylesheetParser() = default; - - - ParseResult parse(const std::string& css_source); - - - std::vector parseSelector(const std::string& selector_string); - std::unordered_map parseDeclarations(const std::string& declarations_string); - - - static std::vector splitSelectors(const std::string& selector_string); - static std::pair splitDeclaration(const std::string& declaration); - -private: - - void skipUntil(char delimiter); - void skipWhitespace(); - void skipComments(); - char peek() const; - char peek(size_t offset) const; - char consume(); - bool match(char c); - bool match(const std::string& str); - std::string consumeWhile(bool (*predicate)(char)); - std::string consumeIdentifier(); - std::string consumeString(); - std::string consumeNumber(); - - - std::vector parseSelectorGroup(); - Selector parseSimpleSelector(); - std::unordered_map parseDeclarationBlock(); - std::pair parseDeclaration(); - - - ParseResult makeError(const std::string& message) const; - std::string getCurrentPosition() const; - - - static bool isWhitespace(char c); - static bool isIdentifierStart(char c); - static bool isIdentifierChar(char c); - static bool isNumberChar(char c); -}; - - -namespace parser { - StylesheetParser::ParseResult parseCSS(const std::string& css_string); - - - bool parseAndAddToStylesheet(Stylesheet& stylesheet, const std::string& css_string); -} - -} \ No newline at end of file diff --git a/src/graphics/ui/style/StylesheetParser.hpp b/src/graphics/ui/style/StylesheetParser.hpp new file mode 100644 index 000000000..3bd47071e --- /dev/null +++ b/src/graphics/ui/style/StylesheetParser.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "SelectorParser.hpp" +#include "coders/BasicParser.hpp" +#include "stylesheet.h" + +class StylesheetParser : public BasicParser { +public: + StylesheetParser(std::string_view file, std::string_view source); + + style::Stylesheet parse(); +private: + std::optional parseRule(); + std::vector parseSelectorList(); + std::vector parseDeclarations(); + std::optional parseDeclaration(); + std::string parseCSSIdentifier(); + + bool is_css_identifier_start(char c) const; + bool is_css_identifier_part(char c) const; + +}; diff --git a/src/graphics/ui/style/value.cpp b/src/graphics/ui/style/value.cpp index 26b87261d..470385ce0 100644 --- a/src/graphics/ui/style/value.cpp +++ b/src/graphics/ui/style/value.cpp @@ -376,7 +376,6 @@ std::size_t value::Hash::operator()(const value& v) const { default: return 0; } - return 0; } diff --git a/test/test.css b/test/test.css new file mode 100644 index 000000000..b89cd74b6 --- /dev/null +++ b/test/test.css @@ -0,0 +1,3 @@ +* { + color: white; +} \ No newline at end of file From e313a9e3b6a6354a500ead136ea75687a151fc8f Mon Sep 17 00:00:00 2001 From: GHOST11111100 Date: Wed, 6 Aug 2025 23:29:58 +0300 Subject: [PATCH 07/11] feat: Enhance Node and ElementData functionality - Added root property to Node structure to identify root nodes. - Implemented methods in ElementData for managing ID and class attributes: - getId, setId for ID management. - getClassList, setClassList, addClass, removeClass, hasClass, toggleClass for class management. - Updated Node class to include is_root method. - Introduced ComputedStyle structure in Stylesheet for managing CSS properties. - Created DeclarationParser for parsing CSS declarations. - Implemented StyleComputer for computing styles based on stylesheets and node structure. - Enhanced StylesheetParser to support parsing of stylesheets with improved error handling. --- src/graphics/ui/GUI.cpp | 22 +- src/graphics/ui/elements/Node.cpp | 102 +++++ src/graphics/ui/elements/Node.hpp | 104 ++++- src/graphics/ui/style/DeclarationParser.cpp | 126 ++++++ src/graphics/ui/style/DeclarationParser.hpp | 34 ++ src/graphics/ui/style/StyleComputer.cpp | 409 ++++++++++++++++++++ src/graphics/ui/style/StyleComputer.hpp | 39 ++ src/graphics/ui/style/Stylesheet.h | 59 +++ src/graphics/ui/style/StylesheetParser.cpp | 167 ++++---- src/graphics/ui/style/StylesheetParser.hpp | 1 + 10 files changed, 959 insertions(+), 104 deletions(-) create mode 100644 src/graphics/ui/style/DeclarationParser.cpp create mode 100644 src/graphics/ui/style/DeclarationParser.hpp create mode 100644 src/graphics/ui/style/StyleComputer.cpp create mode 100644 src/graphics/ui/style/StyleComputer.hpp diff --git a/src/graphics/ui/GUI.cpp b/src/graphics/ui/GUI.cpp index f767c225c..45e0ba44e 100644 --- a/src/graphics/ui/GUI.cpp +++ b/src/graphics/ui/GUI.cpp @@ -19,6 +19,7 @@ #include "graphics/core/LineBatch.hpp" #include "graphics/core/Shader.hpp" #include "gui_util.hpp" +#include "style/StyleComputer.hpp" #include "style/StylesheetParser.hpp" #include "window/Camera.hpp" #include "window/Window.hpp" @@ -59,24 +60,16 @@ GUI::GUI(Engine& engine) // Testing new UI Implementation std::string xmlString = R"( - - - -
- 123-456-7890 - - +
+test +
)"; std::string css_code = R"( // comment variant 1 /* comment variant 2 */ - div.hover#open:focus + span:hover, button { + #id:hover { color: red; } - - span { - background: white red; - } )"; Node root = Node::from_xml_string("example.xml", xmlString); @@ -84,10 +77,11 @@ GUI::GUI(Engine& engine) StylesheetParser parser("test.css", css_code); style::Stylesheet stylesheet = parser.parse(); + style::StyleComputer computer; + computer.set_stylesheets({stylesheet}); + computer.compute(root); } - - GUI::~GUI() = default; void GUI::setPageLoader(PageLoaderFunc pageLoader) { diff --git a/src/graphics/ui/elements/Node.cpp b/src/graphics/ui/elements/Node.cpp index ec383dea2..e2f39b2a4 100644 --- a/src/graphics/ui/elements/Node.cpp +++ b/src/graphics/ui/elements/Node.cpp @@ -1,7 +1,10 @@ #pragma once +#include + #include "Node.hpp" + Node Node::from_xml_node(const xml::Node& xml_node) { if (xml_node.isText()) { @@ -15,6 +18,7 @@ Node Node::from_xml_node(const xml::Node& xml_node) { } Node dom_node(xml_node.getTag(), std::move(attrs)); + dom_node.root = true; for (size_t i = 0; i < xml_node.size(); ++i) { const xml::Node& child = xml_node.sub(i); @@ -37,6 +41,7 @@ Node Node::from_xml_string(std::string_view filename, std::string_view source) { return from_xml_document(*xml_doc); } + // Working with childs // @@ -314,4 +319,101 @@ const Node* Node::find_by_path_impl(const std::vector& parts, size_ const std::vector& Node::get_children() const { return children; +} + +bool Node::is_root() const { + return root; +} + +// Получение ID +std::optional ElementData::getId() const { + auto it = attributes.find("id"); + if (it != attributes.end()) { + return it->second; + } + return std::nullopt; +} + +// Установка ID +void ElementData::setId(std::string_view id) { + if (id.empty()) { + attributes.erase("id"); + } else { + attributes["id"] = std::string(id); + } +} + +// Получение списка классов +std::vector ElementData::getClassList() const { + std::vector classes; + auto it = attributes.find("class"); + if (it != attributes.end() && !it->second.empty()) { + std::istringstream iss(it->second); + std::string cls; + while (iss >> cls) { + if (!cls.empty()) { + classes.push_back(cls); + } + } + } + return classes; +} + +// Установка списка классов +void ElementData::setClassList(const std::vector& classes) { + if (classes.empty()) { + attributes.erase("class"); + return; + } + + std::string class_string; + for (size_t i = 0; i < classes.size(); ++i) { + if (i > 0) { + class_string += " "; + } + class_string += classes[i]; + } + attributes["class"] = class_string; +} + +// Добавление класса +void ElementData::addClass(std::string_view className) { + if (className.empty()) return; + + auto classes = getClassList(); + if (std::find(classes.begin(), classes.end(), className) == classes.end()) { + classes.push_back(std::string(className)); + setClassList(classes); + } +} + +// Удаление класса +void ElementData::removeClass(std::string_view className) { + if (className.empty()) return; + + auto classes = getClassList(); + auto it = std::find(classes.begin(), classes.end(), className); + if (it != classes.end()) { + classes.erase(it); + setClassList(classes); + } +} + +// Проверка наличия класса +bool ElementData::hasClass(std::string_view className) const { + if (className.empty()) return false; + + auto classes = getClassList(); + return std::find(classes.begin(), classes.end(), className) != classes.end(); +} + +// Переключение класса +void ElementData::toggleClass(std::string_view className) { + if (className.empty()) return; + + if (hasClass(className)) { + removeClass(className); + } else { + addClass(className); + } } \ No newline at end of file diff --git a/src/graphics/ui/elements/Node.hpp b/src/graphics/ui/elements/Node.hpp index 3783fad2d..e20bc526e 100644 --- a/src/graphics/ui/elements/Node.hpp +++ b/src/graphics/ui/elements/Node.hpp @@ -1,13 +1,14 @@ #pragma once +#include #include #include #include #include #include -#include #include "coders/xml.hpp" +#include "graphics/ui/style/Stylesheet.h" using AttributesMap = std::unordered_map; @@ -15,30 +16,93 @@ struct ElementData { std::string tag_name; AttributesMap attributes; - ElementData(std::string_view tag, AttributesMap attrs): tag_name(tag), attributes(std::move(attrs)) {} + ElementData(std::string_view tag, AttributesMap attrs) : tag_name(tag), attributes(std::move(attrs)) {} + + std::optional getId() const; + void setId(std::string_view id); + + std::vector getClassList() const; + void setClassList(const std::vector& classes); + void addClass(std::string_view className); + void removeClass(std::string_view className); + bool hasClass(std::string_view className) const; + void toggleClass(std::string_view className); }; struct Text { std::string content; - Text(std::string_view text_content) : content(text_content) {} + Text(std::string_view text_content) : content(text_content) { + } +}; + +struct ElementState { + bool hover = false; + bool focus = false; + bool active = false; + bool checked = false; + bool disabled = false; + size_t nth_child = 0; + size_t nth_of_type = 0; + + // Оператор для использования в хэш-таблицах + bool operator==(const ElementState& other) const { + return hover == other.hover && focus == other.focus && + active == other.active && checked == other.checked && + disabled == other.disabled && nth_child == other.nth_child && + nth_of_type == other.nth_of_type; + } +}; + +struct ElementStateHash { + std::size_t operator()(const ElementState& state) const { + size_t seed = 0; + + // Простая реализация hash_combine + auto hash_combine = [](size_t& seed, size_t value) { + seed ^= value + 0x9e3779b9 + (seed << 6) + (seed >> 2); + }; + + hash_combine(seed, std::hash {}(state.hover)); + hash_combine(seed, std::hash {}(state.focus)); + hash_combine(seed, std::hash {}(state.active)); + hash_combine(seed, std::hash {}(state.checked)); + hash_combine(seed, std::hash {}(state.disabled)); + hash_combine(seed, std::hash {}(state.nth_child)); + hash_combine(seed, std::hash {}(state.nth_of_type)); + + return seed; + } }; struct Element { ElementData data; - - Element(std::string_view tag, AttributesMap attrs): data(tag, std::move(attrs)) {} + ElementState state; + style::ComputedStyle style; + std::unordered_map + state_styles; + + Element(std::string_view tag, AttributesMap attrs) + : data(tag, std::move(attrs)) { + } }; using NodeType = std::variant; struct Node { + bool root = false; std::vector children; NodeType node_type; - Node(std::string_view text): node_type(Text(text)) {} - Node(std::string_view tag, AttributesMap attrs): node_type(Element(tag, std::move(attrs))) {} - Node(std::string_view tag, AttributesMap attrs, std::vector childs): children(std::move(childs)), node_type(Element(tag, std::move(attrs))) {} + Node(std::string_view text) : node_type(Text(text)) { + } + Node(std::string_view tag, AttributesMap attrs) + : node_type(Element(tag, std::move(attrs))) { + } + Node(std::string_view tag, AttributesMap attrs, std::vector childs) + : children(std::move(childs)), + node_type(Element(tag, std::move(attrs))) { + } // Working with childs void append_child(Node node); @@ -55,6 +119,7 @@ struct Node { bool is_element() const; size_t child_count() const; const std::vector& get_children() const; + bool is_root() const; // Working with attributes void set_attribute(std::string_view name, std::string_view value); @@ -62,22 +127,27 @@ struct Node { bool has_attribute(std::string_view name) const; std::optional get_attribute(std::string_view name) const; const AttributesMap& get_attributes() const; - + // Search std::vector find_by_tag(std::string_view tag); std::vector find_by_tag(std::string_view tag) const; Node* find_first_by_tag(std::string_view tag); const Node* find_first_by_tag(std::string_view tag) const; - Node* find_by_path(std::string_view path); // "div>p>span" + Node* find_by_path(std::string_view path); // "div>p>span" const Node* find_by_path(std::string_view path) const; // Parse from XML - static Node from_xml_string(std::string_view filename, std::string_view source); + static Node from_xml_string( + std::string_view filename, std::string_view source + ); static Node from_xml_document(const xml::Document& xml_doc); - - private: - static Node from_xml_node(const xml::Node& xml_node); - - Node* find_by_path_impl(const std::vector& parts, size_t index); - const Node* find_by_path_impl(const std::vector& parts, size_t index) const; +private: + static Node from_xml_node(const xml::Node& xml_node); + + Node* find_by_path_impl( + const std::vector& parts, size_t index + ); + const Node* find_by_path_impl( + const std::vector& parts, size_t index + ) const; }; diff --git a/src/graphics/ui/style/DeclarationParser.cpp b/src/graphics/ui/style/DeclarationParser.cpp new file mode 100644 index 000000000..fd133ff22 --- /dev/null +++ b/src/graphics/ui/style/DeclarationParser.cpp @@ -0,0 +1,126 @@ +#include "DeclarationParser.hpp" +#include +#include +#include "util/stringutil.hpp" + +DeclarationParser::DeclarationParser(std::string_view file, std::string_view source) + : BasicParser(file, source) { + // Включаем поддержку C-подобных комментариев + this->clikeComment = true; +} + +std::vector DeclarationParser::parseDeclarations() { + std::vector declarations; + + while (hasNext()) { + skipWhitespace(); + // Если встречаем '}' или конец, значит блок закончился + // Для inline стиля это будет просто конец + if (!hasNext() || peekNoJump() == '}') { + break; + } + + // Пытаемся распарсить одну декларацию + auto decl_opt = parseDeclaration(); + if (decl_opt.has_value()) { + declarations.push_back(std::move(decl_opt.value())); + } else { + // Если не удалось распарсить декларацию, пропускаем до ';' + // или до конца блока/ввода, чтобы избежать зависания + std::cerr << "Warning: Could not parse declaration near line " << (line + 1) << "\n"; + while(hasNext() && peekNoJump() != ';' && peekNoJump() != '}') { + nextChar(); + } + if (hasNext() && peekNoJump() == ';') { + nextChar(); // Пропускаем ';' + } + } + } + + return declarations; +} + +std::optional DeclarationParser::parseDeclaration() { + try { + skipWhitespace(); + if (!hasNext() || peekNoJump() == '}') { + return std::nullopt; // Нечего парсить или конец блока + } + + // 1. Парсим имя свойства (property name) + std::string property_name = parseCSSIdentifier(); + if (property_name.empty()) { + // Пропускаем странный символ + if(hasNext()) nextChar(); + return std::nullopt; + } + + skipWhitespace(); + // 2. Ожидаем двоеточие + expect(':'); + + // 3. Парсим значение свойства (пока как строку) + skipWhitespace(); + size_t start = pos; + // Читаем до ';' или '}' или конца + while (hasNext() && peekNoJump() != ';' && peekNoJump() != '}') { + nextChar(); + } + std::string property_value_str = std::string(source.substr(start, pos - start)); + // Триммим значение + size_t first = property_value_str.find_first_not_of(" \t\n\r\f\v"); + size_t last = property_value_str.find_last_not_of(" \t\n\r\f\v"); + if (first != std::string::npos) { + property_value_str = property_value_str.substr(first, (last - first + 1)); + } else { + property_value_str.clear(); // Только пробелы + } + + // 4. Ожидаем точку с запятой или проверяем на конец/'}' + skipWhitespace(); + if (hasNext() && peekNoJump() == ';') { + nextChar(); // Пропускаем ';' + } else if (hasNext() && peekNoJump() != '}') { + // Если нет ';' и это не конец блока, это ошибка + throw error("';' expected"); + } + // Если следующий символ '}' или конец ввода, ';' не обязателен (для последней декларации в блоке) + + // TODO: Здесь нужно будет преобразовать property_value_str в style::value + style::value property_value = style::value(property_value_str); // Предполагаем конструктор из строки + + return style::Declaration{std::move(property_name), std::move(property_value)}; + + } catch (const parsing_error& e) { + std::cerr << "Declaration parsing error: " << e.what() << "\n"; + return std::nullopt; + } +} + +// Парсит CSS-идентификатор +std::string DeclarationParser::parseCSSIdentifier() { + if (!hasNext() || !is_css_identifier_start(peekNoJump())) { + return ""; + } + size_t start = pos; + while (hasNext() && is_css_identifier_part(peekNoJump())) { + nextChar(); + } + return std::string(source.substr(start, pos - start)); +} + +// Проверяет, является ли символ допустимым для начала CSS идентификатора +bool DeclarationParser::is_css_identifier_start(char c) const { + return is_identifier_start(c) || c == '-'; +} + +// Проверяет, является ли символ допустимым для продолжения CSS идентификатора +bool DeclarationParser::is_css_identifier_part(char c) const { + return is_identifier_part(c) || c == '-'; +} + +// --- Внешняя функция для удобства --- +std::vector parseInlineStyle(std::string_view style_content) { + DeclarationParser parser("inline-style", style_content); + return parser.parseDeclarations(); +} \ No newline at end of file diff --git a/src/graphics/ui/style/DeclarationParser.hpp b/src/graphics/ui/style/DeclarationParser.hpp new file mode 100644 index 000000000..27bae8808 --- /dev/null +++ b/src/graphics/ui/style/DeclarationParser.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include "coders/BasicParser.hpp" +#include "Stylesheet.h" +#include +#include +#include + + +using namespace style; + +class DeclarationParser : public BasicParser { +public: + // Конструктор принимает строку с декларациями (без { и }) + DeclarationParser(std::string_view file, std::string_view source); + + // Парсит список деклараций, разделенных ';' + // Используется как для блоков правил {...}, так и для inline стилей + std::vector parseDeclarations(); + +private: + // Парсит одну декларацию property: value; + std::optional parseDeclaration(); + + // Парсит CSS-идентификатор (имя свойства) + std::string parseCSSIdentifier(); + + // Проверяет, является ли символ допустимым для начала/продолжения CSS идентификатора + bool is_css_identifier_start(char c) const; + bool is_css_identifier_part(char c) const; +}; + +// Удобная внешняя функция для парсинга inline стилей +std::vector parseInlineStyle(std::string_view style_content); \ No newline at end of file diff --git a/src/graphics/ui/style/StyleComputer.cpp b/src/graphics/ui/style/StyleComputer.cpp new file mode 100644 index 000000000..d4b6e0331 --- /dev/null +++ b/src/graphics/ui/style/StyleComputer.cpp @@ -0,0 +1,409 @@ +#include "StyleComputer.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "DeclarationParser.hpp" + +using namespace style; + +void StyleComputer::set_stylesheets(const std::vector& sheets) { + stylesheet.rules.clear(); + size_t total_rules = 0; + for (const auto& sheet : sheets) { + total_rules += sheet.rules.size(); + } + stylesheet.rules.reserve(total_rules); + + for (const auto& sheet : sheets) { + stylesheet.rules.insert( + stylesheet.rules.end(), sheet.rules.begin(), sheet.rules.end() + ); + } +} + +void StyleComputer::compute(Node& node) { + std::queue> queue; + + queue.push({&node, nullptr}); + + while (!queue.empty()) { + auto [current_node, parent_node] = queue.front(); + queue.pop(); + + if (parent_node) { + compute_base(*current_node, *parent_node); + } else { + compute_base(*current_node); + } + + if (auto* element = std::get_if(¤t_node->node_type)) { + for (auto& child : current_node->children) { + queue.push({&child, current_node}); + } + } + } +} + +void StyleComputer::compute_base(Node& node, Node& parent) { + if (auto* element = std::get_if(&node.node_type)) { + if (auto* parent_element = std::get_if(&parent.node_type)) { + element->style = parent_element->style; + element->state_styles = parent_element->state_styles; + } + } + + compute_base(node); +} + +void StyleComputer::compute_base(Node& node) { + // Базовый стиль (без псевдоклассов) + ComputedStyle base_style; + + // Мап для стилей псевдоклассов + std::unordered_map + state_styles; + + // Проходим по всем правилам + for (auto& rule : stylesheet.rules) { + for (auto& sel : rule.selectors) { + if (match_selector(node, sel)) { + // Проверяем, есть ли псевдоклассы в селекторе + ElementState state = extract_element_state(sel); + + if (is_default_state(state)) { + // Базовый стиль (без псевдоклассов) + for (const auto& declaration : rule.declarations) { + base_style.set(declaration.name, declaration.value); + } + } else { + // Стиль для конкретного состояния + auto& state_style = state_styles[state]; + for (const auto& declaration : rule.declarations) { + state_style.set(declaration.name, declaration.value); + } + } + } + } + } + + // Применяем inline стили (они всегда к базовому состоянию) + auto inline_style_attr = node.get_attribute("style"); + if (inline_style_attr && !inline_style_attr->empty()) { + auto inline_declarations = parseInlineStyle(*inline_style_attr); + for (const auto& decl : inline_declarations) { + base_style.set(decl.name, decl.value); + } + } + + // Сохраняем стили в элемент + if (auto* element = std::get_if(&node.node_type)) { + element->style = std::move(base_style); + element->state_styles = std::move(state_styles); + } +} + +// Проверяет, является ли состояние базовым (по умолчанию) +bool StyleComputer::is_default_state(const ElementState& state) { + return !state.hover && !state.focus && !state.active && !state.checked && + !state.disabled && state.nth_child == 0 && state.nth_of_type == 0; +} + +// Извлекает состояние элемента из селектора +ElementState StyleComputer::extract_element_state(const Selector& selector) { + ElementState state; + + std::visit( + [this, &state](const auto& sel) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + this->extract_state_from_simple_selector(sel, state); + } else if constexpr (std::is_same_v) { + this->extract_state_from_complex_selector(sel, state); + } + }, + selector.data + ); + + return state; +} + +void StyleComputer::extract_state_from_simple_selector( + const SimpleSelector& selector, ElementState& state +) { + for (const auto& [pseudo_class, value] : selector.pseudoClasses) { + switch (pseudo_class) { + case PseudoClass::Hover: + state.hover = true; + break; + case PseudoClass::Focus: + state.focus = true; + break; + case PseudoClass::Active: + state.active = true; + break; + case PseudoClass::Checked: + state.checked = true; + break; + case PseudoClass::Disabled: + state.disabled = true; + break; + case PseudoClass::NthChild: + // Парсим значение nth-child + try { + state.nth_child = std::stoull(value); + } catch (...) { + state.nth_child = 0; + } + break; + case PseudoClass::NthOfType: + // Парсим значение nth-of-type + try { + state.nth_of_type = std::stoull(value); + } catch (...) { + state.nth_of_type = 0; + } + break; + default: + break; + } + } +} + +void StyleComputer::extract_state_from_complex_selector( + const ComplexSelector& selector, ElementState& state +) { + // Извлекаем состояние из first селекторов + for (const auto& simple_sel : selector.first) { + extract_state_from_simple_selector(simple_sel, state); + } + + // Извлекаем состояние из всех частей + for (const auto& part : selector.parts) { + for (const auto& simple_sel : part.selectors) { + extract_state_from_simple_selector(simple_sel, state); + } + } +} + +bool StyleComputer::match_selector( + Node& node, Selector& selector, Node* parent +) { + return std::visit( + [this, &node, parent](auto& sel) { + return this->process_selector_variant(node, sel, parent); + }, + selector.data + ); +} + +template +bool StyleComputer::process_selector_variant( + Node& node, const T& selector, Node* parent +) { + if constexpr (std::is_same_v, SimpleSelector>) { + return process_simple_selector(node, selector); + } else if constexpr (std::is_same_v, ComplexSelector>) { + return process_complex_selector(node, selector, parent); + } +} + +bool StyleComputer::process_simple_selector( + Node& node, const SimpleSelector& selector +) { + if (auto* element = std::get_if(&node.node_type)) { + bool basic_match = false; + switch (selector.type) { + case SimpleSelectorType::Universal: + basic_match = true; + break; + + case SimpleSelectorType::Tag: + if (selector.value.empty()) { + basic_match = true; + } else { + basic_match = element->data.tag_name == selector.value; + } + break; + + case SimpleSelectorType::ID: { + auto element_id = element->data.getId(); + basic_match = element_id.has_value() && + element_id.value() == selector.value; + break; + } + + case SimpleSelectorType::Class: + basic_match = element->data.hasClass(selector.value); + break; + } + + if (basic_match) { + return check_pseudo_classes(node, selector); + } + + return false; + } + return false; +} + +bool StyleComputer::process_complex_selector( + Node& node, ComplexSelector selector, Node* parent +) { + // Проверяем first селекторы на текущем узле + for (const auto& simple_selector : selector.first) { + if (!process_simple_selector(node, simple_selector)) { + return false; + } + } + + // Если нет дополнительных частей, то успешно + if (selector.parts.empty()) { + return true; + } + + // Пока реализуем только простые случаи с одной частью + if (selector.parts.size() == 1) { + const SelectorPart& part = selector.parts[0]; + Node* related_node = nullptr; + + // Определяем связанный узел в зависимости от комбинатора + switch (part.combinator) { + case Combinator::Child: + case Combinator::Descendant: + related_node = parent; // Родитель передается как параметр + break; + + case Combinator::Adjacent: + // Для соседа нужно получить предыдущий sibling + // Пока возвращаем nullptr, так как у нас нет доступа к siblings + related_node = nullptr; + break; + + case Combinator::GeneralSibling: + // Для общего соседа тоже нужен доступ к siblings + related_node = nullptr; + break; + } + + // Если связанный узел существует, проверяем селекторы в части + if (related_node) { + for (const auto& simple_selector : part.selectors) { + if (!process_simple_selector(*related_node, simple_selector)) { + return false; + } + } + return true; + } + } + + // Пока не поддерживаем более сложные случаи + return selector.parts + .empty(); // true только если нет дополнительных частей +} + +bool StyleComputer::check_pseudo_classes( + Node& node, const SimpleSelector& selector +) { + if (selector.pseudoClasses.empty()) { + return true; + } + + if (auto* element = std::get_if(&node.node_type)) { + for (const auto& [pseudo_class, value] : selector.pseudoClasses) { + switch (pseudo_class) { + case PseudoClass::Hover: + if (!element->state.hover) return false; + break; + + case PseudoClass::Focus: + if (!element->state.focus) return false; + break; + + case PseudoClass::Active: + if (!element->state.active) return false; + break; + + case PseudoClass::Checked: + if (!element->state.checked) return false; + break; + + case PseudoClass::Disabled: + if (!element->state.disabled) return false; + break; + + case PseudoClass::NthChild: { + if (!check_nth_child(*element, value)) return false; + break; + } + + case PseudoClass::NthOfType: { + if (!check_nth_of_type(*element, value)) return false; + break; + } + + case PseudoClass::Custom: + if (!check_custom_pseudo_class(*element, value)) + return false; + break; + + case PseudoClass::None: + break; + } + } + return true; + } + + return false; +} + +bool StyleComputer::check_nth_child( + const Element& element, const std::string& formula +) { + if (formula == "odd") { + return element.state.nth_child % 2 == 1; + } else if (formula == "even") { + return element.state.nth_child % 2 == 0; + } else { + try { + size_t pos; + int n = std::stoi(formula, &pos); + if (pos == formula.length()) { + return static_cast(element.state.nth_child) == n; + } + } catch (...) { + } + } + return false; +} + +bool StyleComputer::check_nth_of_type( + const Element& element, const std::string& formula +) { + if (formula == "odd") { + return element.state.nth_of_type % 2 == 1; + } else if (formula == "even") { + return element.state.nth_of_type % 2 == 0; + } else { + try { + size_t pos; + int n = std::stoi(formula, &pos); + if (pos == formula.length()) { + return static_cast(element.state.nth_of_type) == n; + } + } catch (...) { + } + } + return false; +} + +bool StyleComputer::check_custom_pseudo_class( + const Element& element, const std::string& name +) { + return false; +} \ No newline at end of file diff --git a/src/graphics/ui/style/StyleComputer.hpp b/src/graphics/ui/style/StyleComputer.hpp new file mode 100644 index 000000000..81f144684 --- /dev/null +++ b/src/graphics/ui/style/StyleComputer.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include +#include + +#include "../elements/Node.hpp" +#include "Stylesheet.h" + +namespace style { + class StyleComputer { + public: + StyleComputer() = default; + + void set_stylesheets(const std::vector& sheets); + void compute(Node& node); + private: + Stylesheet stylesheet; + void compute_base(Node& node, Node& parent); + void compute_base(Node& node); + + bool match_selector(Node& node, Selector& selector, Node* parent = nullptr); + + template + bool process_selector_variant( Node& node, const T& selector, Node* parent = nullptr); + bool process_simple_selector(Node& node, const SimpleSelector& selector); + bool process_complex_selector(Node& node, ComplexSelector selector, Node* parent = nullptr); + bool check_pseudo_classes( Node& node, const SimpleSelector& selector ); + bool check_nth_child(const Element& element, const std::string& formula); + bool check_nth_of_type(const Element& element, const std::string& formula); + bool check_custom_pseudo_class(const Element& element, const std::string& name); + bool is_default_state(const ElementState& state); + ElementState extract_element_state(const Selector& selector); + void extract_state_from_simple_selector(const SimpleSelector& selector, ElementState& state); + void extract_state_from_complex_selector(const ComplexSelector& selector, ElementState& state); + }; + +} \ No newline at end of file diff --git a/src/graphics/ui/style/Stylesheet.h b/src/graphics/ui/style/Stylesheet.h index 6bc83dd72..fa3c8d85b 100644 --- a/src/graphics/ui/style/Stylesheet.h +++ b/src/graphics/ui/style/Stylesheet.h @@ -101,4 +101,63 @@ namespace style { struct Stylesheet { std::vector rules; }; + + struct ComputedStyle { + std::unordered_map properties; + + ComputedStyle() = default; + + explicit ComputedStyle(const std::vector& declarations) { + for (const auto& decl : declarations) { + properties[decl.name] = decl.value; + } + } + + const style::value* get(const std::string& property_name) const { + auto it = properties.find(property_name); + if (it != properties.end()) { + return &it->second; + } + return nullptr; + } + + style::value get(const std::string& property_name, style::value default_value) const { + auto it = properties.find(property_name); + if (it != properties.end()) { + return it->second; + } + return default_value; + } + + void set(const std::string& property_name, style::value value) { + properties[std::move(property_name)] = std::move(value); + } + + bool has(const std::string& property_name) const { + return properties.find(property_name) != properties.end(); + } + + void remove(const std::string& property_name) { + properties.erase(property_name); + } + + size_t size() const { + return properties.size(); + } + + bool empty() const { + return properties.empty(); + } + + void clear() { + properties.clear(); + } + + auto begin() { return properties.begin(); } + auto end() { return properties.end(); } + auto begin() const { return properties.begin(); } + auto end() const { return properties.end(); } + auto cbegin() const { return properties.cbegin(); } + auto cend() const { return properties.cend(); } + }; } \ No newline at end of file diff --git a/src/graphics/ui/style/StylesheetParser.cpp b/src/graphics/ui/style/StylesheetParser.cpp index 988ba034e..6f2d19e77 100644 --- a/src/graphics/ui/style/StylesheetParser.cpp +++ b/src/graphics/ui/style/StylesheetParser.cpp @@ -1,13 +1,16 @@ #include "StylesheetParser.hpp" -#include -#include + #include +#include +#include #include "util/stringutil.hpp" using namespace style; -StylesheetParser::StylesheetParser(std::string_view file, std::string_view source) +StylesheetParser::StylesheetParser( + std::string_view file, std::string_view source +) : BasicParser(file, source) { clikeComment = true; } @@ -16,7 +19,7 @@ Stylesheet StylesheetParser::parse() { Stylesheet stylesheet; while (hasNext()) { - skipWhitespace(); // Пропускаем ведущие пробелы, комментарии + skipWhitespace(); // Пропускаем ведущие пробелы, комментарии // Проверяем, не достигли ли конца if (!hasNext()) { @@ -29,7 +32,8 @@ Stylesheet StylesheetParser::parse() { if (rule_opt.has_value()) { stylesheet.rules.push_back(std::move(rule_opt.value())); } - // Если правило не распарсилось (nullopt), parseRule уже обработал ошибку + // Если правило не распарсилось (nullopt), parseRule уже обработал + // ошибку } catch (const parsing_error& e) { std::cerr << "Error parsing rule: " << e.what() << "\n"; // Пытаемся восстановиться: пропустить до следующей возможной точки @@ -38,7 +42,7 @@ Stylesheet StylesheetParser::parse() { nextChar(); } if (hasNext() && peekNoJump() == '}') { - nextChar(); // Пропускаем '}' + nextChar(); // Пропускаем '}' } // Продолжаем парсинг } @@ -58,7 +62,7 @@ std::optional StylesheetParser::parseRule() { if (selectors.empty()) { // Если не удалось распарсить ни одного селектора if (!hasNext()) return std::nullopt; - + // Попробуем пропустить непонятный символ и продолжить std::cerr << "Warning: No selectors found, skipping character.\n"; if (hasNext()) nextChar(); @@ -72,11 +76,11 @@ std::optional StylesheetParser::parseRule() { } catch (const parsing_error&) { std::cerr << "Error: Expected '{' after selectors.\n"; // Пытаемся восстановиться - while(hasNext() && peekNoJump() != '{' && peekNoJump() != '}') { + while (hasNext() && peekNoJump() != '{' && peekNoJump() != '}') { nextChar(); } if (hasNext() && peekNoJump() == '{') { - nextChar(); + //nextChar(); } else { return std::nullopt; } @@ -86,21 +90,8 @@ std::optional StylesheetParser::parseRule() { auto declarations = parseDeclarations(); skipWhitespace(); - // 4. Ожидаем закрывающую фигурную скобку - try { - expect('}'); - } catch (const parsing_error&) { - std::cerr << "Error: Expected '}' after declarations.\n"; - // Пытаемся восстановиться - while(hasNext() && peekNoJump() != '}') { - nextChar(); - } - if (hasNext() && peekNoJump() == '}') { - nextChar(); // Пропускаем '}' - } - } - return Rule{std::move(selectors), std::move(declarations)}; + return Rule {std::move(selectors), std::move(declarations)}; } // Парсит список селекторов, разделенных запятыми @@ -112,7 +103,7 @@ std::vector StylesheetParser::parseSelectorList() { // Пропускаем запятую, если это не первый селектор skipWhitespace(); if (hasNext() && peekNoJump() == ',') { - nextChar(); // Пропускаем ',' + nextChar(); // Пропускаем ',' } else { // Если нет запятой, значит, это конец списка селекторов break; @@ -121,7 +112,7 @@ std::vector StylesheetParser::parseSelectorList() { first = false; skipWhitespace(); - + // Определяем конец текущего селектора: ',' или '{' size_t start_pos = pos; bool found_delimiter = false; @@ -129,61 +120,81 @@ std::vector StylesheetParser::parseSelectorList() { nextChar(); } // pos теперь указывает на ',' или '{' или на конец - + // Извлекаем текст селектора std::string selector_text(source.substr(start_pos, pos - start_pos)); - + // Триммируем selector_text size_t first_char = selector_text.find_first_not_of(" \t\n\r\f\v"); size_t last_char = selector_text.find_last_not_of(" \t\n\r\f\v"); if (first_char != std::string::npos) { - selector_text = selector_text.substr(first_char, (last_char - first_char + 1)); + selector_text = + selector_text.substr(first_char, (last_char - first_char + 1)); } else { - selector_text.clear(); // Только пробелы + selector_text.clear(); // Только пробелы } if (!selector_text.empty()) { try { - // Предполагается, что функция parseCSSSelector существует и возвращает style::Selector + // Предполагается, что функция parseCSSSelector существует и + // возвращает style::Selector selectors.push_back(parseCSSSelector(selector_text)); } catch (const std::exception& e) { - std::cerr << "Error parsing selector '" << selector_text << "': " << e.what() << "\n"; + std::cerr << "Error parsing selector '" << selector_text + << "': " << e.what() << "\n"; } } - - } while (hasNext() && peekNoJump() == ','); // Продолжаем, если следующий символ - запятая + + } while ( + hasNext() && peekNoJump() == ',' + ); // Продолжаем, если следующий символ - запятая return selectors; } - std::vector StylesheetParser::parseDeclarations() { - std::vector declarations; - - while (hasNext()) { - skipWhitespace(); - // Если встречаем '}', значит блок закончился - if (peekNoJump() == '}') { - break; + // На этом моменте позиция парсера должна быть сразу после '{' + // Нам нужно найти закрывающую '}' и извлечь содержимое между ними. + + size_t block_start_pos = this->pos; // Позиция начала блока деклараций + size_t brace_count = 1; // Мы уже прошли первую '{' + + // Ищем парную закрывающую скобку + while (hasNext() && brace_count > 0) { + char c = + this->source[this->pos]; // Доступ к source напрямую, предполагая, + // что он protected или friend + if (c == '{') { + brace_count++; + } else if (c == '}') { + brace_count--; } - - // Пытаемся распарсить одну декларацию - auto decl_opt = parseDeclaration(); - if (decl_opt.has_value()) { - declarations.push_back(std::move(decl_opt.value())); - } else { - // Если не удалось распарсить декларацию, пропускаем до ';' - // или до конца блока, чтобы избежать зависания - std::cerr << "Warning: Could not parse declaration near line " << (line + 1) << "\n"; - while(hasNext() && peekNoJump() != ';' && peekNoJump() != '}') { - nextChar(); - } - if (hasNext() && peekNoJump() == ';') { - nextChar(); // Пропускаем ';' - } + if (brace_count > 0) { // Не продвигаем позицию на последней '}' + this->pos++; } } + if (brace_count != 0) { + throw error("Unmatched '{' in rule block"); + } + + size_t block_end_pos = this->pos; // Позиция символа '}' + + // Извлекаем подстроку с декларациями + std::string_view declarations_content = + this->source.substr(block_start_pos, block_end_pos - block_start_pos); + + // Создаем отдельный парсер для этой подстроки + // Предполагаем, что у DeclarationParser есть конструктор (std::string_view, + // std::string_view) + DeclarationParser decl_parser(this->filename, declarations_content); + + // Парсим декларации + auto declarations = decl_parser.parseDeclarations(); + + // Продвигаем позицию основного парсера за закрывающую скобку + this->pos = block_end_pos + 1; + return declarations; } @@ -191,14 +202,14 @@ std::optional StylesheetParser::parseDeclaration() { try { skipWhitespace(); if (!hasNext() || peekNoJump() == '}') { - return std::nullopt; // Нечего парсить + return std::nullopt; // Нечего парсить } // 1. Парсим имя свойства (property name) std::string property_name = parseCSSIdentifier(); if (property_name.empty()) { // Пропускаем странный символ - if(hasNext()) nextChar(); + if (hasNext()) nextChar(); return std::nullopt; } @@ -210,34 +221,40 @@ std::optional StylesheetParser::parseDeclaration() { // TODO: Здесь нужно будет реализовать полноценный парсер значений CSS skipWhitespace(); size_t start = pos; - // Читаем до ';' или '}' + // Читаем до ';' или '}' while (hasNext() && peekNoJump() != ';' && peekNoJump() != '}') { nextChar(); } - std::string property_value_str = std::string(source.substr(start, pos - start)); + std::string property_value_str = + std::string(source.substr(start, pos - start)); // Триммим значение size_t first = property_value_str.find_first_not_of(" \t\n\r\f\v"); size_t last = property_value_str.find_last_not_of(" \t\n\r\f\v"); if (first != std::string::npos) { - property_value_str = property_value_str.substr(first, (last - first + 1)); + property_value_str = + property_value_str.substr(first, (last - first + 1)); } else { - property_value_str.clear(); // Только пробелы + property_value_str.clear(); // Только пробелы } // 4. Ожидаем точку с запятой skipWhitespace(); if (hasNext() && peekNoJump() == ';') { - nextChar(); // Пропускаем ';' + nextChar(); // Пропускаем ';' } else if (hasNext() && peekNoJump() != '}') { - // Если нет ';' и это не конец блока, это ошибка - throw error("';' expected"); + // Если нет ';' и это не конец блока, это ошибка + throw error("';' expected"); } - // TODO: Здесь нужно будет преобразовать property_value_str в style::value - // Пока что просто создаем строковое значение - style::value property_value = style::value(property_value_str); // Предполагаем конструктор из строки + // TODO: Здесь нужно будет преобразовать property_value_str в + // style::value Пока что просто создаем строковое значение + style::value property_value = style::value( + property_value_str + ); // Предполагаем конструктор из строки - return Declaration{std::move(property_name), std::move(property_value)}; + return Declaration { + std::move(property_name), std::move(property_value) + }; } catch (const parsing_error& e) { std::cerr << "Declaration parsing error: " << e.what() << "\n"; @@ -259,12 +276,16 @@ std::string StylesheetParser::parseCSSIdentifier() { // Проверяет, является ли символ допустимым для начала CSS идентификатора bool StylesheetParser::is_css_identifier_start(char c) const { - return is_identifier_start(c) || c == '-'; // CSS разрешает '-' в начале (после которого должна быть буква) - // Более точная проверка для '-' в начале потребовала бы просмотра следующего символа - // Для простоты разрешим '-' везде, где разрешены идентификаторы + return is_identifier_start(c) || + c == '-'; // CSS разрешает '-' в начале (после которого должна быть + // буква) + // Более точная проверка для '-' в начале потребовала бы просмотра + // следующего символа Для простоты разрешим '-' везде, где разрешены + // идентификаторы } // Проверяет, является ли символ допустимым для продолжения CSS идентификатора bool StylesheetParser::is_css_identifier_part(char c) const { - return is_identifier_part(c) || c == '-'; // CSS разрешает '-' в середине/конце + return is_identifier_part(c) || + c == '-'; // CSS разрешает '-' в середине/конце } \ No newline at end of file diff --git a/src/graphics/ui/style/StylesheetParser.hpp b/src/graphics/ui/style/StylesheetParser.hpp index 3bd47071e..4c3741f57 100644 --- a/src/graphics/ui/style/StylesheetParser.hpp +++ b/src/graphics/ui/style/StylesheetParser.hpp @@ -8,6 +8,7 @@ #include #include "SelectorParser.hpp" +#include "DeclarationParser.hpp" #include "coders/BasicParser.hpp" #include "stylesheet.h" From 2ad1b95df48cfab3e3e6c1cdc147c85bf1e807d3 Mon Sep 17 00:00:00 2001 From: GHOST11111100 Date: Thu, 7 Aug 2025 01:05:20 +0300 Subject: [PATCH 08/11] feat: Implement layout system with Node and LayoutBox classes --- src/graphics/ui/GUI.cpp | 67 +--------- src/graphics/ui/GUI.hpp | 3 + src/graphics/ui/elements/Node.cpp | 187 +++++++++++++++++---------- src/graphics/ui/elements/Node.hpp | 110 +++++++++++++++- src/graphics/ui/layout/Layout.cpp | 181 ++++++++++++++++++++++++++ src/graphics/ui/layout/Layout.hpp | 46 +++++++ src/graphics/ui/layout/LayoutBox.hpp | 58 +++++++++ 7 files changed, 525 insertions(+), 127 deletions(-) create mode 100644 src/graphics/ui/layout/Layout.cpp create mode 100644 src/graphics/ui/layout/Layout.hpp create mode 100644 src/graphics/ui/layout/LayoutBox.hpp diff --git a/src/graphics/ui/GUI.cpp b/src/graphics/ui/GUI.cpp index 45e0ba44e..4f39a7090 100644 --- a/src/graphics/ui/GUI.cpp +++ b/src/graphics/ui/GUI.cpp @@ -60,26 +60,26 @@ GUI::GUI(Engine& engine) // Testing new UI Implementation std::string xmlString = R"( -
+
test
)"; std::string css_code = R"( // comment variant 1 /* comment variant 2 */ - #id:hover { - color: red; + div { + padding-left: 100; } )"; - Node root = Node::from_xml_string("example.xml", xmlString); + document_root = Node::from_xml_string("example.xml", xmlString); StylesheetParser parser("test.css", css_code); style::Stylesheet stylesheet = parser.parse(); style::StyleComputer computer; computer.set_stylesheets({stylesheet}); - computer.compute(root); + computer.compute(document_root); } GUI::~GUI() = default; @@ -259,64 +259,9 @@ void GUI::draw(const DrawContext& pctx, const Assets& assets) { auto ctx = pctx.sub(batch2D.get()); auto& viewport = ctx.getViewport(); - - auto& page = menu->getCurrent(); - if (page.panel) { - menu->setSize(page.panel->getSize()); - page.panel->refresh(); - if (auto panel = std::dynamic_pointer_cast(page.panel)) { - panel->cropToContent(); - } - } - menu->setPos((glm::vec2(viewport) - menu->getSize()) / 2.0f); - uicamera->setFov(viewport.y); - uicamera->setAspectRatio(viewport.x / static_cast(viewport.y)); - - auto uishader = assets.get("ui"); - uishader->use(); - uishader->uniformMatrix("u_projview", uicamera->getProjView()); - batch2D->begin(); - container->draw(ctx, assets); - if (hover) { - engine.getWindow().setCursor(hover->getCursor()); - } - if (hover && debug) { - auto pos = hover->calcPos(); - const auto& id = hover->getId(); - if (!id.empty()) { - auto& font = assets.require(FONT_DEFAULT); - auto text = util::str2wstr_utf8(id); - int width = font.calcWidth(text); - int height = font.getLineHeight(); - - batch2D->untexture(); - batch2D->setColor(0, 0, 0); - batch2D->rect(pos.x, pos.y, width, height); - - batch2D->resetColor(); - font.draw(*batch2D, text, pos.x, pos.y, nullptr, 0); - } - - batch2D->untexture(); - auto node = hover->getParent(); - while (node) { - auto parentPos = node->calcPos(); - auto size = node->getSize(); - - batch2D->setColor(0, 255, 255); - batch2D->lineRect( - parentPos.x + 1, parentPos.y, size.x - 2, size.y - 1 - ); - - node = node->getParent(); - } - // debug draw - auto size = hover->getSize(); - batch2D->setColor(0, 255, 0); - batch2D->lineRect(pos.x, pos.y, size.x - 1, size.y - 1); - } + document_root.draw(ctx, assets); } std::shared_ptr GUI::getFocused() const { diff --git a/src/graphics/ui/GUI.hpp b/src/graphics/ui/GUI.hpp index 92e58db54..eee05a5f8 100644 --- a/src/graphics/ui/GUI.hpp +++ b/src/graphics/ui/GUI.hpp @@ -8,6 +8,7 @@ #include #include #include +#include "elements/Node.hpp" class DrawContext; class Assets; @@ -84,6 +85,8 @@ namespace gui { bool doubleClicked = false; bool debug = false; + Node document_root = Node("div"); + void actMouse(float delta, const CursorState& cursor); void actFocused(); void updateTooltip(float delta); diff --git a/src/graphics/ui/elements/Node.cpp b/src/graphics/ui/elements/Node.cpp index e2f39b2a4..af1eb7fc3 100644 --- a/src/graphics/ui/elements/Node.cpp +++ b/src/graphics/ui/elements/Node.cpp @@ -1,30 +1,33 @@ #pragma once -#include - #include "Node.hpp" +#include +#include -Node Node::from_xml_node(const xml::Node& xml_node) { +#include "constants.hpp" +#include "util/stringutil.hpp" +#include "../layout/Layout.hpp" +Node Node::from_xml_node(const xml::Node& xml_node) { if (xml_node.isText()) { return Node(xml_node.getInnerText()); } - + AttributesMap attrs; const auto& xml_attrs = xml_node.getAttributes(); for (const auto& [name, attr] : xml_attrs) { attrs[name] = attr.getText(); } - + Node dom_node(xml_node.getTag(), std::move(attrs)); dom_node.root = true; - + for (size_t i = 0; i < xml_node.size(); ++i) { const xml::Node& child = xml_node.sub(i); dom_node.append_child(from_xml_node(child)); } - + return dom_node; } @@ -32,7 +35,7 @@ Node Node::from_xml_document(const xml::Document& xml_doc) { if (!xml_doc.getRoot()) { throw std::runtime_error("XML document has no root element"); } - + return from_xml_node(*xml_doc.getRoot()); } @@ -41,23 +44,22 @@ Node Node::from_xml_string(std::string_view filename, std::string_view source) { return from_xml_document(*xml_doc); } - // Working with childs // void Node::append_child(Node node) { - children.push_back( std::move( node ) ); + children.push_back(std::move(node)); } void Node::prepend_child(Node node) { - children.insert( children.begin(), std::move( node ) ); + children.insert(children.begin(), std::move(node)); } void Node::append_childs(std::vector nodes) { - children.reserve( children.size() + nodes.size() ); + children.reserve(children.size() + nodes.size()); - for ( Node node: nodes ) { - children.push_back( std::move( node ) ); + for (Node node : nodes) { + children.push_back(std::move(node)); } } @@ -80,27 +82,33 @@ void Node::clear_children() { } const std::string& Node::get_tag() const { - return std::visit([](const auto& node) -> const std::string& { - using T = std::decay_t; - if constexpr (std::is_same_v) { - static const std::string text_tag = "#text"; - return text_tag; - } else { - return node.data.tag_name; - } - }, node_type); + return std::visit( + [](const auto& node) -> const std::string& { + using T = std::decay_t; + if constexpr (std::is_same_v) { + static const std::string text_tag = "#text"; + return text_tag; + } else { + return node.data.tag_name; + } + }, + node_type + ); } const std::string& Node::get_text() const { - return std::visit([](const auto& node) -> const std::string& { - using T = std::decay_t; - if constexpr (std::is_same_v) { - return node.content; - } else { - static const std::string empty_string = ""; - return empty_string; - } - }, node_type); + return std::visit( + [](const auto& node) -> const std::string& { + using T = std::decay_t; + if constexpr (std::is_same_v) { + return node.content; + } else { + static const std::string empty_string = ""; + return empty_string; + } + }, + node_type + ); } bool Node::is_text() const { @@ -131,7 +139,8 @@ void Node::remove_attribute(std::string_view name) { bool Node::has_attribute(std::string_view name) const { if (const auto* element = std::get_if(&node_type)) { - return element->data.attributes.find(std::string(name)) != element->data.attributes.end(); + return element->data.attributes.find(std::string(name)) != + element->data.attributes.end(); } return false; } @@ -148,7 +157,7 @@ std::optional Node::get_attribute(std::string_view name) const { const AttributesMap& Node::get_attributes() const { static const AttributesMap empty_attrs = {}; - + if (const auto* element = std::get_if(&node_type)) { return element->data.attributes; } @@ -157,7 +166,7 @@ const AttributesMap& Node::get_attributes() const { std::vector Node::find_by_tag(std::string_view tag) { std::vector result; - + if (is_element() && get_tag() == tag) { result.push_back(this); } @@ -166,22 +175,22 @@ std::vector Node::find_by_tag(std::string_view tag) { auto child_results = child.find_by_tag(tag); result.insert(result.end(), child_results.begin(), child_results.end()); } - + return result; } std::vector Node::find_by_tag(std::string_view tag) const { std::vector result; - + if (is_element() && get_tag() == tag) { result.push_back(this); } - + for (const auto& child : children) { auto child_results = child.find_by_tag(tag); result.insert(result.end(), child_results.begin(), child_results.end()); } - + return result; } @@ -189,13 +198,13 @@ Node* Node::find_first_by_tag(std::string_view tag) { if (is_element() && get_tag() == tag) { return this; } - + for (auto& child : children) { if (auto* found = child.find_first_by_tag(tag)) { return found; } } - + return nullptr; } @@ -203,13 +212,13 @@ const Node* Node::find_first_by_tag(std::string_view tag) const { if (is_element() && get_tag() == tag) { return this; } - + for (const auto& child : children) { if (const auto* found = child.find_first_by_tag(tag)) { return found; } } - + return nullptr; } @@ -217,12 +226,12 @@ Node* Node::find_by_path(std::string_view path) { if (path.empty()) { return nullptr; } - + std::string path_str(path); std::vector parts; size_t start = 0; size_t pos = 0; - + while ((pos = path_str.find('>', start)) != std::string::npos) { std::string part = path_str.substr(start, pos - start); if (!part.empty()) { @@ -230,16 +239,16 @@ Node* Node::find_by_path(std::string_view path) { } start = pos + 1; } - + std::string last_part = path_str.substr(start); if (!last_part.empty()) { parts.push_back(std::move(last_part)); } - + if (parts.empty()) { return nullptr; } - + return find_by_path_impl(parts, 0); } @@ -247,12 +256,12 @@ const Node* Node::find_by_path(std::string_view path) const { if (path.empty()) { return nullptr; } - + std::string path_str(path); std::vector parts; size_t start = 0; size_t pos = 0; - + while ((pos = path_str.find('>', start)) != std::string::npos) { std::string part = path_str.substr(start, pos - start); if (!part.empty()) { @@ -260,60 +269,64 @@ const Node* Node::find_by_path(std::string_view path) const { } start = pos + 1; } - + std::string last_part = path_str.substr(start); if (!last_part.empty()) { parts.push_back(std::move(last_part)); } - + if (parts.empty()) { return nullptr; } - + return find_by_path_impl(parts, 0); } -Node* Node::find_by_path_impl(const std::vector& parts, size_t index) { +Node* Node::find_by_path_impl( + const std::vector& parts, size_t index +) { if (index >= parts.size()) { return nullptr; } - + const std::string& expected_tag = parts[index]; - + if (is_element() && get_tag() == expected_tag) { if (index == parts.size() - 1) { return this; } - + for (auto& child : children) { if (auto* found = child.find_by_path_impl(parts, index + 1)) { return found; } } } - + return nullptr; } -const Node* Node::find_by_path_impl(const std::vector& parts, size_t index) const { +const Node* Node::find_by_path_impl( + const std::vector& parts, size_t index +) const { if (index >= parts.size()) { return nullptr; } - + const std::string& expected_tag = parts[index]; - + if (is_element() && get_tag() == expected_tag) { if (index == parts.size() - 1) { return this; } - + for (const auto& child : children) { if (const auto* found = child.find_by_path_impl(parts, index + 1)) { return found; } } } - + return nullptr; } @@ -402,9 +415,10 @@ void ElementData::removeClass(std::string_view className) { // Проверка наличия класса bool ElementData::hasClass(std::string_view className) const { if (className.empty()) return false; - + auto classes = getClassList(); - return std::find(classes.begin(), classes.end(), className) != classes.end(); + return std::find(classes.begin(), classes.end(), className) != + classes.end(); } // Переключение класса @@ -416,4 +430,47 @@ void ElementData::toggleClass(std::string_view className) { } else { addClass(className); } +} + +void Node::draw(const DrawContext& ctx, const Assets& assets) { + Layout::LayoutContext context; + context.available_space = ctx.getViewport(); + context.parent_position = {0, 0}; + context.parent_layout = nullptr; + + Layout::compute_layout(*this, context); + + std::visit( + [this, &ctx, &assets, &context](const auto& node_type) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + // Рендеринг текста + draw_text(ctx, assets, node_type); + } else if constexpr (std::is_same_v) { + // Рендеринг элемента + draw_element(ctx, assets, node_type); + } + }, + node_type + ); + + // Рекурсивно отрисовываем детей + for (auto& child : children) { + child.draw(ctx, assets); + } +} + +void Node::draw_text(const DrawContext& ctx, const Assets& assets, Text text) { + auto batch = ctx.getBatch2D(); + auto font = assets.get(FONT_DEFAULT); + + const LayoutBox& layout = get_layout(); + + font->draw(*batch, util::str2wstr_utf8(get_text()), layout.position.x, layout.position.y, text.styles.get(), 0); +} + +void Node::draw_element( + const DrawContext& ctx, const Assets& assets, Element element +) { + auto batch = ctx.getBatch2D(); } \ No newline at end of file diff --git a/src/graphics/ui/elements/Node.hpp b/src/graphics/ui/elements/Node.hpp index e20bc526e..4aad11870 100644 --- a/src/graphics/ui/elements/Node.hpp +++ b/src/graphics/ui/elements/Node.hpp @@ -7,8 +7,13 @@ #include #include +#include "assets/Assets.hpp" #include "coders/xml.hpp" +#include "graphics/core/Batch2D.hpp" +#include "graphics/core/DrawContext.hpp" +#include "graphics/core/Font.hpp" #include "graphics/ui/style/Stylesheet.h" +#include "graphics/ui/layout/LayoutBox.hpp" using AttributesMap = std::unordered_map; @@ -16,7 +21,9 @@ struct ElementData { std::string tag_name; AttributesMap attributes; - ElementData(std::string_view tag, AttributesMap attrs) : tag_name(tag), attributes(std::move(attrs)) {} + ElementData(std::string_view tag, AttributesMap attrs) + : tag_name(tag), attributes(std::move(attrs)) { + } std::optional getId() const; void setId(std::string_view id); @@ -31,9 +38,27 @@ struct ElementData { struct Text { std::string content; + std::unique_ptr styles; Text(std::string_view text_content) : content(text_content) { } + Text(const Text& other) : content(other.content) { + } + Text& operator=(const Text& other) { + if (this != &other) { + content = other.content; + } + return *this; + } + + Text(Text&& other) noexcept : content(std::move(other.content)) { + } + Text& operator=(Text&& other) noexcept { + if (this != &other) { + content = std::move(other.content); + } + return *this; + } }; struct ElementState { @@ -85,6 +110,39 @@ struct Element { Element(std::string_view tag, AttributesMap attrs) : data(tag, std::move(attrs)) { } + Element(const Element& other) + : data(other.data), + state(other.state), + style(other.style), + state_styles(other.state_styles) { + } + + Element& operator=(const Element& other) { + if (this != &other) { + data = other.data; + state = other.state; + style = other.style; + state_styles = other.state_styles; + } + return *this; + } + + Element(Element&& other) noexcept + : data(std::move(other.data)), + state(std::move(other.state)), + style(std::move(other.style)), + state_styles(std::move(other.state_styles)) { + } + + Element& operator=(Element&& other) noexcept { + if (this != &other) { + data = std::move(other.data); + state = std::move(other.state); + style = std::move(other.style); + state_styles = std::move(other.state_styles); + } + return *this; + } }; using NodeType = std::variant; @@ -104,6 +162,51 @@ struct Node { node_type(Element(tag, std::move(attrs))) { } + Node(const Node& other) + : root(other.root), + children(other.children), + node_type(other.node_type) { + } + + Node& operator=(const Node& other) { + if (this != &other) { + root = other.root; + children = other.children; + node_type = other.node_type; + } + return *this; + } + + Node(Node&& other) noexcept + : root(other.root), + children(std::move(other.children)), + node_type(std::move(other.node_type)) { + } + + Node& operator=(Node&& other) noexcept { + if (this != &other) { + root = other.root; + children = std::move(other.children); + node_type = std::move(other.node_type); + } + return *this; + } + + // Layout + LayoutBox layout_box; // Layout данные + bool layout_dirty = true; + + // Методы для работы с layout + LayoutBox& get_layout() { return layout_box; } + const LayoutBox& get_layout() const { return layout_box; } + + void mark_layout_dirty() { layout_dirty = true; } + void mark_layout_clean() { layout_dirty = false; } + bool is_layout_dirty() const { return layout_dirty; } + + // Draw + void draw(const DrawContext& ctx, const Assets& assets); + // Working with childs void append_child(Node node); void append_childs(std::vector nodes); @@ -150,4 +253,9 @@ struct Node { const Node* find_by_path_impl( const std::vector& parts, size_t index ) const; + + void draw_text(const DrawContext& ctx, const Assets& assets, Text text); + void draw_element( + const DrawContext& ctx, const Assets& assets, Element element + ); }; diff --git a/src/graphics/ui/layout/Layout.cpp b/src/graphics/ui/layout/Layout.cpp new file mode 100644 index 000000000..f9c64aac6 --- /dev/null +++ b/src/graphics/ui/layout/Layout.cpp @@ -0,0 +1,181 @@ +#include "Layout.hpp" + +void Layout::apply_styles_to_layout(Node& node) { + LayoutBox& layout = node.get_layout(); + + if (auto* element = std::get_if(&node.node_type)) { + const auto& style = element->style; + + // Размеры + layout.content_size.x = + get_dimension_from_style(style, "width", layout.content_size.x); + layout.content_size.y = + get_dimension_from_style(style, "height", layout.content_size.y); + + // Минимальные размеры + layout.min_size.x = get_dimension_from_style(style, "min-width", 0); + layout.min_size.y = get_dimension_from_style(style, "min-height", 0); + + // Максимальные размеры + layout.max_size.x = + get_dimension_from_style(style, "max-width", FLT_MAX); + layout.max_size.y = + get_dimension_from_style(style, "max-height", FLT_MAX); + + // Padding + layout.padding.x = get_dimension_from_style(style, "padding-left", 0) + + get_dimension_from_style(style, "padding-right", 0); + layout.padding.y = get_dimension_from_style(style, "padding-top", 0) + + get_dimension_from_style(style, "padding-bottom", 0); + + // Margin + layout.margin.x = get_dimension_from_style(style, "margin-left", 0) + + get_dimension_from_style(style, "margin-right", 0); + layout.margin.y = get_dimension_from_style(style, "margin-top", 0) + + get_dimension_from_style(style, "margin-bottom", 0); + + // Border + layout.border.x = + get_dimension_from_style(style, "border-left-width", 0) + + get_dimension_from_style(style, "border-right-width", 0); + layout.border.y = + get_dimension_from_style(style, "border-top-width", 0) + + get_dimension_from_style(style, "border-bottom-width", 0); + } +} + +float Layout::get_dimension_from_style( + const style::ComputedStyle& style, + const std::string& property, + float default_value +) { + if (auto value = style.get(property)) { + return value->asFloat(0.0); + } + return default_value; +} + +void Layout::compute_layout(Node& node, const LayoutContext& context) { + // Если layout не нуждается в обновлении, пропускаем + if (!node.is_layout_dirty()) { + return; + } + + // Вычисляем layout для текущего узла + std::visit( + [&node, &context](auto& node_type) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + compute_element_layout(node, node_type, context); + } else if constexpr (std::is_same_v) { + compute_text_layout(node, node_type, context); + } + }, + node.node_type + ); + + // Помечаем узел как "чистый" + node.mark_layout_clean(); + + // Рекурсивно вычисляем layout для детей + LayoutContext child_context = context; + child_context.parent_layout = &node.get_layout(); + child_context.parent_position = node.get_layout().position; + + for (auto& child : node.children) { + compute_layout(child, child_context); + } +} + +void Layout::compute_element_layout( + Node& node, Element& element, const LayoutContext& context +) { + apply_styles_to_layout(node); + compute_block_layout(node, element, context); +} + +void Layout::compute_text_layout( + Node& node, Text& text, const LayoutContext& context +) { + LayoutBox& layout = node.get_layout(); + + // Для текста вычисляем размер на основе содержимого + // Пока используем приблизительные значения + float text_width = text.content.length() * 8.0f; // ~8px на символ + float text_height = 16.0f; // высота строки + + layout.content_size = {text_width, text_height}; + layout.position = context.parent_position; + + // Если есть родитель, позиционируем относительно него + if (context.parent_layout) { + // Простое позиционирование - можно улучшить + layout.position.x = context.parent_position.x; + layout.position.y = context.parent_position.y; + } +} + +void Layout::compute_block_layout( + Node& node, Element& element, const LayoutContext& context +) { + LayoutBox& layout = node.get_layout(); + + // Устанавливаем позицию + layout.position = context.parent_position; + + // Если есть родитель, учитываем его padding + if (context.parent_layout) { + layout.position.x += context.parent_layout->padding.x; + layout.position.y += context.parent_layout->padding.y; + } + + // Вычисляем размеры детей рекурсивно + float max_child_width = 0.0f; + float total_child_height = 0.0f; + + // Сначала вычисляем layout для всех детей + LayoutContext child_context = context; + child_context.parent_position = { + layout.position.x, layout.position.y + total_child_height + }; + + for (auto& child : node.children) { + compute_layout(child, child_context); + const LayoutBox& child_layout = child.get_layout(); + + max_child_width = + glm::max(max_child_width, child_layout.get_outer_size().x); + total_child_height += child_layout.get_outer_size().y; + } + + // Устанавливаем размеры элемента + float specified_width = layout.content_size.x; + float specified_height = layout.content_size.y; + + // Если ширина не задана, используем максимальную ширину детей или доступное + // пространство + if (specified_width <= 0) { + if (max_child_width > 0) { + layout.content_size.x = max_child_width; + } else { + layout.content_size.x = context.available_space.x > 0 + ? context.available_space.x + : 100.0f; // дефолт + } + } + + // Если высота не задана, используем высоту детей + if (specified_height <= 0) { + layout.content_size.y = + total_child_height > 0 ? total_child_height : 20.0f; // дефолт + } + + // Позиционируем детей вертикально (block layout) + float current_y = layout.position.y + layout.padding.y; + for (auto& child : node.children) { + LayoutBox& child_layout = child.get_layout(); + child_layout.position.x = layout.position.x + layout.padding.x; + child_layout.position.y = current_y; + current_y += child_layout.get_outer_size().y; + } +} \ No newline at end of file diff --git a/src/graphics/ui/layout/Layout.hpp b/src/graphics/ui/layout/Layout.hpp new file mode 100644 index 000000000..ec6f32586 --- /dev/null +++ b/src/graphics/ui/layout/Layout.hpp @@ -0,0 +1,46 @@ +#pragma once +#include "LayoutBox.hpp" +#include "../elements/Node.hpp" + +class Layout { +public: + struct LayoutContext { + glm::vec2 available_space; + glm::vec2 parent_position; + LayoutBox* parent_layout = nullptr; + }; + + // Основной метод вычисления layout + static void compute_layout(Node& node, const LayoutContext& context); + + // Методы для разных типов layout + static void compute_text_layout(Node& node, Text& text, const LayoutContext& context); + static void compute_element_layout(Node& node, Element& element, const LayoutContext& context); + static void compute_block_layout(Node& node, Element& element, const LayoutContext& context); + + // Утилиты + static LayoutBox& get_layout_box(Node& node); + static void apply_styles_to_layout(Node& node); + static glm::vec2 calculate_preferred_size( + Node& node, const LayoutContext& context + ); +private: + // Вспомогательные методы + static void calculate_margins(Node& node, const LayoutContext& context); + static void calculate_padding(Node& node); + static void calculate_borders(Node& node); + static void position_children(Node& node); + + // Работа со стилями + static float get_dimension_from_style( + const style::ComputedStyle& style, + const std::string& property, + float default_value + ); + static glm::vec2 get_dimensions_from_style( + const style::ComputedStyle& style, + const std::string& width_prop, + const std::string& height_prop, + const glm::vec2& default_size + ); +}; \ No newline at end of file diff --git a/src/graphics/ui/layout/LayoutBox.hpp b/src/graphics/ui/layout/LayoutBox.hpp new file mode 100644 index 000000000..4d831b4f9 --- /dev/null +++ b/src/graphics/ui/layout/LayoutBox.hpp @@ -0,0 +1,58 @@ +#pragma once +#include +#include "graphics/ui/style/Stylesheet.h" + +struct LayoutBox { + glm::vec2 content_size = {0, 0}; // Размер контента + glm::vec2 position = {0, 0}; // Абсолютная позиция + glm::vec2 margin = {0, 0}; // Внешние отступы + glm::vec2 padding = {0, 0}; // Внутренние отступы + glm::vec2 border = {0, 0}; // Границы + glm::vec2 full_size = {0, 0}; // Полный размер (content + padding + border) + + // Минимальные и максимальные размеры + glm::vec2 min_size = {0, 0}; + glm::vec2 max_size = {FLT_MAX, FLT_MAX}; + + // Preferred size (для flexbox и т.д.) + std::optional preferred_size; + + // Флаги + bool dirty = true; // Нужно пересчитать layout + bool visible = true; + + // Методы для работы с размерами + glm::vec2 get_content_box() const { + return content_size; + } + + glm::vec2 get_padding_box() const { + return {content_size.x + padding.x * 2, content_size.y + padding.y * 2}; + } + + glm::vec2 get_border_box() const { + return {get_padding_box().x + border.x * 2, get_padding_box().y + border.y * 2}; + } + + glm::vec2 get_margin_box() const { + return {get_border_box().x + margin.x * 2, get_border_box().y + margin.y * 2}; + } + + // Утилиты + glm::vec2 get_inner_size() const { + return content_size; + } + + glm::vec2 get_outer_size() const { + return get_margin_box(); + } + + glm::vec4 get_content_rect() const { + return {position.x, position.y, content_size.x, content_size.y}; + } + + glm::vec4 get_padding_rect() const { + return {position.x - padding.x, position.y - padding.y, + get_padding_box().x, get_padding_box().y}; + } +}; \ No newline at end of file From 5902c1b375405825f0cbb4dadcfe852271814a70 Mon Sep 17 00:00:00 2001 From: GHOST11111100 Date: Thu, 28 Aug 2025 20:13:24 +0300 Subject: [PATCH 09/11] Refactor layout computation and style application - Consolidated layout computation logic into a single method, `compute_layout`, improving readability and maintainability. - Removed the `apply_styles_to_layout` method and integrated its functionality directly into the layout computation process. - Enhanced the handling of text layout by calculating dimensions based on font size. - Introduced a new `LayoutContext` structure to manage layout context more effectively. - Updated `LayoutBox` to simplify its structure, focusing on essential properties for layout calculations. - Improved the handling of child layout computations, allowing for better flexibility in layout direction (vertical/horizontal). - Refined style value handling in `value.cpp` and `value.h`, ensuring consistent type management and default values. - Adjusted color handling to default to transparent instead of opaque in `asColor` method. --- src/graphics/ui/GUI.cpp | 17 +- src/graphics/ui/GUI.hpp | 2 +- src/graphics/ui/elements/Node.cpp | 113 ++-- src/graphics/ui/elements/Node.hpp | 81 ++- src/graphics/ui/layout/Layout.cpp | 211 +++----- src/graphics/ui/layout/Layout.hpp | 50 +- src/graphics/ui/layout/LayoutBox.hpp | 64 +-- src/graphics/ui/style/StyleComputer.cpp | 2 +- src/graphics/ui/style/value.cpp | 693 ++++++++++++------------ src/graphics/ui/style/value.h | 2 +- 10 files changed, 605 insertions(+), 630 deletions(-) diff --git a/src/graphics/ui/GUI.cpp b/src/graphics/ui/GUI.cpp index 4f39a7090..db767d02f 100644 --- a/src/graphics/ui/GUI.cpp +++ b/src/graphics/ui/GUI.cpp @@ -61,15 +61,24 @@ GUI::GUI(Engine& engine) std::string xmlString = R"(
-test +

test

+

test2

)"; std::string css_code = R"( // comment variant 1 /* comment variant 2 */ div { - padding-left: 100; + background: #111; + width: 200; + height: 200; + direction: vertical; } + + p { + background: #222; + height: 16; + } )"; document_root = Node::from_xml_string("example.xml", xmlString); @@ -79,7 +88,7 @@ test style::StyleComputer computer; computer.set_stylesheets({stylesheet}); - computer.compute(document_root); + computer.compute(*document_root); } GUI::~GUI() = default; @@ -261,7 +270,7 @@ void GUI::draw(const DrawContext& pctx, const Assets& assets) { auto& viewport = ctx.getViewport(); batch2D->begin(); - document_root.draw(ctx, assets); + document_root->draw(ctx, assets); } std::shared_ptr GUI::getFocused() const { diff --git a/src/graphics/ui/GUI.hpp b/src/graphics/ui/GUI.hpp index eee05a5f8..427854b30 100644 --- a/src/graphics/ui/GUI.hpp +++ b/src/graphics/ui/GUI.hpp @@ -85,7 +85,7 @@ namespace gui { bool doubleClicked = false; bool debug = false; - Node document_root = Node("div"); + std::shared_ptr document_root = std::make_shared("div");; void actMouse(float delta, const CursorState& cursor); void actFocused(); diff --git a/src/graphics/ui/elements/Node.cpp b/src/graphics/ui/elements/Node.cpp index af1eb7fc3..2e7f0bd95 100644 --- a/src/graphics/ui/elements/Node.cpp +++ b/src/graphics/ui/elements/Node.cpp @@ -5,13 +5,13 @@ #include #include +#include "../layout/Layout.hpp" #include "constants.hpp" #include "util/stringutil.hpp" -#include "../layout/Layout.hpp" -Node Node::from_xml_node(const xml::Node& xml_node) { +std::shared_ptr Node::from_xml_node(const xml::Node& xml_node) { if (xml_node.isText()) { - return Node(xml_node.getInnerText()); + return std::make_shared(xml_node.getInnerText()); } AttributesMap attrs; @@ -20,18 +20,20 @@ Node Node::from_xml_node(const xml::Node& xml_node) { attrs[name] = attr.getText(); } - Node dom_node(xml_node.getTag(), std::move(attrs)); - dom_node.root = true; + auto dom_node = std::make_shared(xml_node.getTag(), std::move(attrs)); + dom_node->root = true; for (size_t i = 0; i < xml_node.size(); ++i) { const xml::Node& child = xml_node.sub(i); - dom_node.append_child(from_xml_node(child)); + auto child_node = from_xml_node(child); + child_node->parent = dom_node; // безопасно, shared_from_this не нужен здесь + dom_node->children.push_back(std::move(child_node)); } return dom_node; } -Node Node::from_xml_document(const xml::Document& xml_doc) { +std::shared_ptr Node::from_xml_document(const xml::Document& xml_doc) { if (!xml_doc.getRoot()) { throw std::runtime_error("XML document has no root element"); } @@ -39,7 +41,7 @@ Node Node::from_xml_document(const xml::Document& xml_doc) { return from_xml_node(*xml_doc.getRoot()); } -Node Node::from_xml_string(std::string_view filename, std::string_view source) { +std::shared_ptr Node::from_xml_string(std::string_view filename, std::string_view source) { auto xml_doc = xml::parse(filename, source); return from_xml_document(*xml_doc); } @@ -48,18 +50,27 @@ Node Node::from_xml_string(std::string_view filename, std::string_view source) { // void Node::append_child(Node node) { - children.push_back(std::move(node)); + auto child_ptr = + std::make_shared(std::move(node)); + child_ptr->parent = shared_from_this(); + children.push_back(std::move(child_ptr)); // добавляем в children } void Node::prepend_child(Node node) { - children.insert(children.begin(), std::move(node)); + auto child_ptr = + std::make_unique(std::move(node)); // создаём unique_ptr + child_ptr->parent = shared_from_this(); + children.insert(children.begin(), std::move(child_ptr)); } void Node::append_childs(std::vector nodes) { children.reserve(children.size() + nodes.size()); for (Node node : nodes) { - children.push_back(std::move(node)); + auto child_ptr = + std::make_unique(std::move(node)); // создаём unique_ptr + child_ptr->parent = shared_from_this(); + children.push_back(std::move(child_ptr)); } } @@ -67,7 +78,10 @@ void Node::insert_child(size_t index, Node node) { if (index > children.size()) { throw std::out_of_range("Index out of range"); } - children.insert(children.begin() + index, std::move(node)); + auto child_ptr = + std::make_unique(std::move(node)); // создаём unique_ptr + child_ptr->parent = shared_from_this(); + children.insert(children.begin() + index, std::move(child_ptr)); } void Node::remove_child(size_t index) { @@ -172,7 +186,7 @@ std::vector Node::find_by_tag(std::string_view tag) { } for (auto& child : children) { - auto child_results = child.find_by_tag(tag); + auto child_results = child->find_by_tag(tag); result.insert(result.end(), child_results.begin(), child_results.end()); } @@ -187,7 +201,7 @@ std::vector Node::find_by_tag(std::string_view tag) const { } for (const auto& child : children) { - auto child_results = child.find_by_tag(tag); + auto child_results = child->find_by_tag(tag); result.insert(result.end(), child_results.begin(), child_results.end()); } @@ -200,7 +214,7 @@ Node* Node::find_first_by_tag(std::string_view tag) { } for (auto& child : children) { - if (auto* found = child.find_first_by_tag(tag)) { + if (auto* found = child->find_first_by_tag(tag)) { return found; } } @@ -214,7 +228,7 @@ const Node* Node::find_first_by_tag(std::string_view tag) const { } for (const auto& child : children) { - if (const auto* found = child.find_first_by_tag(tag)) { + if (const auto* found = child->find_first_by_tag(tag)) { return found; } } @@ -297,7 +311,7 @@ Node* Node::find_by_path_impl( } for (auto& child : children) { - if (auto* found = child.find_by_path_impl(parts, index + 1)) { + if (auto* found = child->find_by_path_impl(parts, index + 1)) { return found; } } @@ -321,7 +335,7 @@ const Node* Node::find_by_path_impl( } for (const auto& child : children) { - if (const auto* found = child.find_by_path_impl(parts, index + 1)) { + if (const auto* found = child->find_by_path_impl(parts, index + 1)) { return found; } } @@ -330,7 +344,7 @@ const Node* Node::find_by_path_impl( return nullptr; } -const std::vector& Node::get_children() const { +const std::vector>& Node::get_children() const { return children; } @@ -433,44 +447,79 @@ void ElementData::toggleClass(std::string_view className) { } void Node::draw(const DrawContext& ctx, const Assets& assets) { - Layout::LayoutContext context; + LayoutContext context; context.available_space = ctx.getViewport(); - context.parent_position = {0, 0}; context.parent_layout = nullptr; Layout::compute_layout(*this, context); + draw(ctx, assets, context); +} + +void Node::draw( + const DrawContext& ctx, const Assets& assets, const LayoutContext& context +) { + auto batch = ctx.getBatch2D(); + + // Создаём под-контекст для локального управления состоянием + DrawContext childCtx = ctx.sub(); + + // Определяем viewport/скиссоры, если нужно (для примера оставим полный размер) + // childCtx.setScissors(glm::vec4(layout.x, layout.y, layout.width, layout.height)); + + // Сбрасываем батч перед рисованием текущей ноды + batch->flush(); + batch->untexture(); + std::visit( - [this, &ctx, &assets, &context](const auto& node_type) { + [this, &childCtx, &assets](const auto& node_type) { using T = std::decay_t; if constexpr (std::is_same_v) { - // Рендеринг текста - draw_text(ctx, assets, node_type); + draw_text(childCtx, assets, node_type); } else if constexpr (std::is_same_v) { - // Рендеринг элемента - draw_element(ctx, assets, node_type); + draw_element(childCtx, assets, node_type); } }, node_type ); - // Рекурсивно отрисовываем детей + // Рекурсивно рисуем детей в отдельном под-контексте for (auto& child : children) { - child.draw(ctx, assets); + child->draw(childCtx, assets, context); } + + // Сбрасываем батч после рисования всех детей, чтобы гарантировать корректное состояние OpenGL + batch->flush(); + batch->untexture(); } void Node::draw_text(const DrawContext& ctx, const Assets& assets, Text text) { auto batch = ctx.getBatch2D(); - auto font = assets.get(FONT_DEFAULT); - const LayoutBox& layout = get_layout(); + batch->flush(); + batch->untexture(); + + auto font = assets.get(FONT_DEFAULT); - font->draw(*batch, util::str2wstr_utf8(get_text()), layout.position.x, layout.position.y, text.styles.get(), 0); + batch->setColor(glm::vec4(1.0f, 1.0f, 1.0f, 1.0f)); + font->draw( + *batch, + util::str2wstr_utf8("A"), + layout.x, + layout.y, + text.styles.get(), + 0 + ); } void Node::draw_element( const DrawContext& ctx, const Assets& assets, Element element ) { auto batch = ctx.getBatch2D(); -} \ No newline at end of file + + batch->flush(); + batch->untexture(); + + batch->setColor(element.style.get("background", "#ff0000").asColor()); + batch->rect(layout.x, layout.y, layout.width, layout.height); +} diff --git a/src/graphics/ui/elements/Node.hpp b/src/graphics/ui/elements/Node.hpp index 4aad11870..67bf84862 100644 --- a/src/graphics/ui/elements/Node.hpp +++ b/src/graphics/ui/elements/Node.hpp @@ -12,8 +12,8 @@ #include "graphics/core/Batch2D.hpp" #include "graphics/core/DrawContext.hpp" #include "graphics/core/Font.hpp" -#include "graphics/ui/style/Stylesheet.h" #include "graphics/ui/layout/LayoutBox.hpp" +#include "graphics/ui/style/Stylesheet.h" using AttributesMap = std::unordered_map; @@ -147,9 +147,12 @@ struct Element { using NodeType = std::variant; -struct Node { +struct Node : public std::enable_shared_from_this { + + std::weak_ptr parent; + std::vector> children; + bool root = false; - std::vector children; NodeType node_type; Node(std::string_view text) : node_type(Text(text)) { @@ -157,35 +160,52 @@ struct Node { Node(std::string_view tag, AttributesMap attrs) : node_type(Element(tag, std::move(attrs))) { } - Node(std::string_view tag, AttributesMap attrs, std::vector childs) + Node( + std::string_view tag, + AttributesMap attrs, + std::vector> childs + ) : children(std::move(childs)), node_type(Element(tag, std::move(attrs))) { } - Node(const Node& other) - : root(other.root), - children(other.children), - node_type(other.node_type) { + // Копирующий конструктор (глубокое копирование детей) + Node(const Node& other) : root(other.root), node_type(other.node_type) { + children.reserve(other.children.size()); + for (const auto& child : other.children) { + children.push_back( + std::make_unique(*child) + ); // клонируем каждого ребенка + } } + // Копирующий оператор присваивания Node& operator=(const Node& other) { if (this != &other) { root = other.root; - children = other.children; node_type = other.node_type; + + // Глубокое копирование детей + children.clear(); + children.reserve(other.children.size()); + for (const auto& child : other.children) { + children.push_back(std::make_unique(*child)); + } } return *this; } + // Перемещающий конструктор Node(Node&& other) noexcept - : root(other.root), + : root(std::move(other.root)), children(std::move(other.children)), node_type(std::move(other.node_type)) { } + // Перемещающий оператор присваивания Node& operator=(Node&& other) noexcept { if (this != &other) { - root = other.root; + root = std::move(other.root); children = std::move(other.children); node_type = std::move(other.node_type); } @@ -193,19 +213,34 @@ struct Node { } // Layout - LayoutBox layout_box; // Layout данные + LayoutBox layout; // Layout данные bool layout_dirty = true; - + // Методы для работы с layout - LayoutBox& get_layout() { return layout_box; } - const LayoutBox& get_layout() const { return layout_box; } - - void mark_layout_dirty() { layout_dirty = true; } - void mark_layout_clean() { layout_dirty = false; } - bool is_layout_dirty() const { return layout_dirty; } + LayoutBox& get_layout() { + return layout; + } + const LayoutBox& get_layout() const { + return layout; + } + + void mark_layout_dirty() { + layout_dirty = true; + } + void mark_layout_clean() { + layout_dirty = false; + } + bool is_layout_dirty() const { + return layout_dirty; + } // Draw void draw(const DrawContext& ctx, const Assets& assets); + void draw( + const DrawContext& ctx, + const Assets& assets, + const LayoutContext& context + ); // Working with childs void append_child(Node node); @@ -221,7 +256,7 @@ struct Node { bool is_text() const; bool is_element() const; size_t child_count() const; - const std::vector& get_children() const; + const std::vector>& get_children() const; bool is_root() const; // Working with attributes @@ -240,12 +275,12 @@ struct Node { const Node* find_by_path(std::string_view path) const; // Parse from XML - static Node from_xml_string( + static std::shared_ptr from_xml_string( std::string_view filename, std::string_view source ); - static Node from_xml_document(const xml::Document& xml_doc); + static std::shared_ptr from_xml_document(const xml::Document& xml_doc); private: - static Node from_xml_node(const xml::Node& xml_node); + static std::shared_ptr from_xml_node(const xml::Node& xml_node); Node* find_by_path_impl( const std::vector& parts, size_t index diff --git a/src/graphics/ui/layout/Layout.cpp b/src/graphics/ui/layout/Layout.cpp index f9c64aac6..c13471a43 100644 --- a/src/graphics/ui/layout/Layout.cpp +++ b/src/graphics/ui/layout/Layout.cpp @@ -1,181 +1,92 @@ #include "Layout.hpp" -void Layout::apply_styles_to_layout(Node& node) { - LayoutBox& layout = node.get_layout(); +#include - if (auto* element = std::get_if(&node.node_type)) { - const auto& style = element->style; +// Основной метод вычисления layout +void Layout::compute_layout(Node& node, LayoutContext& context) { + if (!node.is_layout_dirty()) return; - // Размеры - layout.content_size.x = - get_dimension_from_style(style, "width", layout.content_size.x); - layout.content_size.y = - get_dimension_from_style(style, "height", layout.content_size.y); - - // Минимальные размеры - layout.min_size.x = get_dimension_from_style(style, "min-width", 0); - layout.min_size.y = get_dimension_from_style(style, "min-height", 0); - - // Максимальные размеры - layout.max_size.x = - get_dimension_from_style(style, "max-width", FLT_MAX); - layout.max_size.y = - get_dimension_from_style(style, "max-height", FLT_MAX); - - // Padding - layout.padding.x = get_dimension_from_style(style, "padding-left", 0) + - get_dimension_from_style(style, "padding-right", 0); - layout.padding.y = get_dimension_from_style(style, "padding-top", 0) + - get_dimension_from_style(style, "padding-bottom", 0); - - // Margin - layout.margin.x = get_dimension_from_style(style, "margin-left", 0) + - get_dimension_from_style(style, "margin-right", 0); - layout.margin.y = get_dimension_from_style(style, "margin-top", 0) + - get_dimension_from_style(style, "margin-bottom", 0); - - // Border - layout.border.x = - get_dimension_from_style(style, "border-left-width", 0) + - get_dimension_from_style(style, "border-right-width", 0); - layout.border.y = - get_dimension_from_style(style, "border-top-width", 0) + - get_dimension_from_style(style, "border-bottom-width", 0); - } -} - -float Layout::get_dimension_from_style( - const style::ComputedStyle& style, - const std::string& property, - float default_value -) { - if (auto value = style.get(property)) { - return value->asFloat(0.0); - } - return default_value; -} - -void Layout::compute_layout(Node& node, const LayoutContext& context) { - // Если layout не нуждается в обновлении, пропускаем - if (!node.is_layout_dirty()) { - return; - } - - // Вычисляем layout для текущего узла std::visit( - [&node, &context](auto& node_type) { - using T = std::decay_t; - if constexpr (std::is_same_v) { - compute_element_layout(node, node_type, context); - } else if constexpr (std::is_same_v) { - compute_text_layout(node, node_type, context); + [&](auto& inner) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + compute_text_layout(node, inner, context); + } else if constexpr (std::is_same_v) { + compute_element_layout(node, inner, context); } }, node.node_type ); - // Помечаем узел как "чистый" node.mark_layout_clean(); - - // Рекурсивно вычисляем layout для детей - LayoutContext child_context = context; - child_context.parent_layout = &node.get_layout(); - child_context.parent_position = node.get_layout().position; - - for (auto& child : node.children) { - compute_layout(child, child_context); - } -} - -void Layout::compute_element_layout( - Node& node, Element& element, const LayoutContext& context -) { - apply_styles_to_layout(node); - compute_block_layout(node, element, context); } +// --- Текстовый layout --- void Layout::compute_text_layout( - Node& node, Text& text, const LayoutContext& context + Node& node, Text& text, LayoutContext& context ) { - LayoutBox& layout = node.get_layout(); - - // Для текста вычисляем размер на основе содержимого - // Пока используем приблизительные значения - float text_width = text.content.length() * 8.0f; // ~8px на символ - float text_height = 16.0f; // высота строки + // Вычисляем размеры текста + float char_width = node.layout.font_size * 0.6f; + float text_width = char_width * static_cast(text.content.size()); + float text_height = node.layout.font_size; - layout.content_size = {text_width, text_height}; - layout.position = context.parent_position; + node.layout.width = text_width; + node.layout.height = text_height; - // Если есть родитель, позиционируем относительно него - if (context.parent_layout) { - // Простое позиционирование - можно улучшить - layout.position.x = context.parent_position.x; - layout.position.y = context.parent_position.y; + if (auto p = node.parent.lock()) { // проверяем, что родитель существует + node.layout.x = p->layout.x; + node.layout.y = p->layout.y; } } -void Layout::compute_block_layout( - Node& node, Element& element, const LayoutContext& context +// --- Создание контекста для детей --- +LayoutContext Layout::create_child_context( + Node& node, float content_width, float max_height ) { - LayoutBox& layout = node.get_layout(); + LayoutContext ctx; + ctx.parent_layout = &node.layout; + ctx.available_space = { + static_cast(content_width), static_cast(max_height) + }; + return ctx; +} - // Устанавливаем позицию - layout.position = context.parent_position; +// --- Основной layout для элемента --- +void Layout::compute_element_layout( + Node& node, Element& element, LayoutContext& context +) { + // 1) Позиционируем элемент - // Если есть родитель, учитываем его padding - if (context.parent_layout) { - layout.position.x += context.parent_layout->padding.x; - layout.position.y += context.parent_layout->padding.y; - } + float max_width = static_cast(context.available_space.x); + float max_height = static_cast(context.available_space.y); - // Вычисляем размеры детей рекурсивно - float max_child_width = 0.0f; - float total_child_height = 0.0f; + // 2) Вычисляем размеры элемента + node.layout.width = + element.style.get("width", max_width).asFloat(max_width); + node.layout.height = element.style.get("height", max_height) + .asFloat(max_height); // временно - // Сначала вычисляем layout для всех детей - LayoutContext child_context = context; - child_context.parent_position = { - layout.position.x, layout.position.y + total_child_height - }; + bool vertical = + element.style.get("direction", "horizontal").asString("vertical") == + "vertical"; - for (auto& child : node.children) { - compute_layout(child, child_context); - const LayoutBox& child_layout = child.get_layout(); + // 3) Раскладываем детей по flex - max_child_width = - glm::max(max_child_width, child_layout.get_outer_size().x); - total_child_height += child_layout.get_outer_size().y; - } - - // Устанавливаем размеры элемента - float specified_width = layout.content_size.x; - float specified_height = layout.content_size.y; + float main_cursor = 0.f; // основная ось - // Если ширина не задана, используем максимальную ширину детей или доступное - // пространство - if (specified_width <= 0) { - if (max_child_width > 0) { - layout.content_size.x = max_child_width; + LayoutContext child_ctx = + create_child_context(node, node.layout.width, node.layout.height); + for (auto& child : node.children) { + if (vertical) { + child->layout.x = node.layout.x; + child->layout.y = node.layout.y + main_cursor; + compute_layout(*child, child_ctx); + main_cursor += child->layout.height; } else { - layout.content_size.x = context.available_space.x > 0 - ? context.available_space.x - : 100.0f; // дефолт + child->layout.x = node.layout.x + main_cursor; + child->layout.y = node.layout.y; + compute_layout(*child, child_ctx); + main_cursor += child->layout.width; } } - - // Если высота не задана, используем высоту детей - if (specified_height <= 0) { - layout.content_size.y = - total_child_height > 0 ? total_child_height : 20.0f; // дефолт - } - - // Позиционируем детей вертикально (block layout) - float current_y = layout.position.y + layout.padding.y; - for (auto& child : node.children) { - LayoutBox& child_layout = child.get_layout(); - child_layout.position.x = layout.position.x + layout.padding.x; - child_layout.position.y = current_y; - current_y += child_layout.get_outer_size().y; - } -} \ No newline at end of file +} diff --git a/src/graphics/ui/layout/Layout.hpp b/src/graphics/ui/layout/Layout.hpp index ec6f32586..8f9361261 100644 --- a/src/graphics/ui/layout/Layout.hpp +++ b/src/graphics/ui/layout/Layout.hpp @@ -1,46 +1,32 @@ #pragma once -#include "LayoutBox.hpp" #include "../elements/Node.hpp" +#include "LayoutBox.hpp" class Layout { public: - struct LayoutContext { - glm::vec2 available_space; - glm::vec2 parent_position; - LayoutBox* parent_layout = nullptr; - }; + glm::uvec2 size = glm::uvec2(0, 0); // Основной метод вычисления layout - static void compute_layout(Node& node, const LayoutContext& context); + static void compute_layout(Node& node, LayoutContext& context); // Методы для разных типов layout - static void compute_text_layout(Node& node, Text& text, const LayoutContext& context); - static void compute_element_layout(Node& node, Element& element, const LayoutContext& context); - static void compute_block_layout(Node& node, Element& element, const LayoutContext& context); - - // Утилиты - static LayoutBox& get_layout_box(Node& node); - static void apply_styles_to_layout(Node& node); - static glm::vec2 calculate_preferred_size( - Node& node, const LayoutContext& context + static void compute_text_layout( + Node& node, Text& text, LayoutContext& context + ); + static void compute_element_layout( + Node& node, Element& element, LayoutContext& context ); private: - // Вспомогательные методы - static void calculate_margins(Node& node, const LayoutContext& context); - static void calculate_padding(Node& node); - static void calculate_borders(Node& node); - static void position_children(Node& node); - - // Работа со стилями - static float get_dimension_from_style( - const style::ComputedStyle& style, - const std::string& property, - float default_value + static float compute_element_content_width( + const Element& element, float max_width + ); + static float compute_element_content_height( + const Element& element, float children_height ); - static glm::vec2 get_dimensions_from_style( - const style::ComputedStyle& style, - const std::string& width_prop, - const std::string& height_prop, - const glm::vec2& default_size + static void set_element_position(Node& node, LayoutContext& context); + static LayoutContext create_child_context( + Node& node, float content_width, float max_height ); + static float layout_children(Node& node, LayoutContext& child_ctx); + static float layout_children_flex(Node& node, LayoutContext& ctx); }; \ No newline at end of file diff --git a/src/graphics/ui/layout/LayoutBox.hpp b/src/graphics/ui/layout/LayoutBox.hpp index 4d831b4f9..a5a3689fc 100644 --- a/src/graphics/ui/layout/LayoutBox.hpp +++ b/src/graphics/ui/layout/LayoutBox.hpp @@ -1,58 +1,18 @@ #pragma once #include + #include "graphics/ui/style/Stylesheet.h" struct LayoutBox { - glm::vec2 content_size = {0, 0}; // Размер контента - glm::vec2 position = {0, 0}; // Абсолютная позиция - glm::vec2 margin = {0, 0}; // Внешние отступы - glm::vec2 padding = {0, 0}; // Внутренние отступы - glm::vec2 border = {0, 0}; // Границы - glm::vec2 full_size = {0, 0}; // Полный размер (content + padding + border) - - // Минимальные и максимальные размеры - glm::vec2 min_size = {0, 0}; - glm::vec2 max_size = {FLT_MAX, FLT_MAX}; - - // Preferred size (для flexbox и т.д.) - std::optional preferred_size; - - // Флаги - bool dirty = true; // Нужно пересчитать layout - bool visible = true; - - // Методы для работы с размерами - glm::vec2 get_content_box() const { - return content_size; - } - - glm::vec2 get_padding_box() const { - return {content_size.x + padding.x * 2, content_size.y + padding.y * 2}; - } - - glm::vec2 get_border_box() const { - return {get_padding_box().x + border.x * 2, get_padding_box().y + border.y * 2}; - } - - glm::vec2 get_margin_box() const { - return {get_border_box().x + margin.x * 2, get_border_box().y + margin.y * 2}; - } - - // Утилиты - glm::vec2 get_inner_size() const { - return content_size; - } - - glm::vec2 get_outer_size() const { - return get_margin_box(); - } - - glm::vec4 get_content_rect() const { - return {position.x, position.y, content_size.x, content_size.y}; - } - - glm::vec4 get_padding_rect() const { - return {position.x - padding.x, position.y - padding.y, - get_padding_box().x, get_padding_box().y}; - } + float x = 0; + float y = 0; + float width = 0; + float height = 0; + float font_size = 16; +}; + +struct LayoutContext { + const LayoutBox* parent_layout = nullptr; + // const LayoutBox* current_layout = nullptr; + glm::uvec2 available_space; }; \ No newline at end of file diff --git a/src/graphics/ui/style/StyleComputer.cpp b/src/graphics/ui/style/StyleComputer.cpp index d4b6e0331..857c80ed4 100644 --- a/src/graphics/ui/style/StyleComputer.cpp +++ b/src/graphics/ui/style/StyleComputer.cpp @@ -44,7 +44,7 @@ void StyleComputer::compute(Node& node) { if (auto* element = std::get_if(¤t_node->node_type)) { for (auto& child : current_node->children) { - queue.push({&child, current_node}); + queue.push({child.get(), current_node}); } } } diff --git a/src/graphics/ui/style/value.cpp b/src/graphics/ui/style/value.cpp index 470385ce0..961161fc1 100644 --- a/src/graphics/ui/style/value.cpp +++ b/src/graphics/ui/style/value.cpp @@ -1,442 +1,467 @@ #include "value.h" -#include -#include + #include +#include #include #include +#include namespace style { + value::value() = default; - - -value::value() = default; - - -value::value(const value& other) : data(other.data) { - - - if (other.color_cache) { - color_cache = std::make_unique(*other.color_cache); - } - color_cache_valid = other.color_cache_valid; - cached_string_value = other.cached_string_value; - -} - - -value& value::operator=(const value& other) { - - if (this != &other) { - data = other.data; + value::value(const value& other) : data(other.data) { if (other.color_cache) { color_cache = std::make_unique(*other.color_cache); - } else { - color_cache.reset(); } color_cache_valid = other.color_cache_valid; cached_string_value = other.cached_string_value; } - return *this; -} - -value::value(value&& other) noexcept - : data(std::move(other.data)) - , color_cache(std::move(other.color_cache)) - , color_cache_valid(other.color_cache_valid) - , cached_string_value(std::move(other.cached_string_value)) { - - -} - - -value& value::operator=(value&& other) noexcept { - - if (this != &other) { - data = std::move(other.data); - color_cache = std::move(other.color_cache); - color_cache_valid = other.color_cache_valid; - cached_string_value = std::move(other.cached_string_value); - + value& value::operator=(const value& other) { + if (this != &other) { + data = other.data; + if (other.color_cache) { + color_cache = std::make_unique(*other.color_cache); + } else { + color_cache.reset(); + } + color_cache_valid = other.color_cache_valid; + cached_string_value = other.cached_string_value; + } + return *this; } - return *this; -} - - -value::value(std::monostate) : data(std::monostate{}) {} -value::value(const char* v) : data(std::string(v)) {} -value::value(std::string v) : data(std::move(v)) {} -value::value(int v) : data(static_cast(v)) {} -value::value(unsigned int v) : data(static_cast(v)) {} -value::value(int64_t v) : data(v) {} -value::value(double v) : data(v) {} -value::value(float v) : data(static_cast(v)) {} -value::value(bool v) : data(v) {} -value::value(glm::vec2 v) : data(v) {} -value::value(glm::vec3 v) : data(v) {} -value::value(glm::vec4 v) : data(v) {} - - - - -bool value::isNull() const { - return std::holds_alternative(data); -} - -bool value::isString() const { - return std::holds_alternative(data); -} - -bool value::isNumber() const { - return isInteger() || isFloat(); -} - -bool value::isInteger() const { - return std::holds_alternative(data); -} - -bool value::isFloat() const { - return std::holds_alternative(data); -} - -bool value::isBool() const { - return std::holds_alternative(data); -} - -bool value::isVec2() const { - return std::holds_alternative(data); -} - -bool value::isVec3() const { - return std::holds_alternative(data); -} - -bool value::isVec4() const { - return std::holds_alternative(data); -} - -value::Type value::getType() const { - if (isNull()) return Type::Null; - if (isString()) return Type::String; - if (isInteger()) return Type::Integer; - if (isFloat()) return Type::Float; - if (isBool()) return Type::Boolean; - if (isVec2()) return Type::Vec2; - if (isVec3()) return Type::Vec3; - if (isVec4()) return Type::Vec4; - return Type::Null; -} - - + value::value(value&& other) noexcept + : data(std::move(other.data)), + color_cache(std::move(other.color_cache)), + color_cache_valid(other.color_cache_valid), + cached_string_value(std::move(other.cached_string_value)) { + } + value& value::operator=(value&& other) noexcept { + if (this != &other) { + data = std::move(other.data); + color_cache = std::move(other.color_cache); + color_cache_valid = other.color_cache_valid; + cached_string_value = std::move(other.cached_string_value); + } + return *this; + } -int64_t value::asInt(int64_t def) const { - if (isInteger()) { - return std::get(data); + value::value(std::monostate) : data(std::monostate {}) { } - if (isFloat()) { - return static_cast(std::get(data)); + value::value(const char* v) : data(std::string(v)) { } - if (isString()) { - if (auto parsed = parseInt(std::get(data))) { - return *parsed; - } + value::value(std::string v) : data(std::move(v)) { } - return def; -} - -double value::asFloat(double def) const { - if (isFloat()) { - return std::get(data); + value::value(int v) : data(static_cast(v)) { } - if (isInteger()) { - return static_cast(std::get(data)); + value::value(unsigned int v) : data(static_cast(v)) { } - if (isString()) { - if (auto parsed = parseFloat(std::get(data))) { - return *parsed; - } + value::value(int64_t v) : data(v) { + } + value::value(double v) : data(v) { + } + value::value(float v) : data(static_cast(v)) { + } + value::value(bool v) : data(v) { + } + value::value(glm::vec2 v) : data(v) { + } + value::value(glm::vec3 v) : data(v) { + } + value::value(glm::vec4 v) : data(v) { } - return def; -} - - + bool value::isNull() const { + return std::holds_alternative(data); + } + bool value::isString() const { + return std::holds_alternative(data); + } + bool value::isNumber() const { + return isInteger() || isFloat(); + } -value value::fromString(const std::string& str) { - if (auto result = tryFromString(str)) { - - return *result; + bool value::isInteger() const { + return std::holds_alternative(data); } - return value(str); -} -std::optional value::tryFromString(const std::string& str) { - if (str.empty()) return std::nullopt; + bool value::isFloat() const { + return std::holds_alternative(data); + } - - if (auto val = parseInt(str)) { - return value(*val); + bool value::isBool() const { + return std::holds_alternative(data); } - if (auto val = parseFloat(str)) { - return value(*val); + bool value::isVec2() const { + return std::holds_alternative(data); } - if (auto val = parseBool(str)) { - return value(*val); + bool value::isVec3() const { + return std::holds_alternative(data); } - - if (auto color = parseColor(str)) { - return value(*color); + bool value::isVec4() const { + return std::holds_alternative(data); } - - - return std::nullopt; -} + value::Type value::getType() const { + if (isNull()) return Type::Null; + if (isString()) return Type::String; + if (isInteger()) return Type::Integer; + if (isFloat()) return Type::Float; + if (isBool()) return Type::Boolean; + if (isVec2()) return Type::Vec2; + if (isVec3()) return Type::Vec3; + if (isVec4()) return Type::Vec4; + return Type::Null; + } + int64_t value::asInt(int64_t def) const { + if (isInteger()) { + return std::get(data); + } + if (isFloat()) { + return static_cast(std::get(data)); + } + if (isString()) { + if (auto parsed = parseInt(std::get(data))) { + return *parsed; + } + } + return def; + } + double value::asFloat(double def) const { + if (isFloat()) { + return std::get(data); + } + if (isInteger()) { + return static_cast(std::get(data)); + } + if (isString()) { + if (auto parsed = parseFloat(std::get(data))) { + return *parsed; + } + } + return def; + } + glm::vec4 value::asColor(const glm::vec4& def) const { + if (isVec4()) { + return std::get(data); // если храним glm::vec4 + } + if (isString()) { + // Попытка распарсить строку в цвет: "#RRGGBBAA" или "rgba(...)" + if (auto parsed = parseColor(std::get(data))) { + return *parsed; + } + } + return def; + } -std::optional value::parseInt(const std::string& str) { - return parsers::parseInt(str); -} + float value::asFloat(float def) const { + if (isFloat()) { + return static_cast(std::get(data)); + } + if (isInteger()) { + return static_cast(std::get(data)); + } + if (isString()) { + if (auto parsed = parseFloat(std::get(data))) { + return *parsed; + } + } + return def; + } -std::optional value::parseFloat(const std::string& str) { - return parsers::parseFloat(str); -} + value value::fromString(const std::string& str) { + if (auto result = tryFromString(str)) { + return *result; + } + return value(str); + } -std::optional value::parseBool(const std::string& str) { - return parsers::parseBool(str); -} + std::optional value::tryFromString(const std::string& str) { + if (str.empty()) return std::nullopt; -std::optional value::parseColor(const std::string& str) { - return parsers::parseColor(str); -} + if (auto val = parseInt(str)) { + return value(*val); + } + if (auto val = parseFloat(str)) { + return value(*val); + } -namespace parsers { - std::optional parseInt(const std::string& str) { - if (str.empty()) return std::nullopt; - try { - size_t pos; - int64_t val = std::stoll(str, &pos); - return pos == str.length() ? std::make_optional(val) : std::nullopt; - } catch (...) { - return std::nullopt; + if (auto val = parseBool(str)) { + return value(*val); } - } - std::optional parseFloat(const std::string& str) { - if (str.empty()) return std::nullopt; - try { - size_t pos; - double val = std::stod(str, &pos); - return pos == str.length() ? std::make_optional(val) : std::nullopt; - } catch (...) { - return std::nullopt; + if (auto color = parseColor(str)) { + return value(*color); } - } - std::optional parseBool(const std::string& str) { - if (str == "true" || str == "1") return true; - if (str == "false" || str == "0") return false; return std::nullopt; } - std::optional parseColor(const std::string& str) { - if (str.empty() || str[0] != '#') return std::nullopt; - - std::string hex = str.substr(1); + std::optional value::parseInt(const std::string& str) { + return parsers::parseInt(str); + } - - hex.erase(std::remove(hex.begin(), hex.end(), ' '), hex.end()); + std::optional value::parseFloat(const std::string& str) { + return parsers::parseFloat(str); + } - if (hex.length() == 3) { - - std::string expanded; - for (char c : hex) { - expanded += c; - expanded += c; - } - hex = expanded; - } + std::optional value::parseBool(const std::string& str) { + return parsers::parseBool(str); + } - if (hex.length() == 4) { - - std::string expanded; - for (size_t i = 0; i < 4; ++i) { - expanded += hex[i]; - expanded += hex[i]; - } - hex = expanded; - } + std::optional value::parseColor(const std::string& str) { + return parsers::parseColor(str); + } - if (hex.length() == 6) { - + namespace parsers { + std::optional parseInt(const std::string& str) { + if (str.empty()) return std::nullopt; try { - unsigned int r = std::stoi(hex.substr(0, 2), nullptr, 16); - unsigned int g = std::stoi(hex.substr(2, 2), nullptr, 16); - unsigned int b = std::stoi(hex.substr(4, 2), nullptr, 16); - return glm::vec4(r / 255.0f, g / 255.0f, b / 255.0f, 1.0f); + size_t pos; + int64_t val = std::stoll(str, &pos); + return pos == str.length() ? std::make_optional(val) + : std::nullopt; } catch (...) { return std::nullopt; } } - if (hex.length() == 8) { - + std::optional parseFloat(const std::string& str) { + if (str.empty()) return std::nullopt; try { - unsigned int r = std::stoi(hex.substr(0, 2), nullptr, 16); - unsigned int g = std::stoi(hex.substr(2, 2), nullptr, 16); - unsigned int b = std::stoi(hex.substr(4, 2), nullptr, 16); - unsigned int a = std::stoi(hex.substr(6, 2), nullptr, 16); - return glm::vec4(r / 255.0f, g / 255.0f, b / 255.0f, a / 255.0f); + size_t pos; + double val = std::stod(str, &pos); + return pos == str.length() ? std::make_optional(val) + : std::nullopt; } catch (...) { return std::nullopt; } } - return std::nullopt; - } -} + std::optional parseBool(const std::string& str) { + if (str == "true" || str == "1") return true; + if (str == "false" || str == "0") return false; + return std::nullopt; + } + std::optional parseColor(const std::string& str) { + if (str.empty() || str[0] != '#') return std::nullopt; + std::string hex = str.substr(1); + hex.erase(std::remove(hex.begin(), hex.end(), ' '), hex.end()); + if (hex.length() == 3) { + std::string expanded; + for (char c : hex) { + expanded += c; + expanded += c; + } + hex = expanded; + } -bool value::operator==(const value& other) const { - - if (this->getType() != other.getType()) return false; + if (hex.length() == 4) { + std::string expanded; + for (size_t i = 0; i < 4; ++i) { + expanded += hex[i]; + expanded += hex[i]; + } + hex = expanded; + } - - switch (this->getType()) { - case Type::Null: return other.isNull(); - case Type::String: return std::get(this->data) == std::get(other.data); - case Type::Integer: return std::get(this->data) == std::get(other.data); - case Type::Float: return std::abs(std::get(this->data) - std::get(other.data)) < 1e-10; - case Type::Boolean: return std::get(this->data) == std::get(other.data); - case Type::Vec2: return std::get(this->data) == std::get(other.data); - case Type::Vec3: return std::get(this->data) == std::get(other.data); - case Type::Vec4: return std::get(this->data) == std::get(other.data); - default: return false; - } -} + if (hex.length() == 6) { + try { + unsigned int r = std::stoi(hex.substr(0, 2), nullptr, 16); + unsigned int g = std::stoi(hex.substr(2, 2), nullptr, 16); + unsigned int b = std::stoi(hex.substr(4, 2), nullptr, 16); + return glm::vec4(r / 255.0f, g / 255.0f, b / 255.0f, 1.0f); + } catch (...) { + return std::nullopt; + } + } -bool value::operator!=(const value& other) const { - return !(*this == other); -} + if (hex.length() == 8) { + try { + unsigned int r = std::stoi(hex.substr(0, 2), nullptr, 16); + unsigned int g = std::stoi(hex.substr(2, 2), nullptr, 16); + unsigned int b = std::stoi(hex.substr(4, 2), nullptr, 16); + unsigned int a = std::stoi(hex.substr(6, 2), nullptr, 16); + return glm::vec4( + r / 255.0f, g / 255.0f, b / 255.0f, a / 255.0f + ); + } catch (...) { + return std::nullopt; + } + } -std::string value::typeName() const { - switch (getType()) { - case Type::Null: return "null"; - case Type::String: return "string"; - case Type::Integer: return "integer"; - case Type::Float: return "float"; - case Type::Boolean: return "boolean"; - case Type::Vec2: return "vec2"; - case Type::Vec3: return "vec3"; - case Type::Vec4: return "vec4"; - default: return "unknown"; + return std::nullopt; + } } -} - -std::size_t value::Hash::operator()(const value& v) const { - - switch (v.getType()) { - case Type::String: - return std::hash{}(v.asString()); - case Type::Integer: - return std::hash{}(v.asInt()); - case Type::Float: - return std::hash{}(v.asFloat(0.0)); - case Type::Boolean: - return std::hash{}(v.asBool()); - case Type::Vec2: { - auto vec = v.asVec2(); - return std::hash{}(vec.x) ^ (std::hash{}(vec.y) << 1); + bool value::operator==(const value& other) const { + if (this->getType() != other.getType()) return false; + + switch (this->getType()) { + case Type::Null: + return other.isNull(); + case Type::String: + return std::get(this->data) == + std::get(other.data); + case Type::Integer: + return std::get(this->data) == + std::get(other.data); + case Type::Float: + return std::abs( + std::get(this->data) - + std::get(other.data) + ) < 1e-10; + case Type::Boolean: + return std::get(this->data) == std::get(other.data); + case Type::Vec2: + return std::get(this->data) == + std::get(other.data); + case Type::Vec3: + return std::get(this->data) == + std::get(other.data); + case Type::Vec4: + return std::get(this->data) == + std::get(other.data); + default: + return false; } - case Type::Vec3: { - auto vec = v.asVec3(); - return std::hash{}(vec.x) ^ (std::hash{}(vec.y) << 1) ^ (std::hash{}(vec.z) << 2); + } + + bool value::operator!=(const value& other) const { + return !(*this == other); + } + + std::string value::typeName() const { + switch (getType()) { + case Type::Null: + return "null"; + case Type::String: + return "string"; + case Type::Integer: + return "integer"; + case Type::Float: + return "float"; + case Type::Boolean: + return "boolean"; + case Type::Vec2: + return "vec2"; + case Type::Vec3: + return "vec3"; + case Type::Vec4: + return "vec4"; + default: + return "unknown"; } - case Type::Vec4: { - auto vec = v.asVec4(); - return std::hash{}(vec.x) ^ (std::hash{}(vec.y) << 1) ^ (std::hash{}(vec.z) << 2) ^ (std::hash{}(vec.w) << 3); + } + + std::size_t value::Hash::operator()(const value& v) const { + switch (v.getType()) { + case Type::String: + return std::hash {}(v.asString()); + case Type::Integer: + return std::hash {}(v.asInt()); + case Type::Float: + return std::hash {}(v.asFloat(0.0)); + case Type::Boolean: + return std::hash {}(v.asBool()); + case Type::Vec2: { + auto vec = v.asVec2(); + return std::hash {}(vec.x) ^ + (std::hash {}(vec.y) << 1); + } + case Type::Vec3: { + auto vec = v.asVec3(); + return std::hash {}(vec.x) ^ + (std::hash {}(vec.y) << 1) ^ + (std::hash {}(vec.z) << 2); + } + case Type::Vec4: { + auto vec = v.asVec4(); + return std::hash {}(vec.x) ^ + (std::hash {}(vec.y) << 1) ^ + (std::hash {}(vec.z) << 2) ^ + (std::hash {}(vec.w) << 3); + } + default: + return 0; } - default: - return 0; } -} + bool value::asBool(bool def) const { + if (isBool()) { + return std::get(data); + } -bool value::asBool(bool def) const { - if (isBool()) { - return std::get(data); + return def; } - return def; -} + std::string value::asString(const std::string& def) const { + if (isString()) { + return std::get(data); + } -std::string value::asString(const std::string& def) const { - if (isString()) { - return std::get(data); - } + if (isNull()) { + return def; + } - if (isNull()) { - return def; + return def; } - return def; -} + glm::vec2 value::asVec2(const glm::vec2& def) const { + if (isVec2()) { + return std::get(data); + } -glm::vec2 value::asVec2(const glm::vec2& def) const { - if (isVec2()) { - return std::get(data); + return def; } - return def; -} + glm::vec3 value::asVec3(const glm::vec3& def) const { + if (isVec3()) { + return std::get(data); + } -glm::vec3 value::asVec3(const glm::vec3& def) const { - if (isVec3()) { - return std::get(data); - } + if (isVec2()) { + auto v2 = std::get(data); + return glm::vec3(v2, 0.0f); + } - if (isVec2()) { - auto v2 = std::get(data); - return glm::vec3(v2, 0.0f); + return def; } - return def; -} + glm::vec4 value::asVec4(const glm::vec4& def) const { + if (isVec4()) { + return std::get(data); + } -glm::vec4 value::asVec4(const glm::vec4& def) const { - if (isVec4()) { - return std::get(data); - } - - if (isVec3()) { - auto v3 = std::get(data); - return glm::vec4(v3, 1.0f); - } - - if (isVec2()) { - auto v2 = std::get(data); - return glm::vec4(v2, 0.0f, 1.0f); - } + if (isVec3()) { + auto v3 = std::get(data); + return glm::vec4(v3, 1.0f); + } - return def; -} + if (isVec2()) { + auto v2 = std::get(data); + return glm::vec4(v2, 0.0f, 1.0f); + } + return def; + } } \ No newline at end of file diff --git a/src/graphics/ui/style/value.h b/src/graphics/ui/style/value.h index bae009655..f0618c9bc 100644 --- a/src/graphics/ui/style/value.h +++ b/src/graphics/ui/style/value.h @@ -87,7 +87,7 @@ namespace style { glm::vec2 asVec2(const glm::vec2& def = {}) const; glm::vec3 asVec3(const glm::vec3& def = {}) const; glm::vec4 asVec4(const glm::vec4& def = {}) const; - glm::vec4 asColor(const glm::vec4& def = {0, 0, 0, 1}) const; + glm::vec4 asColor(const glm::vec4& def = {0, 0, 0, 0}) const; std::string typeName() const; std::string toString() const; From d881be553d37a1ececc421317c3ecc09c298e597 Mon Sep 17 00:00:00 2001 From: GHOST11111100 Date: Thu, 28 Aug 2025 21:58:12 +0300 Subject: [PATCH 10/11] feat: Enhance layout and styling system with inherited properties and updated structure --- src/graphics/ui/GUI.cpp | 28 +++++++----- src/graphics/ui/elements/Node.cpp | 60 +++++++++++++++---------- src/graphics/ui/elements/Node.hpp | 10 +---- src/graphics/ui/layout/Layout.cpp | 32 +++++++------ src/graphics/ui/layout/Layout.hpp | 15 ++----- src/graphics/ui/style/StyleComputer.cpp | 44 +++++++++++++++--- src/graphics/ui/style/StyleComputer.hpp | 42 ++++++++++++----- 7 files changed, 145 insertions(+), 86 deletions(-) diff --git a/src/graphics/ui/GUI.cpp b/src/graphics/ui/GUI.cpp index db767d02f..3297833c6 100644 --- a/src/graphics/ui/GUI.cpp +++ b/src/graphics/ui/GUI.cpp @@ -60,25 +60,27 @@ GUI::GUI(Engine& engine) // Testing new UI Implementation std::string xmlString = R"( -
-

test

-

test2

+
+ Header +
+ Left Col + Right Col +
)"; std::string css_code = R"( // comment variant 1 /* comment variant 2 */ - div { - background: #111; - width: 200; - height: 200; - direction: vertical; + div.main { + background: #111f; + direction: row; + } + + div.content { + background: #222f; + direction: column; } - p { - background: #222; - height: 16; - } )"; document_root = Node::from_xml_string("example.xml", xmlString); @@ -87,6 +89,8 @@ GUI::GUI(Engine& engine) style::Stylesheet stylesheet = parser.parse(); style::StyleComputer computer; + computer.set_inherit_exceptions({"width", "height", "margin", "padding", "direction"}); + computer.set_stylesheets({stylesheet}); computer.compute(*document_root); } diff --git a/src/graphics/ui/elements/Node.cpp b/src/graphics/ui/elements/Node.cpp index 2e7f0bd95..036630d19 100644 --- a/src/graphics/ui/elements/Node.cpp +++ b/src/graphics/ui/elements/Node.cpp @@ -26,7 +26,7 @@ std::shared_ptr Node::from_xml_node(const xml::Node& xml_node) { for (size_t i = 0; i < xml_node.size(); ++i) { const xml::Node& child = xml_node.sub(i); auto child_node = from_xml_node(child); - child_node->parent = dom_node; // безопасно, shared_from_this не нужен здесь + child_node->parent = dom_node; dom_node->children.push_back(std::move(child_node)); } @@ -53,12 +53,12 @@ void Node::append_child(Node node) { auto child_ptr = std::make_shared(std::move(node)); child_ptr->parent = shared_from_this(); - children.push_back(std::move(child_ptr)); // добавляем в children + children.push_back(std::move(child_ptr)); } void Node::prepend_child(Node node) { auto child_ptr = - std::make_unique(std::move(node)); // создаём unique_ptr + std::make_unique(std::move(node)); child_ptr->parent = shared_from_this(); children.insert(children.begin(), std::move(child_ptr)); } @@ -68,7 +68,7 @@ void Node::append_childs(std::vector nodes) { for (Node node : nodes) { auto child_ptr = - std::make_unique(std::move(node)); // создаём unique_ptr + std::make_unique(std::move(node)); child_ptr->parent = shared_from_this(); children.push_back(std::move(child_ptr)); } @@ -79,7 +79,7 @@ void Node::insert_child(size_t index, Node node) { throw std::out_of_range("Index out of range"); } auto child_ptr = - std::make_unique(std::move(node)); // создаём unique_ptr + std::make_unique(std::move(node)); child_ptr->parent = shared_from_this(); children.insert(children.begin() + index, std::move(child_ptr)); } @@ -352,7 +352,6 @@ bool Node::is_root() const { return root; } -// Получение ID std::optional ElementData::getId() const { auto it = attributes.find("id"); if (it != attributes.end()) { @@ -361,7 +360,6 @@ std::optional ElementData::getId() const { return std::nullopt; } -// Установка ID void ElementData::setId(std::string_view id) { if (id.empty()) { attributes.erase("id"); @@ -370,7 +368,6 @@ void ElementData::setId(std::string_view id) { } } -// Получение списка классов std::vector ElementData::getClassList() const { std::vector classes; auto it = attributes.find("class"); @@ -386,7 +383,6 @@ std::vector ElementData::getClassList() const { return classes; } -// Установка списка классов void ElementData::setClassList(const std::vector& classes) { if (classes.empty()) { attributes.erase("class"); @@ -403,7 +399,6 @@ void ElementData::setClassList(const std::vector& classes) { attributes["class"] = class_string; } -// Добавление класса void ElementData::addClass(std::string_view className) { if (className.empty()) return; @@ -414,7 +409,6 @@ void ElementData::addClass(std::string_view className) { } } -// Удаление класса void ElementData::removeClass(std::string_view className) { if (className.empty()) return; @@ -426,7 +420,6 @@ void ElementData::removeClass(std::string_view className) { } } -// Проверка наличия класса bool ElementData::hasClass(std::string_view className) const { if (className.empty()) return false; @@ -435,7 +428,6 @@ bool ElementData::hasClass(std::string_view className) const { classes.end(); } -// Переключение класса void ElementData::toggleClass(std::string_view className) { if (className.empty()) return; @@ -451,7 +443,7 @@ void Node::draw(const DrawContext& ctx, const Assets& assets) { context.available_space = ctx.getViewport(); context.parent_layout = nullptr; - Layout::compute_layout(*this, context); + Layout::compute_layout(*this, context, assets); draw(ctx, assets, context); } @@ -461,13 +453,8 @@ void Node::draw( ) { auto batch = ctx.getBatch2D(); - // Создаём под-контекст для локального управления состоянием DrawContext childCtx = ctx.sub(); - // Определяем viewport/скиссоры, если нужно (для примера оставим полный размер) - // childCtx.setScissors(glm::vec4(layout.x, layout.y, layout.width, layout.height)); - - // Сбрасываем батч перед рисованием текущей ноды batch->flush(); batch->untexture(); @@ -483,16 +470,41 @@ void Node::draw( node_type ); - // Рекурсивно рисуем детей в отдельном под-контексте for (auto& child : children) { child->draw(childCtx, assets, context); } - // Сбрасываем батч после рисования всех детей, чтобы гарантировать корректное состояние OpenGL batch->flush(); batch->untexture(); } +glm::vec4 Node::getInheritedTextColor() const { + // Если у элемента есть явно заданный цвет текста — используем его + if (auto* element = std::get_if(&node_type)) { + glm::vec4 color = glm::vec4(1.0f); + + if (element->style.has("color")) { + style::value colorStyle = element->style.get("color", "#ffffff"); + color = colorStyle.asColor(); + } + + if (has_attribute("color")) { + style::value colorAttr = *get_attribute("color"); + color = colorAttr.asColor(); // конвертация "#RRGGBB" или "#RRGGBBAA" в glm::vec4 + } + + return color; + } + + // Если родитель есть, рекурсивно спрашиваем его + if (auto p = parent.lock()) { + return p->getInheritedTextColor(); + } + + // Базовый цвет по умолчанию + return glm::vec4(1.0f); +} + void Node::draw_text(const DrawContext& ctx, const Assets& assets, Text text) { auto batch = ctx.getBatch2D(); @@ -501,10 +513,10 @@ void Node::draw_text(const DrawContext& ctx, const Assets& assets, Text text) { auto font = assets.get(FONT_DEFAULT); - batch->setColor(glm::vec4(1.0f, 1.0f, 1.0f, 1.0f)); + batch->setColor( getInheritedTextColor() ); font->draw( *batch, - util::str2wstr_utf8("A"), + util::str2wstr_utf8(get_text()), layout.x, layout.y, text.styles.get(), @@ -520,6 +532,6 @@ void Node::draw_element( batch->flush(); batch->untexture(); - batch->setColor(element.style.get("background", "#ff0000").asColor()); + batch->setColor(element.style.get("background", "#0000").asColor()); batch->rect(layout.x, layout.y, layout.width, layout.height); } diff --git a/src/graphics/ui/elements/Node.hpp b/src/graphics/ui/elements/Node.hpp index 67bf84862..7e1d8a542 100644 --- a/src/graphics/ui/elements/Node.hpp +++ b/src/graphics/ui/elements/Node.hpp @@ -70,7 +70,6 @@ struct ElementState { size_t nth_child = 0; size_t nth_of_type = 0; - // Оператор для использования в хэш-таблицах bool operator==(const ElementState& other) const { return hover == other.hover && focus == other.focus && active == other.active && checked == other.checked && @@ -83,7 +82,6 @@ struct ElementStateHash { std::size_t operator()(const ElementState& state) const { size_t seed = 0; - // Простая реализация hash_combine auto hash_combine = [](size_t& seed, size_t value) { seed ^= value + 0x9e3779b9 + (seed << 6) + (seed >> 2); }; @@ -169,23 +167,20 @@ struct Node : public std::enable_shared_from_this { node_type(Element(tag, std::move(attrs))) { } - // Копирующий конструктор (глубокое копирование детей) Node(const Node& other) : root(other.root), node_type(other.node_type) { children.reserve(other.children.size()); for (const auto& child : other.children) { children.push_back( std::make_unique(*child) - ); // клонируем каждого ребенка + ); } } - // Копирующий оператор присваивания Node& operator=(const Node& other) { if (this != &other) { root = other.root; node_type = other.node_type; - // Глубокое копирование детей children.clear(); children.reserve(other.children.size()); for (const auto& child : other.children) { @@ -195,14 +190,12 @@ struct Node : public std::enable_shared_from_this { return *this; } - // Перемещающий конструктор Node(Node&& other) noexcept : root(std::move(other.root)), children(std::move(other.children)), node_type(std::move(other.node_type)) { } - // Перемещающий оператор присваивания Node& operator=(Node&& other) noexcept { if (this != &other) { root = std::move(other.root); @@ -289,6 +282,7 @@ struct Node : public std::enable_shared_from_this { const std::vector& parts, size_t index ) const; + glm::vec4 getInheritedTextColor() const; void draw_text(const DrawContext& ctx, const Assets& assets, Text text); void draw_element( const DrawContext& ctx, const Assets& assets, Element element diff --git a/src/graphics/ui/layout/Layout.cpp b/src/graphics/ui/layout/Layout.cpp index c13471a43..d9f085388 100644 --- a/src/graphics/ui/layout/Layout.cpp +++ b/src/graphics/ui/layout/Layout.cpp @@ -1,18 +1,20 @@ #include "Layout.hpp" +#include "assets/Assets.hpp" +#include "constants.hpp" #include // Основной метод вычисления layout -void Layout::compute_layout(Node& node, LayoutContext& context) { +void Layout::compute_layout(Node& node, LayoutContext& context, const Assets& assets) { if (!node.is_layout_dirty()) return; std::visit( [&](auto& inner) { using T = std::decay_t; if constexpr (std::is_same_v) { - compute_text_layout(node, inner, context); + compute_text_layout(node, inner, context, assets); } else if constexpr (std::is_same_v) { - compute_element_layout(node, inner, context); + compute_element_layout(node, inner, context, assets); } }, node.node_type @@ -23,11 +25,10 @@ void Layout::compute_layout(Node& node, LayoutContext& context) { // --- Текстовый layout --- void Layout::compute_text_layout( - Node& node, Text& text, LayoutContext& context + Node& node, Text& text, LayoutContext& context, const Assets& assets ) { - // Вычисляем размеры текста - float char_width = node.layout.font_size * 0.6f; - float text_width = char_width * static_cast(text.content.size()); + auto font = assets.get(FONT_DEFAULT); + float text_width = font->calcWidth(util::str2wstr_utf8(text.content), 0,text.content.size()); float text_height = node.layout.font_size; node.layout.width = text_width; @@ -53,7 +54,7 @@ LayoutContext Layout::create_child_context( // --- Основной layout для элемента --- void Layout::compute_element_layout( - Node& node, Element& element, LayoutContext& context + Node& node, Element& element, LayoutContext& context, const Assets& assets ) { // 1) Позиционируем элемент @@ -67,25 +68,28 @@ void Layout::compute_element_layout( .asFloat(max_height); // временно bool vertical = - element.style.get("direction", "horizontal").asString("vertical") == - "vertical"; + element.style.get("direction", "column").asString("row") == + "row"; // 3) Раскладываем детей по flex float main_cursor = 0.f; // основная ось - LayoutContext child_ctx = - create_child_context(node, node.layout.width, node.layout.height); + LayoutContext child_ctx; + + if (vertical) child_ctx = create_child_context(node, node.layout.width, node.layout.height / node.children.size()); + else child_ctx = create_child_context(node, node.layout.width / node.children.size(), node.layout.height ); + for (auto& child : node.children) { if (vertical) { child->layout.x = node.layout.x; child->layout.y = node.layout.y + main_cursor; - compute_layout(*child, child_ctx); + compute_layout(*child, child_ctx, assets); main_cursor += child->layout.height; } else { child->layout.x = node.layout.x + main_cursor; child->layout.y = node.layout.y; - compute_layout(*child, child_ctx); + compute_layout(*child, child_ctx, assets); main_cursor += child->layout.width; } } diff --git a/src/graphics/ui/layout/Layout.hpp b/src/graphics/ui/layout/Layout.hpp index 8f9361261..54192886b 100644 --- a/src/graphics/ui/layout/Layout.hpp +++ b/src/graphics/ui/layout/Layout.hpp @@ -7,26 +7,19 @@ class Layout { glm::uvec2 size = glm::uvec2(0, 0); // Основной метод вычисления layout - static void compute_layout(Node& node, LayoutContext& context); + static void compute_layout(Node& node, LayoutContext& context, const Assets& assets); // Методы для разных типов layout static void compute_text_layout( - Node& node, Text& text, LayoutContext& context + Node& node, Text& text, LayoutContext& context, const Assets& assets ); static void compute_element_layout( - Node& node, Element& element, LayoutContext& context + Node& node, Element& element, LayoutContext& context, const Assets& assets ); + private: - static float compute_element_content_width( - const Element& element, float max_width - ); - static float compute_element_content_height( - const Element& element, float children_height - ); static void set_element_position(Node& node, LayoutContext& context); static LayoutContext create_child_context( Node& node, float content_width, float max_height ); - static float layout_children(Node& node, LayoutContext& child_ctx); - static float layout_children_flex(Node& node, LayoutContext& ctx); }; \ No newline at end of file diff --git a/src/graphics/ui/style/StyleComputer.cpp b/src/graphics/ui/style/StyleComputer.cpp index 857c80ed4..f1ffd921a 100644 --- a/src/graphics/ui/style/StyleComputer.cpp +++ b/src/graphics/ui/style/StyleComputer.cpp @@ -3,10 +3,10 @@ #include #include #include -#include -#include #include +#include #include +#include #include "DeclarationParser.hpp" @@ -53,12 +53,32 @@ void StyleComputer::compute(Node& node) { void StyleComputer::compute_base(Node& node, Node& parent) { if (auto* element = std::get_if(&node.node_type)) { if (auto* parent_element = std::get_if(&parent.node_type)) { - element->style = parent_element->style; - element->state_styles = parent_element->state_styles; + for (auto& [name, value] : parent_element->style.properties) { + if (std::find( + inherit_exceptions.begin(), + inherit_exceptions.end(), + name + ) != inherit_exceptions.end()) + continue; + element->style.set(name, value); + } + + for (auto& [state, style] : parent_element->state_styles) { + auto& child_state_style = element->state_styles[state]; + for (auto& [name, value] : style.properties) { + if (std::find( + inherit_exceptions.begin(), + inherit_exceptions.end(), + name + ) != inherit_exceptions.end()) + continue; + child_state_style.set(name, value); + } + } } } - compute_base(node); + compute_base(node); // базовые стили } void StyleComputer::compute_base(Node& node) { @@ -103,8 +123,18 @@ void StyleComputer::compute_base(Node& node) { // Сохраняем стили в элемент if (auto* element = std::get_if(&node.node_type)) { - element->style = std::move(base_style); - element->state_styles = std::move(state_styles); + // Перебираем все свойства из base_style + for (auto& [name, value] : base_style.properties) { + element->style.set(name, value); + } + + // То же самое для псевдостилей + for (auto& [state, style] : state_styles) { + auto& target_state_style = element->state_styles[state]; + for (auto& [name, value] : style.properties) { + target_state_style.set(name, value); + } + } } } diff --git a/src/graphics/ui/style/StyleComputer.hpp b/src/graphics/ui/style/StyleComputer.hpp index 81f144684..46f7e130c 100644 --- a/src/graphics/ui/style/StyleComputer.hpp +++ b/src/graphics/ui/style/StyleComputer.hpp @@ -15,25 +15,47 @@ namespace style { void set_stylesheets(const std::vector& sheets); void compute(Node& node); + void set_inherit_exceptions(const std::vector& props) { + inherit_exceptions = props; + } private: + std::vector inherit_exceptions; Stylesheet stylesheet; void compute_base(Node& node, Node& parent); void compute_base(Node& node); - bool match_selector(Node& node, Selector& selector, Node* parent = nullptr); + bool match_selector( + Node& node, Selector& selector, Node* parent = nullptr + ); template - bool process_selector_variant( Node& node, const T& selector, Node* parent = nullptr); - bool process_simple_selector(Node& node, const SimpleSelector& selector); - bool process_complex_selector(Node& node, ComplexSelector selector, Node* parent = nullptr); - bool check_pseudo_classes( Node& node, const SimpleSelector& selector ); - bool check_nth_child(const Element& element, const std::string& formula); - bool check_nth_of_type(const Element& element, const std::string& formula); - bool check_custom_pseudo_class(const Element& element, const std::string& name); + bool process_selector_variant( + Node& node, const T& selector, Node* parent = nullptr + ); + bool process_simple_selector( + Node& node, const SimpleSelector& selector + ); + bool process_complex_selector( + Node& node, ComplexSelector selector, Node* parent = nullptr + ); + bool check_pseudo_classes(Node& node, const SimpleSelector& selector); + bool check_nth_child( + const Element& element, const std::string& formula + ); + bool check_nth_of_type( + const Element& element, const std::string& formula + ); + bool check_custom_pseudo_class( + const Element& element, const std::string& name + ); bool is_default_state(const ElementState& state); ElementState extract_element_state(const Selector& selector); - void extract_state_from_simple_selector(const SimpleSelector& selector, ElementState& state); - void extract_state_from_complex_selector(const ComplexSelector& selector, ElementState& state); + void extract_state_from_simple_selector( + const SimpleSelector& selector, ElementState& state + ); + void extract_state_from_complex_selector( + const ComplexSelector& selector, ElementState& state + ); }; } \ No newline at end of file From 960edbbccdee5835238061034d16f6dce918e39b Mon Sep 17 00:00:00 2001 From: GHOST11111100 Date: Fri, 19 Sep 2025 13:43:33 +0300 Subject: [PATCH 11/11] feat: Add rounded rectangle drawing functionality and enhance layout measurement and positioning --- src/graphics/core/Batch2D.cpp | 61 ++++ src/graphics/core/Batch2D.hpp | 5 + src/graphics/ui/GUI.cpp | 28 +- src/graphics/ui/elements/Node.cpp | 47 ++- src/graphics/ui/layout/Layout.cpp | 508 ++++++++++++++++++++++++--- src/graphics/ui/layout/Layout.hpp | 88 ++++- src/graphics/ui/layout/LayoutBox.hpp | 15 +- src/graphics/ui/style/value.cpp | 2 +- src/graphics/ui/style/value.h | 2 +- 9 files changed, 659 insertions(+), 97 deletions(-) diff --git a/src/graphics/core/Batch2D.cpp b/src/graphics/core/Batch2D.cpp index b39866cef..5cd295464 100644 --- a/src/graphics/core/Batch2D.cpp +++ b/src/graphics/core/Batch2D.cpp @@ -3,6 +3,7 @@ #include "Texture.hpp" #include "gl_util.hpp" #include "maths/UVRegion.hpp" +#include #include @@ -342,6 +343,66 @@ void Batch2D::triangle(float x1, float y1, float x2, float y2, float x3, float y vertex({x3, y3}, {x3, y3}, color.r, color.g, color.b, color.a); } +void Batch2D::roundedRect(float x, float y, float w, float h, float radius, float borderWidth) { + if (radius <= 0.0f) { + if (borderWidth <= 0.0f) { + rect(x, y, w, h); // просто заполненный + } else { + // Нарисовать бордер как 4 линии + lineRect(x, y, w, h); + // Если borderWidth > 1, можно рисовать несколько слоёв + } + return; + } + + int segments = 8; // количество треугольников на угол + float angleStep = HALF_PI / segments; + + if (borderWidth <= 0.0f) { + // --- Заполненный прямоугольник с radius --- + rect(x + radius, y, w - 2*radius, h); + rect(x, y + radius, radius, h - 2*radius); + rect(x + w - radius, y + radius, radius, h - 2*radius); + + for (int i = 0; i < segments; ++i) { + float theta0 = i * angleStep; + float theta1 = (i + 1) * angleStep; + + // Верхний левый угол + triangle(x + radius, y + radius, + x + radius - radius * cos(theta0), y + radius - radius * sin(theta0), + x + radius - radius * cos(theta1), y + radius - radius * sin(theta1)); + + // Верхний правый угол + triangle(x + w - radius, y + radius, + x + w - radius + radius * cos(theta0), y + radius - radius * sin(theta0), + x + w - radius + radius * cos(theta1), y + radius - radius * sin(theta1)); + + // Нижний левый угол + triangle(x + radius, y + h - radius, + x + radius - radius * cos(theta0), y + h - radius + radius * sin(theta0), + x + radius - radius * cos(theta1), y + h - radius + radius * sin(theta1)); + + // Нижний правый угол + triangle(x + w - radius, y + h - radius, + x + w - radius + radius * cos(theta0), y + h - radius + radius * sin(theta0), + x + w - radius + radius * cos(theta1), y + h - radius + radius * sin(theta1)); + } + } else { + // --- Нарисовать только бордер с толщиной --- + // Можно аппроксимировать как 4 полоски + дуги + // Верхняя полоса + roundedRect(x, y, w, borderWidth, radius); + // Нижняя полоса + roundedRect(x, y + h - borderWidth, w, borderWidth, radius); + // Левая полоса + roundedRect(x, y + borderWidth, borderWidth, h - 2*borderWidth, radius); + // Правая полоса + roundedRect(x + w - borderWidth, y + borderWidth, borderWidth, h - 2*borderWidth, radius); + } +} + + void Batch2D::sprite(float x, float y, float w, float h, const UVRegion& region, glm::vec4 tint){ rect(x, y, w, h, region.u1, region.v1, region.u2-region.u1, region.v2-region.v1, tint.r, tint.g, tint.b, tint.a); } diff --git a/src/graphics/core/Batch2D.hpp b/src/graphics/core/Batch2D.hpp index ec181a6fb..aca3c3d94 100644 --- a/src/graphics/core/Batch2D.hpp +++ b/src/graphics/core/Batch2D.hpp @@ -7,6 +7,9 @@ #include "maths/UVRegion.hpp" #include "MeshData.hpp" +constexpr float PI = 3.14159265358979323846f; +constexpr float HALF_PI = PI / 2.0f; + template class Mesh; class Texture; @@ -122,4 +125,6 @@ class Batch2D : public Flushable { void flush() override; void lineWidth(float width); + + void roundedRect(float x, float y, float w, float h, float radius, float borderWidth = 0.0f); }; diff --git a/src/graphics/ui/GUI.cpp b/src/graphics/ui/GUI.cpp index 3297833c6..5ed2a6f39 100644 --- a/src/graphics/ui/GUI.cpp +++ b/src/graphics/ui/GUI.cpp @@ -61,11 +61,12 @@ GUI::GUI(Engine& engine) std::string xmlString = R"(
- Header -
- Left Col - Right Col -
+ + Left + + + Right +
)"; std::string css_code = R"( @@ -73,12 +74,19 @@ GUI::GUI(Engine& engine) /* comment variant 2 */ div.main { background: #111f; - direction: row; + align-x: center; + align-y: center; } - div.content { - background: #222f; - direction: column; + span { + width: 100; + height: 100; + align-x: center; + align-y: center; + + border-width: 1; + border-color: #ff0000; + border-radius: 15; } )"; @@ -89,7 +97,7 @@ GUI::GUI(Engine& engine) style::Stylesheet stylesheet = parser.parse(); style::StyleComputer computer; - computer.set_inherit_exceptions({"width", "height", "margin", "padding", "direction"}); + computer.set_inherit_exceptions({"width", "height", "margin", "padding", "direction", "align-x", "align-y", "border-width","border-color", "border-radius"}); computer.set_stylesheets({stylesheet}); computer.compute(*document_root); diff --git a/src/graphics/ui/elements/Node.cpp b/src/graphics/ui/elements/Node.cpp index 036630d19..c8eab8eb0 100644 --- a/src/graphics/ui/elements/Node.cpp +++ b/src/graphics/ui/elements/Node.cpp @@ -517,21 +517,58 @@ void Node::draw_text(const DrawContext& ctx, const Assets& assets, Text text) { font->draw( *batch, util::str2wstr_utf8(get_text()), - layout.x, - layout.y, + layout.position.x, + layout.position.y, text.styles.get(), 0 ); } +void roundedBorder(Batch2D* batch, float x, float y, float w, float h, float radius, float width) { + // Нарисовать 4 полоски + batch->roundedRect(x, y, w, h, radius); +} + + void Node::draw_element( const DrawContext& ctx, const Assets& assets, Element element ) { auto batch = ctx.getBatch2D(); - batch->flush(); batch->untexture(); - batch->setColor(element.style.get("background", "#0000").asColor()); - batch->rect(layout.x, layout.y, layout.width, layout.height); + // --- Вычисляем стили --- + glm::vec4 bgColor = element.style.get("background", "#0000").asColor(); + glm::vec4 borderColor = element.style.get("border-color", "#0000").asColor(); + float borderWidth = element.style.get("border-width", 0.0f).asFloat(); + float borderRadius = element.style.get("border-radius", 0.0f).asFloat(); + + float x = layout.position.x; + float y = layout.position.y; + float w = layout.size.x; + float h = layout.size.y; + + // --- Рисуем фон --- + if (bgColor.a > 0.0f) { + batch->setColor(bgColor); + + if (borderRadius > 0.0f) { + // Нужно реализовать rect с радиусом + roundedBorder(batch, x, y, w, h, borderRadius, borderWidth); + } else { + batch->rect(x, y, w, h); + } + } + + // --- Рисуем бордер --- + if (borderWidth > 0.0f && borderColor.a > 0.0f) { + batch->setColor(borderColor); + + if (borderRadius > 0.0f) { + roundedBorder(batch, x, y, w, h, borderRadius, borderWidth); + } else { + batch->lineRect(x, y, w, h); + // Если borderWidth > 1, можно рисовать несколько lineRect с шагом + } + } } diff --git a/src/graphics/ui/layout/Layout.cpp b/src/graphics/ui/layout/Layout.cpp index d9f085388..d1bebdeaf 100644 --- a/src/graphics/ui/layout/Layout.cpp +++ b/src/graphics/ui/layout/Layout.cpp @@ -1,96 +1,488 @@ #include "Layout.hpp" + +#include +#include + #include "assets/Assets.hpp" #include "constants.hpp" -#include +void Layout::compute_layout( + Node& root, LayoutContext& ctx, const Assets& assets +) { + measure(root, ctx, assets); -// Основной метод вычисления layout -void Layout::compute_layout(Node& node, LayoutContext& context, const Assets& assets) { - if (!node.is_layout_dirty()) return; + position_node(root, glm::vec2(0, 0)); +} +void Layout::measure( + Node& node, const LayoutContext& ctx, const Assets& assets +) { std::visit( [&](auto& inner) { using T = std::decay_t; if constexpr (std::is_same_v) { - compute_text_layout(node, inner, context, assets); + measure_text(node, inner, ctx, assets); } else if constexpr (std::is_same_v) { - compute_element_layout(node, inner, context, assets); + measure_element(node, inner, ctx, assets); } }, node.node_type ); - - node.mark_layout_clean(); } -// --- Текстовый layout --- -void Layout::compute_text_layout( - Node& node, Text& text, LayoutContext& context, const Assets& assets +// --- Текст --- +void Layout::measure_text( + Node& node, Text& text, const LayoutContext& ctx, const Assets& assets ) { auto font = assets.get(FONT_DEFAULT); - float text_width = font->calcWidth(util::str2wstr_utf8(text.content), 0,text.content.size()); - float text_height = node.layout.font_size; + float w = font->calcWidth( + util::str2wstr_utf8(text.content), 0, text.content.size() + ); + float h = 16; + + glm::vec2 size = {w, h}; + node.layout.content_size = size; + node.layout.size = size; +} + +// --- Элемент --- +void Layout::measure_element( + Node& node, Element& element, const LayoutContext& ctx, const Assets& assets +) { + using std::max; + using std::numeric_limits; + + FlexDirection dir = get_direction(element); + + BoxSides padding = get_padding(element.style); + BoxSides border = get_border(element.style); + BoxSides margin = get_margin( + element.style + ); // margin контейнера (НЕ добавлять в node.layout.size!) + + // 1) Доступное пространство внутри border + padding (content box) + glm::vec2 content_available = ctx.available_space; + content_available.x = + max(0.0f, + content_available.x - (border.horizontal() + padding.horizontal())); + content_available.y = + max(0.0f, + content_available.y - (border.vertical() + padding.vertical())); + + // Первый проход: измеряем детей так, чтобы получить их intrinsic cross-axis + // размеры. + float total_fixed = 0.0f; + size_t flexible_count = 0; + + std::vector child_margins; + child_margins.reserve(node.children.size()); + + for (auto& child : node.children) { + BoxSides cm(0.0f); + Element* child_el = nullptr; + if (auto* nt = std::get_if(&child->node_type)) { + child_el = nt; + cm = get_margin(child_el->style); + } + child_margins.push_back(cm); + + LayoutContext child_ctx; + child_ctx.parent_layout = &node.layout; + + // Передаём на cross-оси реальную доступную ширину/высоту, + // а на main-оси — "неограниченно", чтобы получить intrinsic size. + glm::vec2 child_avail = content_available; + if (dir == FlexDirection::Column) { + child_avail.x = max( + 0.0f, child_avail.x - cm.horizontal() + ); // cross constrained by container content width minus child + // margins + child_avail.y = + numeric_limits::infinity(); // allow child to measure + // its intrinsic height + } else { // Row + child_avail.y = + max(0.0f, child_avail.y - cm.vertical()); // cross constrained + child_avail.x = + numeric_limits::infinity(); // allow intrinsic width + } + child_ctx.available_space = child_avail; + + measure(*child, child_ctx, assets); + + auto& cl = child->get_layout(); + if (dir == FlexDirection::Column) { + if (child_el && child_el->style.has("height")) + total_fixed += cl.size.y + cm.vertical(); + else + flexible_count++; + } else { + if (child_el && child_el->style.has("width")) + total_fixed += cl.size.x + cm.horizontal(); + else + flexible_count++; + } + } + + // 2) Вычисляем stretch_size для гибких детей + float remaining_main = + (dir == FlexDirection::Column ? content_available.y + : content_available.x) - + total_fixed; + remaining_main = max(0.0f, remaining_main); + float stretch_size = + (flexible_count > 0) ? (remaining_main / float(flexible_count)) : 0.0f; + + // 3) Второй проход: перемеряем гибких детей, передав им конкретный + // main-size = stretch_size + for (size_t i = 0; i < node.children.size(); ++i) { + auto& child = node.children[i]; + auto& cm = child_margins[i]; + Element* child_el = nullptr; + if (auto* nt = std::get_if(&child->node_type)) child_el = nt; + + bool is_fixed_main = (dir == FlexDirection::Column) + ? (child_el && child_el->style.has("height")) + : (child_el && child_el->style.has("width")); + + if (!is_fixed_main) { + LayoutContext child_ctx; + child_ctx.parent_layout = &node.layout; - node.layout.width = text_width; - node.layout.height = text_height; + glm::vec2 child_avail = content_available; + if (dir == FlexDirection::Column) { + // main available = stretch_size минус margin вертикальные (не + // может быть отрицательным) + child_avail.y = max(0.0f, stretch_size - cm.vertical()); + child_avail.x = max(0.0f, child_avail.x - cm.horizontal()); + } else { + child_avail.x = max(0.0f, stretch_size - cm.horizontal()); + child_avail.y = max(0.0f, child_avail.y - cm.vertical()); + } + child_ctx.available_space = child_avail; + + measure(*child, child_ctx, assets); - if (auto p = node.parent.lock()) { // проверяем, что родитель существует - node.layout.x = p->layout.x; - node.layout.y = p->layout.y; + // Убедимся, что главный размер ребёнка = stretch_size + // (border/padding внутри child.get_layout().size уже учтены) + auto& cl = child->get_layout(); + if (dir == FlexDirection::Column) + cl.size.y = stretch_size; + else + cl.size.x = stretch_size; + } else { + // у фиксированных детей можно (опционально) убедиться, что их + // размер не превышает content_available + auto& cl = child->get_layout(); + if (dir == FlexDirection::Column) + cl.size.y = std::min(cl.size.y, content_available.y); + else + cl.size.x = std::min(cl.size.x, content_available.x); + } + } + + // 4) Вычисляем content_size контейнера (children sizes + margins!) + glm::vec2 content_size(0.0f); + for (size_t i = 0; i < node.children.size(); ++i) { + auto& child = node.children[i]; + auto& cl = child->get_layout(); + const BoxSides& cm = child_margins[i]; + + if (dir == FlexDirection::Column) { + content_size.x = + max(content_size.x, + cl.size.x + cm.horizontal()); // add margins + content_size.y += cl.size.y + cm.vertical(); + } else { + content_size.x += cl.size.x + cm.horizontal(); + content_size.y = max(content_size.y, cl.size.y + cm.vertical()); + } } + + glm::vec2 final_content = content_size; + + if (element.style.has("width")) + final_content.x = + element.style.get("width", 0).asFloat(final_content.x); + + if (element.style.has("height")) + final_content.y = + element.style.get("height", 0).asFloat(final_content.y); + + // 6) Если размеры не заданы — растягиваем + if (!element.style.has("width")) { + if (dir == FlexDirection::Row) { + // main-ось = X → растягиваем по X + final_content.x = std::max(content_size.x, content_available.x); + } else { + // cross-ось = X → растягиваем на всё + final_content.x = content_available.x; + } + } + + if (!element.style.has("height")) { + if (dir == FlexDirection::Column) { + // main-ось = Y → растягиваем по Y + final_content.y = std::max(content_size.y, content_available.y); + } else { + // cross-ось = Y → растягиваем на всё + final_content.y = content_available.y; + } + } + + // 7) Итоговый размер (margin не входит) + node.layout.size = glm::vec2( + final_content.x + padding.horizontal() + border.horizontal(), + final_content.y + padding.vertical() + border.vertical() + ); } -// --- Создание контекста для детей --- -LayoutContext Layout::create_child_context( - Node& node, float content_width, float max_height -) { - LayoutContext ctx; - ctx.parent_layout = &node.layout; - ctx.available_space = { - static_cast(content_width), static_cast(max_height) - }; - return ctx; +// --- Основная точка входа для позиционирования --- +void Layout::position_node(Node& node, const glm::vec2& offset) { + std::visit( + [&](auto& inner) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + position_text(node, inner, offset); + } else if constexpr (std::is_same_v) { + position_element(node, inner, offset); + } + }, + node.node_type + ); +} + +// --- Позиционирование текста --- +void Layout::position_text(Node& node, Text&, const glm::vec2& offset) { + // Если есть родитель, используем его для вычисления container_size и align + if (auto parent_ptr = node.parent.lock()) { + Node& parent = *parent_ptr; + Element& p_element = *std::get_if(&parent.node_type); + + + BoxSides padding = get_padding(p_element.style); + BoxSides border = get_border(p_element.style); + + FlexDirection dir = get_direction(p_element); // направление родителя + + glm::vec2 content_offset = parent.layout.position + + glm::vec2(border.left + padding.left, + border.top + padding.top); + + glm::vec2 container_size = parent.layout.size - + glm::vec2(border.horizontal() + padding.horizontal(), + border.vertical() + padding.vertical()); + + std::string align_x = p_element.style.get("align-x", "start").asString(); + std::string align_y = p_element.style.get("align-y", "start").asString(); + + float x_extra = 0.0f; + float y_extra = 0.0f; + + // cross-выравнивание по X + if (dir == FlexDirection::Column) { + x_extra = cross_offset(container_size.x, node.layout.size.x, align_x); + } else { // Row + x_extra = cross_offset(container_size.x, node.layout.size.x, align_x); + } + + // cross-выравнивание по Y + if (dir == FlexDirection::Column) { + y_extra = cross_offset(container_size.y, node.layout.size.y, align_y); + } else { // Row + y_extra = cross_offset(container_size.y, node.layout.size.y, align_y); + } + + node.layout.position = content_offset + glm::vec2(x_extra, y_extra); + + } else { + // корневая нода + node.layout.position = offset; + } + + // Рекурсивно позиционируем детей текста + for (auto& child : node.children) { + if (auto* txt = std::get_if(&child->node_type)) { + position_text(*child, *txt, node.layout.position); + } else if (auto* el = std::get_if(&child->node_type)) { + position_element(*child, *el, node.layout.position); + } + } } -// --- Основной layout для элемента --- -void Layout::compute_element_layout( - Node& node, Element& element, LayoutContext& context, const Assets& assets + + +// --- Позиционирование элемента --- +void Layout::position_element( + Node& node, Element& element, const glm::vec2& offset ) { - // 1) Позиционируем элемент + node.layout.position = offset; - float max_width = static_cast(context.available_space.x); - float max_height = static_cast(context.available_space.y); + BoxSides padding = get_padding(element.style); + BoxSides border = get_border(element.style); + FlexDirection dir = get_direction(element); - // 2) Вычисляем размеры элемента - node.layout.width = - element.style.get("width", max_width).asFloat(max_width); - node.layout.height = element.style.get("height", max_height) - .asFloat(max_height); // временно + glm::vec2 content_offset = offset + glm::vec2(border.left + padding.left, + border.top + padding.top); - bool vertical = - element.style.get("direction", "column").asString("row") == - "row"; + std::string align_x = element.style.get("align-x", "start").asString(); + std::string align_y = element.style.get("align-y", "start").asString(); - // 3) Раскладываем детей по flex + glm::vec2 container_size = node.layout.size - + glm::vec2(border.horizontal() + padding.horizontal(), + border.vertical() + padding.vertical()); - float main_cursor = 0.f; // основная ось + // 1) Общий размер вдоль main-оси + float total_main = 0.0f; + for (auto& child : node.children) { + auto& cl = child->get_layout(); + BoxSides cm = get_child_margin(*child); + total_main += get_child_main_size(cl, cm, dir); + } - LayoutContext child_ctx; - - if (vertical) child_ctx = create_child_context(node, node.layout.width, node.layout.height / node.children.size()); - else child_ctx = create_child_context(node, node.layout.width / node.children.size(), node.layout.height ); + // 2) Начальная позиция вдоль main + float container_main = (dir == FlexDirection::Column) ? container_size.y : container_size.x; + std::string main_align = (dir == FlexDirection::Column) ? align_y : align_x; + float cursor_main = initial_cursor(container_main, total_main, main_align); + // 3) Раскладываем детей for (auto& child : node.children) { - if (vertical) { - child->layout.x = node.layout.x; - child->layout.y = node.layout.y + main_cursor; - compute_layout(*child, child_ctx, assets); - main_cursor += child->layout.height; - } else { - child->layout.x = node.layout.x + main_cursor; - child->layout.y = node.layout.y; - compute_layout(*child, child_ctx, assets); - main_cursor += child->layout.width; + auto& cl = child->get_layout(); + BoxSides cm = get_child_margin(*child); + + glm::vec2 child_offset = content_offset; + + if (dir == FlexDirection::Column) { + float cross_extra = cross_offset( + container_size.x, + cl.size.x + cm.horizontal(), + align_x + ); + + child_offset.x += cm.left + cross_extra; + child_offset.y += cursor_main + cm.top; + + cursor_main += cl.size.y + cm.vertical(); + } else { // Row + float cross_extra = cross_offset( + container_size.y, + cl.size.y + cm.vertical(), + align_y + ); + + child_offset.x += cursor_main + cm.left; + child_offset.y += cm.top + cross_extra; + + cursor_main += cl.size.x + cm.horizontal(); } + + position_node(*child, child_offset); } } + + +// ------------------------------------------------------------------------------------- + +FlexDirection Layout::get_direction(Element& element) { + std::string dir = + element.style.get("direction", "column").asString("column"); + if (dir == "row") return FlexDirection::Row; + return FlexDirection::Column; +} + +BoxSides Layout::get_padding(const style::ComputedStyle& style) { + BoxSides box = BoxSides(style.get("padding", 0).asFloat()); + + box.left = style.get("padding-x", box.left).asFloat(); + box.right = style.get("padding-x", box.right).asFloat(); + + box.top = style.get("padding-y", box.top).asFloat(); + box.bottom = style.get("padding-y", box.bottom).asFloat(); + + box.left = style.get("padding-left", box.left).asFloat(); + box.right = style.get("padding-right", box.right).asFloat(); + box.top = style.get("padding-top", box.top).asFloat(); + box.bottom = style.get("padding-bottom", box.bottom).asFloat(); + + return box; +} + +BoxSides Layout::get_margin(const style::ComputedStyle& style) { + BoxSides box = BoxSides(style.get("margin", 0).asFloat()); + + box.left = style.get("margin-x", box.left).asFloat(); + box.right = style.get("margin-x", box.right).asFloat(); + + box.top = style.get("margin-y", box.top).asFloat(); + box.bottom = style.get("margin-y", box.bottom).asFloat(); + + box.left = style.get("margin-left", box.left).asFloat(); + box.right = style.get("margin-right", box.right).asFloat(); + box.top = style.get("margin-top", box.top).asFloat(); + box.bottom = style.get("margin-bottom", box.bottom).asFloat(); + + return box; +} + +BoxSides Layout::get_border(const style::ComputedStyle& style) { + BoxSides box = BoxSides(style.get("border-width", 0).asFloat()); + + box.left = style.get("border-width-x", box.left).asFloat(); + box.right = style.get("border-width-x", box.right).asFloat(); + + box.top = style.get("border-width-y", box.top).asFloat(); + box.bottom = style.get("border-width-y", box.bottom).asFloat(); + + box.left = style.get("border-width-left", box.left).asFloat(); + box.right = style.get("border-width-right", box.right).asFloat(); + box.top = style.get("border-width-top", box.top).asFloat(); + box.bottom = style.get("border-width-bottom", box.bottom).asFloat(); + + return box; +} + +// ---------------------------------------------------------------------- +BoxSides Layout::get_child_margin(Node& child) { + if (auto* child_el = std::get_if(&child.node_type)) + return get_margin(child_el->style); + return BoxSides(0.0f); +} + +// Общая длина вдоль main-оси (включая margin) +float Layout::get_child_main_size( + const LayoutBox& cl, const BoxSides& cm, FlexDirection dir +) { + return (dir == FlexDirection::Column) ? cl.size.y + cm.vertical() + : cl.size.x + cm.horizontal(); +} + +// Общая ширина/высота вдоль cross-оси (включая margin) +float Layout::get_child_cross_size( + const LayoutBox& cl, const BoxSides& cm, FlexDirection dir +) { + return (dir == FlexDirection::Column) ? cl.size.x + cm.horizontal() + : cl.size.y + cm.vertical(); +} + +// Сдвиг по main-оси в зависимости от align и total_main +float Layout::initial_cursor( + float container_main, float total_main, const std::string& align +) { + if (align == "center") + return std::max(0.0f, (container_main - total_main) * 0.5f); + else if (align == "end") + return std::max(0.0f, container_main - total_main); + return 0.0f; // start +} + +// Смещение по cross-оси для ребёнка +float Layout::cross_offset( + float container_cross, float child_cross, const std::string& align +) { + if (align == "center") + return std::max(0.0f, (container_cross - child_cross) * 0.5f); + else if (align == "end") + return std::max(0.0f, container_cross - child_cross); + return 0.0f; +} +// ---------------------------------------------------------------------- \ No newline at end of file diff --git a/src/graphics/ui/layout/Layout.hpp b/src/graphics/ui/layout/Layout.hpp index 54192886b..64e085e24 100644 --- a/src/graphics/ui/layout/Layout.hpp +++ b/src/graphics/ui/layout/Layout.hpp @@ -2,24 +2,82 @@ #include "../elements/Node.hpp" #include "LayoutBox.hpp" -class Layout { -public: - glm::uvec2 size = glm::uvec2(0, 0); +enum class FlexDirection { Column, Row }; - // Основной метод вычисления layout - static void compute_layout(Node& node, LayoutContext& context, const Assets& assets); +struct BoxSides { + float left = 0; + float top = 0; + float right = 0; + float bottom = 0; - // Методы для разных типов layout - static void compute_text_layout( - Node& node, Text& text, LayoutContext& context, const Assets& assets - ); - static void compute_element_layout( - Node& node, Element& element, LayoutContext& context, const Assets& assets - ); + // Конструктор по всем четырем сторонам + BoxSides(float l, float t, float r, float b) + : left(l), top(t), right(r), bottom(b) { + } + + // Конструктор с одинаковым значением для всех сторон + explicit BoxSides(float all) + : left(all), top(all), right(all), bottom(all) { + } + + // Конструктор по осям (горизонталь, вертикаль) + BoxSides(float horizontal, float vertical) + : left(horizontal / 2), + right(horizontal / 2), + top(vertical / 2), + bottom(vertical / 2) { + } + + float horizontal() const { + return left + right; + } + float vertical() const { + return top + bottom; + } + + BoxSides merge(const BoxSides& other) const { + return BoxSides( + left + other.left, + top + other.top, + right + other.right, + bottom + other.bottom + ); + } +}; +class Layout { +public: + // Главный вызов + static void compute_layout( + Node& root, LayoutContext& ctx, const Assets& assets + ); private: - static void set_element_position(Node& node, LayoutContext& context); - static LayoutContext create_child_context( - Node& node, float content_width, float max_height + static void measure( + Node& node, const LayoutContext& ctx, const Assets& assets + ); + static void measure_text( + Node& node, Text& text, const LayoutContext& ctx, const Assets& assets ); + static void measure_element( + Node& node, + Element& element, + const LayoutContext& ctx, + const Assets& assets + ); + static void position_node(Node& node, const glm::vec2& offset); + static void position_text(Node& node, Text&, const glm::vec2& offset); + static void position_element( + Node& node, Element& element, const glm::vec2& offset + ); + + static FlexDirection get_direction(Element& element); + static BoxSides get_padding(const style::ComputedStyle& style); + static BoxSides get_margin(const style::ComputedStyle& style); + static BoxSides get_border(const style::ComputedStyle& style); + + static BoxSides get_child_margin(Node& child); + static float get_child_main_size(const LayoutBox& cl, const BoxSides& cm, FlexDirection dir); + static float get_child_cross_size(const LayoutBox& cl, const BoxSides& cm, FlexDirection dir); + static float initial_cursor(float container_main, float total_main, const std::string& align); + static float cross_offset(float container_cross, float child_cross, const std::string& align); }; \ No newline at end of file diff --git a/src/graphics/ui/layout/LayoutBox.hpp b/src/graphics/ui/layout/LayoutBox.hpp index a5a3689fc..9559adca7 100644 --- a/src/graphics/ui/layout/LayoutBox.hpp +++ b/src/graphics/ui/layout/LayoutBox.hpp @@ -4,15 +4,16 @@ #include "graphics/ui/style/Stylesheet.h" struct LayoutBox { - float x = 0; - float y = 0; - float width = 0; - float height = 0; - float font_size = 16; + glm::vec2 content_size = glm::vec2(0, 0); + glm::vec2 size = glm::vec2(0, 0); + glm::vec2 position = glm::vec2(0, 0); + + glm::vec2 padding = {0, 0}; // left+right, top+bottom + glm::vec2 margin = {0, 0}; // left+right, top+bottom + glm::vec2 border = {0, 0}; // left+right, top+bottom }; struct LayoutContext { const LayoutBox* parent_layout = nullptr; - // const LayoutBox* current_layout = nullptr; - glm::uvec2 available_space; + glm::vec2 available_space; }; \ No newline at end of file diff --git a/src/graphics/ui/style/value.cpp b/src/graphics/ui/style/value.cpp index 961161fc1..b042cad95 100644 --- a/src/graphics/ui/style/value.cpp +++ b/src/graphics/ui/style/value.cpp @@ -138,7 +138,7 @@ namespace style { return def; } - double value::asFloat(double def) const { + double value::asDouble(double def) const { if (isFloat()) { return std::get(data); } diff --git a/src/graphics/ui/style/value.h b/src/graphics/ui/style/value.h index f0618c9bc..b33860b09 100644 --- a/src/graphics/ui/style/value.h +++ b/src/graphics/ui/style/value.h @@ -80,7 +80,7 @@ namespace style { Type getType() const; int64_t asInt(int64_t def = 0) const; - double asFloat(double def = 0.0) const; + double asDouble(double def = 0.0) const; float asFloat(float def = 0.0f) const; bool asBool(bool def = false) const; std::string asString(const std::string& def = "") const;