Files
MagicSetEditor2/src/render/text/element.cpp
T

326 lines
14 KiB
C++

//+----------------------------------------------------------------------------+
//| Description: Magic Set Editor - Program to make Magic (tm) cards |
//| Copyright: (C) Twan van Laarhoven and the other MSE developers |
//| License: GNU General Public License 2 or later (see file COPYING) |
//+----------------------------------------------------------------------------+
// ----------------------------------------------------------------------------- : Includes
#include <util/prec.hpp>
#include <render/text/element.hpp>
#include <util/tagged_string.hpp>
#include <data/field/text.hpp>
#include <gfx/color.hpp>
DECLARE_POINTER_TYPE(FontTextElement);
// ----------------------------------------------------------------------------- : TextElements
// Colors for <atom-param> tags
Color param_colors[] =
{ Color(0,170,0)
, Color(0,0,200)
, Color(200,0,100)
, Color(200,200,0)
, Color(0,170,170)
, Color(200,0,0)
};
const size_t param_colors_count = sizeof(param_colors) / sizeof(param_colors[0]);
struct Margins {
double left, right, top;
};
// Helper class for TextElements::fromString, to allow persistent formating state accross recusive calls
struct TextElementsFromString {
// What formatting is enabled?
int bold = 0, italic = 0, underline = 0, symbol = 0;
int soft = 0, kwpph = 0, param = 0, line = 0, soft_line = 0;
int code = 0, code_kw = 0, code_string = 0, param_ref = 0;
int param_id = 0, li = 0;
vector<Color> colors;
vector<double> sizes;
vector<String> fonts;
vector<Margins> margins;
vector<Alignment> aligns;
const TextStyle& style;
Context& ctx;
vector<TextParagraph>& paragraphs;
TextElementsFromString(TextElements& out, const String& text, const TextStyle& style, Context& ctx)
: style(style), ctx(ctx), paragraphs(out.paragraphs)
{
out.start = 0;
out.end = text.size();
paragraphs.emplace_back();
paragraphs.back().start = 0;
fromString(out.children, text, 0, text.size());
paragraphs.back().end = text.size();
}
private:
// read TextElements from a string
void fromString(vector<TextElementP>& elements, const String& text, size_t start, size_t end) {
size_t text_start = start;
// for each character...
for (size_t pos = start ; pos < end ; ) {
Char c = text.GetChar(pos);
if (c == _('<')) {
if (text_start < pos) {
// text element before this tag?
addText(elements, text, text_start, pos);
addParagraphs(text, text_start, pos);
}
// a (formatting) tag
size_t tag_start = pos;
pos = skip_tag(text, tag_start);
if (is_tag(text, tag_start, _( "<b"))) bold += 1;
else if (is_tag(text, tag_start, _("</b"))) bold -= 1;
else if (is_tag(text, tag_start, _( "<i"))) italic += 1;
else if (is_tag(text, tag_start, _("</i"))) italic -= 1;
else if (is_tag(text, tag_start, _("<u"))) underline += 1;
else if (is_tag(text, tag_start, _("</u"))) underline -= 1;
else if (is_tag(text, tag_start, _( "<sym"))) symbol += 1;
else if (is_tag(text, tag_start, _("</sym"))) symbol -= 1;
else if (is_tag(text, tag_start, _( "<line"))) line += 1;
else if (is_tag(text, tag_start, _("</line"))) line -= 1;
else if (is_tag(text, tag_start, _( "<soft-line"))) soft_line += 1;
else if (is_tag(text, tag_start, _("</soft-line"))) soft_line -= 1;
else if (is_tag(text, tag_start, _( "<sep-soft"))) soft += 1;
else if (is_tag(text, tag_start, _("</sep-soft"))) soft -= 1;
else if (is_tag(text, tag_start, _( "<soft"))) soft += 1; // must be after <soft-line
else if (is_tag(text, tag_start, _("</soft"))) soft -= 1;
else if (is_tag(text, tag_start, _( "<li"))) li += 1;
else if (is_tag(text, tag_start, _("</li"))) li -= 1;
else if (is_tag(text, tag_start, _( "<atom-kwpph"))) kwpph += 1;
else if (is_tag(text, tag_start, _("</atom-kwpph"))) kwpph -= 1;
else if (is_tag(text, tag_start, _( "<code-kw"))) code_kw += 1;
else if (is_tag(text, tag_start, _("</code-kw"))) code_kw -= 1;
else if (is_tag(text, tag_start, _( "<code-str"))) code_string += 1;
else if (is_tag(text, tag_start, _("</code-str"))) code_string -= 1;
else if (is_tag(text, tag_start, _( "<code"))) code += 1;
else if (is_tag(text, tag_start, _("</code"))) code -= 1;
else if (is_tag(text, tag_start, _( "<color"))) {
size_t colon = text.find_first_of(_(">:"), tag_start);
if (colon < pos - 1 && text.GetChar(colon) == _(':')) {
auto c = parse_color(text.substr(colon+1, pos-colon-2));
if (c) {
colors.push_back(*c);
} else {
queue_message(MESSAGE_WARNING, _("Invalid color in tagged string: ") + text.substr(colon + 1, pos - colon - 2));
colors.push_back(style.font.color);
}
}
} else if (is_tag(text, tag_start, _("</color"))) {
if (!colors.empty()) colors.pop_back();
}
else if (is_tag(text, tag_start, _( "<font"))) {
size_t colon = text.find_first_of(_(">:"), tag_start);
if (colon < pos - 1 && text.GetChar(colon) == _(':')) {
fonts.push_back(text.substr(colon+1, pos-colon-2));
}
} else if (is_tag(text, tag_start, _("</font"))) {
if (!fonts.empty()) fonts.pop_back();
}
else if (is_tag(text, tag_start, _( "<size"))) {
size_t colon = text.find_first_of(_(">:"), tag_start);
if (colon < pos - 1 && text.GetChar(colon) == _(':')) {
double size = style.font.size;
String v = text.substr(colon+1, pos-colon-2);
v.ToDouble(&size);
sizes.push_back(size);
}
} else if (is_tag(text, tag_start, _("</size"))) {
if (!sizes.empty()) sizes.pop_back();
}
else if (is_tag(text, tag_start, _( "<ref-param"))) {
// determine the param being referenced
// from a tag <ref-param123>
if (pos != String::npos) {
String ref = text.substr(tag_start + 10, pos - tag_start - 11);
long ref_n;
if (ref.ToLong(&ref_n)) {
param_id = (ref_n - 1)%param_colors_count + param_colors_count;
}
}
param_ref += 1;
}
else if (is_tag(text, tag_start, _("</ref-param"))) param_ref -= 1;
else if (is_tag(text, tag_start, _( "<atom-param"))) param += 1;
else if (is_tag(text, tag_start, _("</atom-param"))) param -= 1;
else if (is_tag(text, tag_start, _("<atom"))) {
// 'atomic' indicator
#if 0
// it would be nice if we could have semi-transparent brushes
Color color = style.font.color; color.a /= 5;
#else
Color fg = style.font.color;
Color color = fg.r+fg.g+fg.b < 255*2 ? Color(210,210,210) : Color(60,60,60);
#endif
size_t end_tag = min(end, match_close_tag(text, tag_start));
intrusive_ptr<AtomTextElement> e = make_intrusive<AtomTextElement>(pos, end_tag, color);
fromString(e->children, text, pos, end_tag);
elements.push_back(e);
pos = skip_tag(text, end_tag);
} else if (is_tag(text, tag_start, _( "<error"))) {
// error indicator
size_t end_tag = min(end, match_close_tag(text, tag_start));
intrusive_ptr<ErrorTextElement> e = make_intrusive<ErrorTextElement>(pos, end_tag);
fromString(e->children, text, pos, end_tag);
elements.push_back(e);
pos = skip_tag(text, end_tag);
} else if (is_tag(text, tag_start, _("</bullet"))) {
// end of bullet point, set margin here
if (li <= 0) {
queue_message(MESSAGE_WARNING, _("<bullet> outside <li> tag"));
}
paragraphs.back().margin_end_char = pos;
} else if (is_tag(text, tag_start, _("<margin"))) {
size_t colon = text.find_first_of(_(">:"), tag_start);
if (colon < pos - 1 && text.GetChar(colon) == _(':')) {
size_t colon2 = text.find_first_of(_(">:"), colon + 1);
size_t colon3 = colon2 < pos-1 ? text.find_first_of(_(">:"), colon2 + 1) : colon2;
Margins m = {0.,0.,0.};
text.substr(colon + 1, colon2 - colon - 2).ToDouble(&m.left);
text.substr(colon2 + 1, colon3 - colon2 - 2).ToDouble(&m.right);
text.substr(colon3 + 1, pos - colon3 - 2).ToDouble(&m.top);
if (!margins.empty()) {
m.left += margins.back().left;
m.right += margins.back().right;
m.top += margins.back().top;
}
margins.emplace_back(m);
paragraphs.back().margin_left = m.left;
paragraphs.back().margin_right = m.right;
paragraphs.back().margin_top = m.top;
}
} else if (is_tag(text, tag_start, _("</margin"))) {
if (!margins.empty()) margins.pop_back();
} else if (is_tag(text, tag_start, _("<align"))) {
size_t colon = text.find_first_of(_(">:"), tag_start);
if (colon < pos - 1 && text.GetChar(colon) == _(':')) {
Alignment align = alignment_from_string(text.substr(colon+1, pos-colon-2));
aligns.push_back(align);
paragraphs.back().alignment = align;
}
} else if (is_tag(text, tag_start, _("</align"))) {
if (!aligns.empty()) aligns.pop_back();
} else {
// ignore other tags
}
// text starts again after the tag
text_start = pos;
} else {
// normal text
pos += 1;
}
}
if (text_start < end) {
// remaining text at the end
addText(elements, text, text_start, end);
addParagraphs(text, text_start, end);
}
}
private:
/// Create a text element for a piece of text, text[start..end)
void addText(vector<TextElementP>& elements, const String& text, size_t start, size_t end) {
String content = untag(text.substr(start, end - start));
assert(content.size() == end-start);
// use symbol font?
if (symbol > 0 && style.symbol_font.valid()) {
elements.push_back(make_intrusive<SymbolTextElement>(content, start, end, style.symbol_font, &ctx));
} else {
// text, possibly mixed with symbols
DrawWhat what = soft > 0 ? DRAW_ACTIVE : DRAW_NORMAL;
LineBreak line_break = line > 0 ? LineBreak::LINE :
soft_line > 0 ? LineBreak::SOFT : LineBreak::HARD;
if (kwpph > 0 || param > 0) {
// bracket the text
content = String(LEFT_ANGLE_BRACKET) + content + RIGHT_ANGLE_BRACKET;
start -= 1;
end += 1;
}
// render as symbols or as text?
if (style.always_symbol && style.symbol_font.valid()) {
// mixed symbols/text, autodetected by symbol font
size_t text_pos = 0;
size_t pos = 0;
FontP font;
while (pos < end-start) {
if (size_t n = style.symbol_font.font->recognizePrefix(content,pos)) {
// at 'pos' there are n symbol font characters
if (text_pos < pos) {
// text before it?
if (!font) font = makeFont(style);
elements.push_back(make_intrusive<FontTextElement>(content.substr(text_pos, pos-text_pos), start+text_pos, start+pos, font, what, line_break));
}
elements.push_back(make_intrusive<SymbolTextElement>(content.substr(pos,n), start+pos, start+pos+n, style.symbol_font, &ctx));
text_pos = pos += n;
} else {
++pos;
}
}
if (text_pos < pos) {
if (!font) font = makeFont(style);
elements.push_back(make_intrusive<FontTextElement>(content.substr(text_pos), start+text_pos, end, font, what, line_break));
}
} else {
elements.push_back(make_intrusive<FontTextElement>(content, start, end, makeFont(style), what, line_break));
}
}
}
// Find paragraph breaks in text
void addParagraphs(const String& text, size_t start, size_t end) {
if (line == 0 && soft_line > 0) return;
for (size_t i = start; i < end; ++i) {
wxUniChar c = text.GetChar(i);
if (c == '\n') {
paragraphs.back().end = i + 1;
paragraphs.emplace_back();
paragraphs.back().start = i + 1;
paragraphs.back().margin_end_char = i + 1;
if (!margins.empty()) {
paragraphs.back().margin_left = margins.back().left;
paragraphs.back().margin_right = margins.back().right;
paragraphs.back().margin_top = margins.back().top;
}
if (!aligns.empty()) {
paragraphs.back().alignment = aligns.back();
}
}
}
}
FontP makeFont(const TextStyle& style) {
return style.font.make(
(bold > 0 ? FONT_BOLD : FONT_NORMAL) |
(italic > 0 ? FONT_ITALIC : FONT_NORMAL) |
(soft > 0 ? FONT_SOFT : FONT_NORMAL) |
(kwpph > 0 ? FONT_SOFT : FONT_NORMAL) |
(code > 0 ? FONT_CODE : FONT_NORMAL) |
(code_kw > 0 ? FONT_CODE_KW : FONT_NORMAL) |
(code_string > 0 ? FONT_CODE_STRING : FONT_NORMAL),
underline > 0,
fonts.empty() ? nullptr : &fonts.back(),
param > 0 || param_ref > 0
? &param_colors[(param_id++) % param_colors_count]
: !colors.empty()
? &colors.back()
: nullptr,
!sizes.empty() ? &sizes.back() : nullptr
);
}
};
void TextElements::clear() {
children.clear();
paragraphs.clear();
}
void TextElements::fromString(const String& text, const TextStyle& style, Context& ctx) {
clear();
TextElementsFromString f(*this, text, style, ctx);
}