diff --git a/res/layouts/console.xml b/res/layouts/console.xml index 3f7331ee..2096fb68 100644 --- a/res/layouts/console.xml +++ b/res/layouts/console.xml @@ -22,6 +22,7 @@ multiline='true' size-func="gui.get_viewport()[1],40" gravity="bottom-left" + markdown="true" > keywords { "and", "break", "do", "else", "elseif", "end", "false", "for", "function", diff --git a/src/coders/lua_parsing.hpp b/src/coders/lua_parsing.hpp index 1578b7fa..0054e4d0 100644 --- a/src/coders/lua_parsing.hpp +++ b/src/coders/lua_parsing.hpp @@ -3,33 +3,12 @@ #include #include +#include "devtools/syntax.hpp" + namespace lua { - struct Location { - int pos; - int lineStart; - int line; - }; - - enum class TokenTag { - KEYWORD, NAME, INTEGER, NUMBER, OPEN_BRACKET, CLOSE_BRACKET, STRING, - OPERATOR, COMMA, SEMICOLON, UNEXPECTED, COMMENT - }; - - struct Token { - TokenTag tag; - std::string text; - Location start; - Location end; - - Token(TokenTag tag, std::string text, Location start, Location end) - : tag(tag), - text(std::move(text)), - start(std::move(start)), - end(std::move(end)) { - } - }; - bool is_lua_keyword(std::string_view view); - std::vector tokenize(std::string_view file, std::string_view source); + std::vector tokenize( + std::string_view file, std::string_view source + ); } diff --git a/src/devtools/syntax.hpp b/src/devtools/syntax.hpp new file mode 100644 index 00000000..6c7d4b15 --- /dev/null +++ b/src/devtools/syntax.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include + +namespace devtools { + struct Location { + int pos; + int lineStart; + int line; + }; + + enum class TokenTag { + KEYWORD, NAME, INTEGER, NUMBER, OPEN_BRACKET, CLOSE_BRACKET, STRING, + OPERATOR, COMMA, SEMICOLON, UNEXPECTED, COMMENT + }; + + struct Token { + TokenTag tag; + std::string text; + Location start; + Location end; + + Token(TokenTag tag, std::string text, Location start, Location end) + : tag(tag), + text(std::move(text)), + start(std::move(start)), + end(std::move(end)) { + } + }; +} diff --git a/src/devtools/syntax_highlighting.cpp b/src/devtools/syntax_highlighting.cpp index 8442229e..f68780df 100644 --- a/src/devtools/syntax_highlighting.cpp +++ b/src/devtools/syntax_highlighting.cpp @@ -7,28 +7,28 @@ using namespace devtools; static std::unique_ptr build_styles( - const std::vector& tokens + const std::vector& tokens ) { + using devtools::TokenTag; FontStylesScheme styles { { - {false, false, glm::vec4(0.8f, 0.8f, 0.8f, 1)}, // default - {true, false, glm::vec4(0.9, 0.6f, 0.4f, 1)}, // keyword - {false, false, glm::vec4(0.4, 0.8f, 0.5f, 1)}, // string - {false, false, glm::vec4(0.3, 0.3f, 0.3f, 1)}, // comment - {false, false, glm::vec4(0.4, 0.45f, 0.5f, 1)}, // self - {true, false, glm::vec4(1.0f, 0.2f, 0.1f, 1)}, // unexpected + {false, false, false, false, glm::vec4(0.8f, 0.8f, 0.8f, 1)}, // default + {true, false, false, false, glm::vec4(0.9, 0.6f, 0.4f, 1)}, // keyword + {false, false, false, false, glm::vec4(0.4, 0.8f, 0.5f, 1)}, // string + {false, false, false, false, glm::vec4(0.3, 0.3f, 0.3f, 1)}, // comment + {true, false, false, false, glm::vec4(1.0f, 0.2f, 0.1f, 1)}, // unexpected }, {} }; size_t offset = 0; for (int i = 0; i < tokens.size(); i++) { const auto& token = tokens.at(i); - if (token.tag != lua::TokenTag::KEYWORD && - token.tag != lua::TokenTag::STRING && - token.tag != lua::TokenTag::INTEGER && - token.tag != lua::TokenTag::NUMBER && - token.tag != lua::TokenTag::COMMENT && - token.tag != lua::TokenTag::UNEXPECTED) { + if (token.tag != TokenTag::KEYWORD && + token.tag != TokenTag::STRING && + token.tag != TokenTag::INTEGER && + token.tag != TokenTag::NUMBER && + token.tag != TokenTag::COMMENT && + token.tag != TokenTag::UNEXPECTED) { continue; } if (token.start.pos > offset) { @@ -38,12 +38,12 @@ static std::unique_ptr build_styles( offset = token.end.pos; int styleIndex; switch (token.tag) { - case lua::TokenTag::KEYWORD: styleIndex = 1; break; - case lua::TokenTag::STRING: - case lua::TokenTag::INTEGER: - case lua::TokenTag::NUMBER: styleIndex = 2; break; - case lua::TokenTag::COMMENT: styleIndex = 3; break; - case lua::TokenTag::UNEXPECTED: styleIndex = 5; break; + case TokenTag::KEYWORD: styleIndex = SyntaxStyles::KEYWORD; break; + case TokenTag::STRING: + case TokenTag::INTEGER: + case TokenTag::NUMBER: styleIndex = SyntaxStyles::LITERAL; break; + case TokenTag::COMMENT: styleIndex = SyntaxStyles::COMMENT; break; + case TokenTag::UNEXPECTED: styleIndex = SyntaxStyles::ERROR; break; default: styleIndex = 0; break; diff --git a/src/devtools/syntax_highlighting.hpp b/src/devtools/syntax_highlighting.hpp index c2f33e45..18db6389 100644 --- a/src/devtools/syntax_highlighting.hpp +++ b/src/devtools/syntax_highlighting.hpp @@ -6,6 +6,10 @@ struct FontStylesScheme; namespace devtools { + enum SyntaxStyles { + DEFAULT, KEYWORD, LITERAL, COMMENT, ERROR + }; + std::unique_ptr syntax_highlight( const std::string& lang, std::string_view source ); diff --git a/src/graphics/core/Font.cpp b/src/graphics/core/Font.cpp index d3a6caf6..2116fdd3 100644 --- a/src/graphics/core/Font.cpp +++ b/src/graphics/core/Font.cpp @@ -101,7 +101,7 @@ static inline void draw_text( const glm::vec3& pos, const glm::vec3& right, const glm::vec3& up, - float glyphInterval, + float interval, const FontStylesScheme* styles, size_t styleMapOffset ) { @@ -115,6 +115,7 @@ static inline void draw_text( uint next = MAX_CODEPAGES; int x = 0; int y = 0; + bool hasLines = false; do { for (size_t i = 0; i < text.length(); i++) { @@ -123,6 +124,9 @@ static inline void draw_text( std::min(styles->map.size() - 1, i + styleMapOffset) ); const FontStyle& style = styles->palette.at(styleIndex); + hasLines |= style.strikethrough; + hasLines |= style.underline; + if (!font.isPrintableChar(c)) { x++; continue; @@ -131,14 +135,7 @@ static inline void draw_text( if (charpage == page){ batch.texture(font.getPage(charpage)); draw_glyph( - batch, - pos, - glm::vec2(x, y), - c, - right, - up, - glyphInterval, - style + batch, pos, glm::vec2(x, y), c, right, up, interval, style ); } else if (charpage > page && charpage < next){ @@ -150,6 +147,31 @@ static inline void draw_text( next = MAX_CODEPAGES; x = 0; } while (page < MAX_CODEPAGES); + + if (!hasLines) { + return; + } + batch.texture(font.getPage(0)); + for (size_t i = 0; i < text.length(); i++) { + uint c = text[i]; + size_t styleIndex = styles->map.at( + std::min(styles->map.size() - 1, i + styleMapOffset) + ); + const FontStyle& style = styles->palette.at(styleIndex); + FontStyle lineStyle = style; + lineStyle.bold = true; + if (style.strikethrough) { + draw_glyph( + batch, pos, glm::vec2(x, y), '-', right, up, interval, lineStyle + ); + } + if (style.underline) { + draw_glyph( + batch, pos, glm::vec2(x, y), '_', right, up, interval, lineStyle + ); + } + x++; + } } const Texture* Font::getPage(int charpage) const { diff --git a/src/graphics/core/Font.hpp b/src/graphics/core/Font.hpp index 6721be98..07fe0532 100644 --- a/src/graphics/core/Font.hpp +++ b/src/graphics/core/Font.hpp @@ -14,7 +14,25 @@ class Camera; struct FontStyle { bool bold = false; bool italic = false; + bool strikethrough = false; + bool underline = false; glm::vec4 color {1, 1, 1, 1}; + + FontStyle() = default; + + FontStyle( + bool bold, + bool italic, + bool strikethrough, + bool underline, + glm::vec4 color + ) + : bold(bold), + italic(italic), + strikethrough(strikethrough), + underline(underline), + color(std::move(color)) { + } }; struct FontStylesScheme { diff --git a/src/graphics/ui/elements/Label.cpp b/src/graphics/ui/elements/Label.cpp index 38cdc7c8..6a49dff7 100644 --- a/src/graphics/ui/elements/Label.cpp +++ b/src/graphics/ui/elements/Label.cpp @@ -6,6 +6,7 @@ #include "graphics/core/Font.hpp" #include "assets/Assets.hpp" #include "util/stringutil.hpp" +#include "../markdown.hpp" using namespace gui; @@ -80,11 +81,16 @@ glm::vec2 Label::calcSize() { ); } -void Label::setText(const std::wstring& text) { +void Label::setText(std::wstring text) { + if (isMarkdown()) { + auto [processedText, styles] = markdown::process(text, true); + text = std::move(processedText); + setStyles(std::move(styles)); + } if (text == this->text && !cache.resetFlag) { return; } - this->text = text; + this->text = std::move(text); cache.update(this->text, multiline, textWrap); if (cache.font && autoresize) { @@ -242,6 +248,15 @@ bool Label::isTextWrapping() const { return textWrap; } +void Label::setMarkdown(bool flag) { + markdown = flag; + setText(text); +} + +bool Label::isMarkdown() const { + return markdown; +} + void Label::setStyles(std::unique_ptr styles) { this->styles = std::move(styles); } diff --git a/src/graphics/ui/elements/Label.hpp b/src/graphics/ui/elements/Label.hpp index ce8a55bc..669dbd96 100644 --- a/src/graphics/ui/elements/Label.hpp +++ b/src/graphics/ui/elements/Label.hpp @@ -53,6 +53,9 @@ namespace gui { /// @brief Auto resize label to fit text bool autoresize = false; + /// @brief Enable text markdown + bool markdown = false; + std::unique_ptr styles; public: Label(const std::string& text, std::string fontName="normal"); @@ -60,7 +63,7 @@ namespace gui { virtual ~Label(); - virtual void setText(const std::wstring& text); + virtual void setText(std::wstring text); const std::wstring& getText() const; virtual void setFontName(std::string name); @@ -113,6 +116,9 @@ namespace gui { virtual void setTextWrapping(bool flag); virtual bool isTextWrapping() const; + virtual void setMarkdown(bool flag); + virtual bool isMarkdown() const; + virtual void setStyles(std::unique_ptr styles); }; } diff --git a/src/graphics/ui/elements/TextBox.cpp b/src/graphics/ui/elements/TextBox.cpp index 198b992e..ef528ec8 100644 --- a/src/graphics/ui/elements/TextBox.cpp +++ b/src/graphics/ui/elements/TextBox.cpp @@ -13,6 +13,7 @@ #include "util/stringutil.hpp" #include "window/Events.hpp" #include "window/Window.hpp" +#include "../markdown.hpp" using namespace gui; @@ -191,7 +192,18 @@ void TextBox::drawBackground(const DrawContext& pctx, const Assets&) { void TextBox::refreshLabel() { label->setColor(textColor * glm::vec4(input.empty() ? 0.5f : 1.0f)); - label->setText(input.empty() && !hint.empty() ? hint : getText()); + + const auto& displayText = input.empty() && !hint.empty() ? hint : getText(); + if (markdown) { + auto [processedText, styles] = markdown::process(displayText, !focused); + label->setText(std::move(processedText)); + label->setStyles(std::move(styles)); + } else { + label->setText(displayText); + if (syntax.empty()) { + label->setStyles(nullptr); + } + } if (showLineNumbers) { if (lineNumbersLabel->getLinesNumber() != label->getLinesNumber()) { @@ -846,3 +858,11 @@ void TextBox::setSyntax(const std::string& lang) { refreshSyntax(); } } + +void TextBox::setMarkdown(bool flag) { + markdown = flag; +} + +bool TextBox::isMarkdown() const { + return markdown; +} diff --git a/src/graphics/ui/elements/TextBox.hpp b/src/graphics/ui/elements/TextBox.hpp index 522c9085..95c3cef8 100644 --- a/src/graphics/ui/elements/TextBox.hpp +++ b/src/graphics/ui/elements/TextBox.hpp @@ -56,6 +56,7 @@ namespace gui { bool editable = true; bool autoresize = false; bool showLineNumbers = false; + bool markdown = false; std::string syntax; @@ -227,5 +228,8 @@ namespace gui { virtual void setOnDownPressed(const runnable &callback); virtual void setSyntax(const std::string& lang); + + virtual void setMarkdown(bool flag); + virtual bool isMarkdown() const; }; } diff --git a/src/graphics/ui/gui_xml.cpp b/src/graphics/ui/gui_xml.cpp index c24d89dd..f26b62f4 100644 --- a/src/graphics/ui/gui_xml.cpp +++ b/src/graphics/ui/gui_xml.cpp @@ -278,6 +278,9 @@ static std::shared_ptr readLabel( if (element.has("text-wrap")) { label->setTextWrapping(element.attr("text-wrap").asBool()); } + if (element.has("markdown")) { + label->setMarkdown(element.attr("markdown").asBool()); + } return label; } @@ -381,6 +384,9 @@ static std::shared_ptr readTextBox(UiXmlReader& reader, const xml::xmlel if (element.has("line-numbers")) { textbox->setShowLineNumbers(element.attr("line-numbers").asBool()); } + if (element.has("markdown")) { + textbox->setMarkdown(element.attr("markdown").asBool()); + } if (element.has("consumer")) { textbox->setTextConsumer(scripting::create_wstring_consumer( reader.getEnvironment(), diff --git a/src/graphics/ui/markdown.cpp b/src/graphics/ui/markdown.cpp new file mode 100644 index 00000000..d65f3910 --- /dev/null +++ b/src/graphics/ui/markdown.cpp @@ -0,0 +1,109 @@ +#include "markdown.hpp" + +#include + +#include "graphics/core/Font.hpp" + +using namespace markdown; + +template +static inline void emit( + CharT c, FontStylesScheme& styles, std::basic_stringstream& ss +) { + ss << c; + styles.map.emplace_back(styles.palette.size()-1); +} + +template +static inline void emit_md( + CharT c, FontStylesScheme& styles, std::basic_stringstream& ss +) { + ss << c; + styles.map.emplace_back(0); +} + +template +static inline void restyle( + CharT c, + FontStyle& style, + FontStylesScheme& styles, + std::basic_stringstream& ss, + int& pos, + bool eraseMarkdown +) { + styles.palette.push_back(style); + if (!eraseMarkdown) { + emit_md(c, styles, ss); + } + pos++; +} + +template +Result process_markdown( + std::basic_string_view source, bool eraseMarkdown +) { + std::basic_stringstream ss; + FontStylesScheme styles { + // markdown default + {{false, false, false, false, glm::vec4(1,1,1,0.5f)}, {}}, + {} + }; + FontStyle style; + int pos = 0; + while (pos < source.size()) { + CharT first = source[pos]; + if (first == '\\') { + if (pos + 1 < source.size()) { + CharT second = source[++pos]; + switch (second) { + case '*': + case '_': + case '~': + if (!eraseMarkdown) { + emit_md(first, styles, ss); + } + emit(second, styles, ss); + pos++; + continue; + } + } + } else if (first == '*') { + if (pos + 1 < source.size() && source[pos+1] == '*') { + pos++; + if (!eraseMarkdown) + emit_md(first, styles, ss); + style.bold = !style.bold; + restyle(first, style, styles, ss, pos, eraseMarkdown); + continue; + } + style.italic = !style.italic; + restyle(first, style, styles, ss, pos, eraseMarkdown); + continue; + } else if (first == '_' && pos + 1 < source.size() && source[pos+1] == '_') { + pos++; + if (!eraseMarkdown) + emit_md(first, styles, ss); + style.underline = !style.underline; + restyle(first, style, styles, ss, pos, eraseMarkdown); + continue; + } else if (first == '~' && pos + 1 < source.size() && source[pos+1] == '~') { + pos++; + if (!eraseMarkdown) + emit_md(first, styles, ss); + style.strikethrough = !style.strikethrough; + restyle(first, style, styles, ss, pos, eraseMarkdown); + continue; + } + emit(first, styles, ss); + pos++; + } + return {ss.str(), std::make_unique(std::move(styles))}; +} + +Result markdown::process(std::string_view source, bool eraseMarkdown) { + return process_markdown(source, eraseMarkdown); +} + +Result markdown::process(std::wstring_view source, bool eraseMarkdown) { + return process_markdown(source, eraseMarkdown); +} diff --git a/src/graphics/ui/markdown.hpp b/src/graphics/ui/markdown.hpp new file mode 100644 index 00000000..625dbe84 --- /dev/null +++ b/src/graphics/ui/markdown.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include +#include + +struct FontStylesScheme; + +// VoxelCore Markdown dialect + +namespace markdown { + + template + struct Result { + /// @brief Text with erased markdown + std::basic_string text; + /// @brief Text styles scheme + std::unique_ptr styles; + }; + + Result process(std::string_view source, bool eraseMarkdown); + Result process(std::wstring_view source, bool eraseMarkdown); +}