From a01c58094f85fcc320cfe9ba38d2a240db471dae Mon Sep 17 00:00:00 2001 From: skyjake Date: Thu, 23 May 2013 15:28:33 +0300 Subject: [PATCH] libgui|Font: Rich formatting of text The format escape sequences in styled text can be interpreted using RichFormat. RichFormat can then be given to Font, FontLineWrapping, and GLTextComposer to modify the appearance of the text. --- doomsday/libgui/include/de/gui/font.h | 135 ++++++++- doomsday/libgui/src/font.cpp | 414 +++++++++++++++++++++++++- 2 files changed, 536 insertions(+), 13 deletions(-) diff --git a/doomsday/libgui/include/de/gui/font.h b/doomsday/libgui/include/de/gui/font.h index 2916f1e157..5fd64510c4 100644 --- a/doomsday/libgui/include/de/gui/font.h +++ b/doomsday/libgui/include/de/gui/font.h @@ -24,9 +24,11 @@ #include #include #include +#include #include #include +#include #include "libgui.h" @@ -37,6 +39,121 @@ namespace de { */ class LIBGUI_PUBLIC Font { +public: + /** + * Rich formatting instructions for a string of text. + * + * The formatting instructions are composed of a sequence of ranges that + * specify various modifications to the original font. It is important to + * note that a RichFormat instance always needs to be set up for a specific + * source text string. Also, RichFormat is out-of-band data: when operating + * on a piece of rich text (using the methods of Font), the formatting is + * always provided in parallel to the plain version of the text. + * + * Use RichFormat::fromPlainText() to set up a RichFormat for a string of + * plain text. + * + * Use RichFormat::initFromStyledText() to set up a RichFormat for a string + * of text that contains style information (as escape sequences that start + * with the Esc ASCII code 0x1b). + */ + class RichFormat + { + public: + RichFormat(); + RichFormat(RichFormat const &other); + + /** + * Constructs a RichFormat that specifies no formatting instructions. + * + * @param plainText Plain text. + * + * @return RichFormat instance with a single range that uses the + * default formatting. + */ + static RichFormat fromPlainText(String const &plainText); + + /** + * Initializes this RichFormat instance with the styles found in the + * provided styled text (using escape sequences). + * + * @param styledText Text with style markup. + * + * @return Corresponding plain text for use with the methods of Font. + */ + String initFromStyledText(String const &styledText); + + enum Weight { + OriginalWeight = -1, + Normal = 0, + Light = 1, + Bold = 2 + }; + enum Style { + OriginalStyle = -1, + Regular = 0, + Italic = 1 + }; + enum Color + { + OriginalColor = -1, + NormalColor = 0, + HighlightColor = 1, + DimmedColor = 2, + AccentColor = 3 + }; + + /** + * Clips this RichFormat so that it covers only the specified range. + * The indices are translated to be relative to the provided range. + * + * @param range Target range for clipping. + * + * @return RichFormat with only those ranges covering @a range. + */ + RichFormat subRange(Rangei const &range) const; + + /** + * Iterates the rich format ranges of a RichFormat. + * + * @note Iterator::next() must be called before at least once after + * constructing the instance to move the iterator onto the first range. + */ + struct Iterator + { + RichFormat const &format; + int index; + + Iterator(RichFormat const &f); + bool hasNext() const; + void next(); + + bool isOriginal() const; + Rangei range() const; + float sizeFactor() const; + Weight weight() const; + Style style() const; + int color() const; + bool markIndent() const; + }; + + private: + struct FormatRange + { + Rangei range; + float sizeFactor; + Weight weight; + Style style; + int colorIndex; + bool markIndent; + + FormatRange() : sizeFactor(1.f), weight(OriginalWeight), + style(OriginalStyle), colorIndex(-1), markIndent(false) {} + }; + typedef QList Ranges; + Ranges _ranges; + }; + public: Font(); @@ -47,10 +164,9 @@ class LIBGUI_PUBLIC Font QFont toQFont() const; /** - * Determines the size of the given line of text (as in how large an area - * is covered by the glyphs). (0,0) is the corner of is at the baseline, - * left edge of the line. The rectangle may extend into negative - * coordinates. + * Determines the size of the given line of text, i.e., how large an area + * is covered by the glyphs. (0,0) is at the baseline, left edge of the + * line. The rectangle may extend into negative coordinates. * * @param textLine Text to measure. * @@ -58,6 +174,8 @@ class LIBGUI_PUBLIC Font */ Rectanglei measure(String const &textLine) const; + Rectanglei measure(String const &textLine, RichFormat const &format) const; + /** * Returns the advance width of a line of text. This is not the same as * the width of the rectangle returned by measure(). @@ -69,6 +187,8 @@ class LIBGUI_PUBLIC Font */ int advanceWidth(String const &textLine) const; + int advanceWidth(String const &textLine, RichFormat const &format) const; + /** * Rasterizes a line of text onto a 32-bit RGBA image. * @@ -76,12 +196,17 @@ class LIBGUI_PUBLIC Font * @param foreground Text foreground color. * @param background Background color. * - * @return + * @return Image containing the rasterized text. */ QImage rasterize(String const &textLine, Vector4ub const &foreground = Vector4ub(255, 255, 255, 255), Vector4ub const &background = Vector4ub(255, 255, 255, 0)) const; + QImage rasterize(String const &textLine, + RichFormat const &format, + Vector4ub const &foreground = Vector4ub(255, 255, 255, 255), + Vector4ub const &background = Vector4ub(255, 255, 255, 0)) const; + Rule const &height() const; Rule const &ascent() const; Rule const &descent() const; diff --git a/doomsday/libgui/src/font.cpp b/doomsday/libgui/src/font.cpp index 080b74d687..9a2f807af4 100644 --- a/doomsday/libgui/src/font.cpp +++ b/doomsday/libgui/src/font.cpp @@ -25,6 +25,289 @@ namespace de { +Font::RichFormat::RichFormat() +{} + +Font::RichFormat::RichFormat(RichFormat const &other) + : _ranges(other._ranges) +{} + +Font::RichFormat Font::RichFormat::fromPlainText(String const &plainText) +{ + FormatRange all; + all.range = Rangei(0, plainText.size()); + RichFormat form; + form._ranges << all; + return form; +} + +String Font::RichFormat::initFromStyledText(String const &styledText) +{ + String plain; + FormatRange *format = 0; + Rangei range; // within styledText + int offset = 0; // from styled to plain + + // Insert the first range. + _ranges << FormatRange(); + format = &_ranges.back(); + + forever + { + range.end = styledText.indexOf(QChar('\x1b'), range.start); + if(range.end >= 0) + { + // Empty ranges do not cause insertion of new formats. + if(range.size() > 0) + { + // Update current range's end. + format->range.end = range.end; + + // Update plaintext. + plain += styledText.substr(range); + format->range.end -= offset; // within plain + + // Start a new range (copying the current one). + _ranges << FormatRange(*format); + format = &_ranges.back(); + format->range = Rangei(range.end - offset, range.end - offset); + } + + // Check the escape sequence. + char ch = styledText[range.end + 1].toLatin1(); + switch(ch) + { + case '.': + format->sizeFactor = 1.f; + format->colorIndex = OriginalColor; + format->weight = OriginalWeight; + format->style = OriginalStyle; + break; + + case '>': + format->markIndent = true; + break; + + case 'b': + format->weight = Bold; + break; + + case 'l': + format->weight = Light; + break; + + case 'w': + format->weight = Normal; + break; + + case 'r': + format->style = Regular; + break; + + case 'i': + format->style = Italic; + break; + + case 's': + format->sizeFactor = .8f; + break; + + case 't': + format->sizeFactor = .75f; + break; + + case 'n': + format->sizeFactor = .6f; + break; + + case 'A': // Normal color + case 'B': // Highlight color + case 'C': // Dimmed color + case 'D': // Accent color + case 'E': + case 'F': + format->colorIndex = ch - 'A'; + break; + + case '0': // Normal style + case '6': // Message style + format->sizeFactor = 1.f; + format->weight = OriginalWeight; + format->style = OriginalStyle; + format->colorIndex = OriginalColor; + break; + + case '1': // Strong style + format->sizeFactor = 1.f; + format->weight = Bold; + format->style = OriginalStyle; + format->colorIndex = OriginalColor; + break; + + case '2': // Log time style + format->sizeFactor = .8f; + format->weight = Light; + format->style = OriginalStyle; + format->colorIndex = DimmedColor; + break; + + case '3': // Log level style + format->sizeFactor = .8f; + format->weight = Bold; + format->style = OriginalStyle; + format->colorIndex = OriginalColor; + break; + + case '4': // Bad log level style + format->sizeFactor = .8f; + format->weight = Bold; + format->style = OriginalStyle; + format->colorIndex = AccentColor; + break; + + case '7': // Debug log level style + format->sizeFactor = .8f; + format->weight = Normal; + format->style = OriginalStyle; + format->colorIndex = OriginalColor; + break; + + case '5': // Log section style + format->sizeFactor = 1.f; + format->weight = OriginalWeight; + format->style = Italic; + format->colorIndex = DimmedColor; + break; + + case '8': // Bad message style + format->sizeFactor = 1.f; + format->weight = Bold; + format->style = Regular; + format->colorIndex = AccentColor; + break; + + case '9': // Debug message style + format->sizeFactor = .8f; + format->weight = Light; + format->style = Regular; + format->colorIndex = DimmedColor; + break; + } + + // Advance the scanner. + range.start = range.end + 2; + offset += 2; // skipped chars + } + else + { + // No more styles. + range.end = styledText.size(); + format->range.end = range.end; + plain += styledText.substr(range); + format->range.end -= offset; + if(!format->range.size()) + { + // Don't keep an empty range at the end. + _ranges.takeLast(); + } + break; + } + } + + /* + qDebug() << "Styled text:" << styledText; + qDebug() << "plain:" << plain; + foreach(FormatRange const &r, _ranges) + { + qDebug() << r.range.asText() + << plain.substr(r.range) + << "size:" << r.sizeFactor + << "weight:" << r.weight + << "style:" << r.style + << "color:" << r.colorIndex; + }*/ + + return plain; +} + +Font::RichFormat Font::RichFormat::subRange(Rangei const &range) const +{ + RichFormat sub(*this); + + for(int i = 0; i < sub._ranges.size(); ++i) + { + Rangei &sr = sub._ranges[i].range; + + sr -= range.start; + if(sr.end < 0 || sr.start >= range.size()) + { + // This range is outside the subrange. + sub._ranges.removeAt(i--); + continue; + } + sr.start = de::max(sr.start, 0); + sr.end = de::min(sr.end, range.size()); + if(!sr.size()) + { + sub._ranges.removeAt(i--); + continue; + } + } + + return sub; +} + +Font::RichFormat::Iterator::Iterator(RichFormat const &f) : format(f), index(-1) {} + +bool Font::RichFormat::Iterator::hasNext() const +{ + return index + 1 < format._ranges.size(); +} + +void Font::RichFormat::Iterator::next() +{ + index++; + DENG2_ASSERT(index < format._ranges.size()); +} + +bool Font::RichFormat::Iterator::isOriginal() const +{ + return (fequal(sizeFactor(), 1.f) && + weight() == OriginalWeight && + style() == OriginalStyle && + color() == OriginalColor); +} + +Rangei Font::RichFormat::Iterator::range() const +{ + return format._ranges[index].range; +} + +float Font::RichFormat::Iterator::sizeFactor() const +{ + return format._ranges[index].sizeFactor; +} + +Font::RichFormat::Weight Font::RichFormat::Iterator::weight() const +{ + return format._ranges[index].weight; +} + +Font::RichFormat::Style Font::RichFormat::Iterator::style() const +{ + return format._ranges[index].style; +} + +int Font::RichFormat::Iterator::color() const +{ + return format._ranges[index].colorIndex; +} + +bool Font::RichFormat::Iterator::markIndent() const +{ + return format._ranges[index].markIndent; +} + DENG2_PIMPL(Font) { QFont font; @@ -62,6 +345,40 @@ DENG2_PIMPL(Font) descentRule->set(metrics->descent()); lineSpacingRule->set(metrics->lineSpacing()); } + + QFont alteredFont(RichFormat::Iterator const &rich) const + { + if(!rich.isOriginal()) + { + QFont mod = font; + + if(!fequal(rich.sizeFactor(), 1.f)) + { + mod.setPointSizeF(mod.pointSizeF() * rich.sizeFactor()); + } + if(rich.style() != RichFormat::OriginalStyle) + { + mod.setItalic(rich.style() == RichFormat::Italic); + } + if(rich.weight() != RichFormat::OriginalWeight) + { + mod.setWeight(rich.weight() == RichFormat::Normal? QFont::Normal : + rich.weight() == RichFormat::Bold? QFont::Bold : + QFont::Light); + } + return mod; + } + return font; + } + + QFontMetrics alteredMetrics(RichFormat::Iterator const &rich) const + { + if(!rich.isOriginal()) + { + return QFontMetrics(alteredFont(rich)); + } + return *metrics; + } }; Font::Font() : d(new Instance(this)) @@ -80,15 +397,62 @@ QFont Font::toQFont() const Rectanglei Font::measure(String const &textLine) const { - return Rectanglei::fromQRect(d->metrics->boundingRect(textLine)); + return measure(textLine, RichFormat::fromPlainText(textLine)); +} + +Rectanglei Font::measure(String const &textLine, RichFormat const &format) const +{ + Rectanglei bounds; + int advance = 0; + + RichFormat::Iterator iter(format); + while(iter.hasNext()) + { + iter.next(); + + QFontMetrics const metrics = d->alteredMetrics(iter); + + String const part = textLine.substr(iter.range()); + Rectanglei rect = Rectanglei::fromQRect(metrics.boundingRect(part)); + + // Combine to the total bounds. + rect.moveTopLeft(Vector2i(advance, rect.top())); + bounds |= rect; + + advance += metrics.width(part); + } + + return bounds; } int Font::advanceWidth(String const &textLine) const { - return d->metrics->width(textLine); + return advanceWidth(textLine, RichFormat::fromPlainText(textLine)); +} + +int Font::advanceWidth(String const &textLine, RichFormat const &format) const +{ + int advance = 0; + RichFormat::Iterator iter(format); + while(iter.hasNext()) + { + iter.next(); + + QFontMetrics const metrics = d->alteredMetrics(iter); + advance += metrics.width(textLine.substr(iter.range())); + } + return advance; +} + +QImage Font::rasterize(String const &textLine, + Vector4ub const &foreground, + Vector4ub const &background) const +{ + return rasterize(textLine, RichFormat::fromPlainText(textLine), foreground, background); } QImage Font::rasterize(String const &textLine, + RichFormat const &format, Vector4ub const &foreground, Vector4ub const &background) const { @@ -97,18 +461,52 @@ QImage Font::rasterize(String const &textLine, return QImage(); } - Rectanglei bounds = measure(textLine); - - QImage img(QSize(bounds.width(), bounds.height() + 1), QImage::Format_ARGB32); + Rectanglei const bounds = measure(textLine, format); + QColor fgColor(foreground.x, foreground.y, foreground.z, foreground.w); QColor bgColor(background.x, background.y, background.z, background.w); + + QImage img(QSize(bounds.width(), + de::max(duint(d->metrics->height()), bounds.height()) + 1), QImage::Format_ARGB32); img.fill(bgColor.rgba()); QPainter painter(&img); - painter.setFont(d->font); - painter.setPen(QColor(foreground.x, foreground.y, foreground.z, foreground.w)); painter.setBrush(bgColor); - painter.drawText(img.rect(), Qt::TextDontClip | Qt::TextSingleLine, textLine); + + int advance = 0; + RichFormat::Iterator iter(format); + while(iter.hasNext()) + { + iter.next(); + //painter.drawText(img.rect(), Qt::TextDontClip | Qt::TextSingleLine, textLine); + + QFont font = d->font; + + if(iter.isOriginal()) + { + painter.setPen(fgColor); + } + else + { + font = d->alteredFont(iter); + + QColor color = fgColor; + if(iter.color() == RichFormat::DimmedColor) + { + color.setAlpha(color.alpha() * 2 / 3); + } + else if(iter.color() != RichFormat::OriginalColor) + { + // where to get the color palette? + } + painter.setPen(color); + } + painter.setFont(font); + + String const part = textLine.substr(iter.range()); + painter.drawText(advance, d->metrics->ascent(), part); + advance += QFontMetrics(font).width(part); + } return img; }