1
0
mirror of https://gitlab.com/OpenMW/openmw.git synced 2025-01-26 18:35:20 +00:00
OpenMW/apps/openmw/mwgui/bookpage.cpp
2023-05-08 17:18:18 +02:00

1406 lines
46 KiB
C++

#include "bookpage.hpp"
#include <optional>
#include "MyGUI_FactoryManager.h"
#include "MyGUI_FontManager.h"
#include "MyGUI_RenderItem.h"
#include "MyGUI_RenderManager.h"
#include "MyGUI_TextureUtility.h"
#include <components/misc/utf8stream.hpp>
#include <components/sceneutil/depth.hpp>
namespace MWGui
{
struct TypesetBookImpl;
class PageDisplay;
class BookPageImpl;
static bool ucsSpace(int codePoint);
static bool ucsLineBreak(int codePoint);
static bool ucsCarriageReturn(int codePoint);
static bool ucsBreakingSpace(int codePoint);
struct BookTypesetter::Style
{
virtual ~Style() {}
};
struct TypesetBookImpl : TypesetBook
{
typedef std::vector<uint8_t> Content;
typedef std::list<Content> Contents;
typedef Utf8Stream::Point Utf8Point;
typedef std::pair<Utf8Point, Utf8Point> Range;
struct StyleImpl : BookTypesetter::Style
{
MyGUI::IFont* mFont;
MyGUI::Colour mHotColour;
MyGUI::Colour mActiveColour;
MyGUI::Colour mNormalColour;
InteractiveId mInteractiveId;
bool match(MyGUI::IFont* tstFont, const MyGUI::Colour& tstHotColour, const MyGUI::Colour& tstActiveColour,
const MyGUI::Colour& tstNormalColour, intptr_t tstInteractiveId)
{
return (mFont == tstFont)
&& partal_match(tstHotColour, tstActiveColour, tstNormalColour, tstInteractiveId);
}
bool match(std::string_view tstFont, const MyGUI::Colour& tstHotColour,
const MyGUI::Colour& tstActiveColour, const MyGUI::Colour& tstNormalColour, intptr_t tstInteractiveId)
{
return (mFont->getResourceName() == tstFont)
&& partal_match(tstHotColour, tstActiveColour, tstNormalColour, tstInteractiveId);
}
bool partal_match(const MyGUI::Colour& tstHotColour, const MyGUI::Colour& tstActiveColour,
const MyGUI::Colour& tstNormalColour, intptr_t tstInteractiveId)
{
return (mHotColour == tstHotColour) && (mActiveColour == tstActiveColour)
&& (mNormalColour == tstNormalColour) && (mInteractiveId == tstInteractiveId);
}
};
typedef std::list<StyleImpl> Styles;
struct Run
{
StyleImpl* mStyle;
Range mRange;
int mLeft, mRight;
int mPrintableChars;
};
typedef std::vector<Run> Runs;
struct Line
{
Runs mRuns;
MyGUI::IntRect mRect;
};
typedef std::vector<Line> Lines;
struct Section
{
Lines mLines;
MyGUI::IntRect mRect;
};
typedef std::vector<Section> Sections;
// Holds "top" and "bottom" vertical coordinates in the source text.
// A page is basically a "window" into a portion of the source text, similar to a ScrollView.
typedef std::pair<int, int> Page;
typedef std::vector<Page> Pages;
Pages mPages;
Sections mSections;
Contents mContents;
Styles mStyles;
MyGUI::IntRect mRect;
virtual ~TypesetBookImpl() {}
Range addContent(const BookTypesetter::Utf8Span& text)
{
Contents::iterator i = mContents.insert(mContents.end(), Content(text.first, text.second));
if (i->empty())
return Range(Utf8Point(nullptr), Utf8Point(nullptr));
return Range(i->data(), i->data() + i->size());
}
size_t pageCount() const override { return mPages.size(); }
std::pair<unsigned int, unsigned int> getSize() const override
{
return std::make_pair(mRect.width(), mRect.height());
}
template <typename Visitor>
void visitRuns(int top, int bottom, MyGUI::IFont* Font, Visitor const& visitor) const
{
for (Sections::const_iterator i = mSections.begin(); i != mSections.end(); ++i)
{
if (top >= mRect.bottom || bottom <= i->mRect.top)
continue;
for (Lines::const_iterator j = i->mLines.begin(); j != i->mLines.end(); ++j)
{
if (top >= j->mRect.bottom || bottom <= j->mRect.top)
continue;
for (Runs::const_iterator k = j->mRuns.begin(); k != j->mRuns.end(); ++k)
if (!Font || k->mStyle->mFont == Font)
visitor(*i, *j, *k);
}
}
}
template <typename Visitor>
void visitRuns(int top, int bottom, Visitor const& visitor) const
{
visitRuns(top, bottom, nullptr, visitor);
}
/// hit test with a margin for error. only hits on interactive text fragments are reported.
StyleImpl* hitTestWithMargin(int left, int top)
{
StyleImpl* hit = hitTest(left, top);
if (hit && hit->mInteractiveId != 0)
return hit;
const int maxMargin = 10;
for (int margin = 1; margin < maxMargin; ++margin)
{
for (int i = 0; i < 4; ++i)
{
if (i == 0)
hit = hitTest(left, top - margin);
else if (i == 1)
hit = hitTest(left, top + margin);
else if (i == 2)
hit = hitTest(left - margin, top);
else
hit = hitTest(left + margin, top);
if (hit && hit->mInteractiveId != 0)
return hit;
}
}
return nullptr;
}
StyleImpl* hitTest(int left, int top) const
{
for (Sections::const_iterator i = mSections.begin(); i != mSections.end(); ++i)
{
if (top < i->mRect.top || top >= i->mRect.bottom)
continue;
int left1 = left - i->mRect.left;
for (Lines::const_iterator j = i->mLines.begin(); j != i->mLines.end(); ++j)
{
if (top < j->mRect.top || top >= j->mRect.bottom)
continue;
int left2 = left1 - j->mRect.left;
for (Runs::const_iterator k = j->mRuns.begin(); k != j->mRuns.end(); ++k)
{
if (left2 < k->mLeft || left2 >= k->mRight)
continue;
return k->mStyle;
}
}
}
return nullptr;
}
MyGUI::IFont* affectedFont(StyleImpl* style)
{
for (Styles::iterator i = mStyles.begin(); i != mStyles.end(); ++i)
if (&*i == style)
return i->mFont;
return nullptr;
}
struct Typesetter;
};
struct TypesetBookImpl::Typesetter : BookTypesetter
{
struct PartialText
{
StyleImpl* mStyle;
Utf8Stream::Point mBegin;
Utf8Stream::Point mEnd;
int mWidth;
PartialText(StyleImpl* style, Utf8Stream::Point begin, Utf8Stream::Point end, int width)
: mStyle(style)
, mBegin(begin)
, mEnd(end)
, mWidth(width)
{
}
};
typedef TypesetBookImpl Book;
typedef std::shared_ptr<Book> BookPtr;
typedef std::vector<PartialText>::const_iterator PartialTextConstIterator;
int mPageWidth;
int mPageHeight;
BookPtr mBook;
Section* mSection;
Line* mLine;
Run* mRun;
std::vector<Alignment> mSectionAlignment;
std::vector<PartialText> mPartialWhitespace;
std::vector<PartialText> mPartialWord;
Book::Content const* mCurrentContent;
Alignment mCurrentAlignment;
Typesetter(size_t width, size_t height)
: mPageWidth(width)
, mPageHeight(height)
, mSection(nullptr)
, mLine(nullptr)
, mRun(nullptr)
, mCurrentContent(nullptr)
, mCurrentAlignment(AlignLeft)
{
mBook = std::make_shared<Book>();
}
virtual ~Typesetter() {}
Style* createStyle(const std::string& fontName, const Colour& fontColour, bool useBookFont) override
{
std::string fullFontName;
if (fontName.empty())
fullFontName = MyGUI::FontManager::getInstance().getDefaultFont();
else
fullFontName = fontName;
if (useBookFont)
fullFontName = "Journalbook " + fullFontName;
for (Styles::iterator i = mBook->mStyles.begin(); i != mBook->mStyles.end(); ++i)
if (i->match(fullFontName, fontColour, fontColour, fontColour, 0))
return &*i;
MyGUI::IFont* font = MyGUI::FontManager::getInstance().getByName(fullFontName);
if (!font)
throw std::runtime_error(std::string("can't find font ") + fullFontName);
StyleImpl& style = *mBook->mStyles.insert(mBook->mStyles.end(), StyleImpl());
style.mFont = font;
style.mHotColour = fontColour;
style.mActiveColour = fontColour;
style.mNormalColour = fontColour;
style.mInteractiveId = 0;
return &style;
}
Style* createHotStyle(Style* baseStyle, const Colour& normalColour, const Colour& hoverColour,
const Colour& activeColour, InteractiveId id, bool unique) override
{
StyleImpl* BaseStyle = static_cast<StyleImpl*>(baseStyle);
if (!unique)
for (Styles::iterator i = mBook->mStyles.begin(); i != mBook->mStyles.end(); ++i)
if (i->match(BaseStyle->mFont, hoverColour, activeColour, normalColour, id))
return &*i;
StyleImpl& style = *mBook->mStyles.insert(mBook->mStyles.end(), StyleImpl());
style.mFont = BaseStyle->mFont;
style.mHotColour = hoverColour;
style.mActiveColour = activeColour;
style.mNormalColour = normalColour;
style.mInteractiveId = id;
return &style;
}
void write(Style* style, Utf8Span text) override
{
Range range = mBook->addContent(text);
writeImpl(static_cast<StyleImpl*>(style), range.first, range.second);
}
intptr_t addContent(Utf8Span text, bool select) override
{
add_partial_text();
Contents::iterator i = mBook->mContents.insert(mBook->mContents.end(), Content(text.first, text.second));
if (select)
mCurrentContent = &(*i);
return reinterpret_cast<intptr_t>(&(*i));
}
void selectContent(intptr_t contentHandle) override
{
add_partial_text();
mCurrentContent = reinterpret_cast<Content const*>(contentHandle);
}
void write(Style* style, size_t begin, size_t end) override
{
assert(mCurrentContent != nullptr);
assert(end <= mCurrentContent->size());
assert(begin <= mCurrentContent->size());
Utf8Point begin_ = mCurrentContent->data() + begin;
Utf8Point end_ = mCurrentContent->data() + end;
writeImpl(static_cast<StyleImpl*>(style), begin_, end_);
}
void lineBreak(float margin) override
{
assert(margin == 0); // TODO: figure out proper behavior here...
add_partial_text();
mRun = nullptr;
mLine = nullptr;
}
void sectionBreak(int margin) override
{
add_partial_text();
if (mBook->mSections.size() > 0)
{
mRun = nullptr;
mLine = nullptr;
mSection = nullptr;
if (mBook->mRect.bottom < (mBook->mSections.back().mRect.bottom + margin))
mBook->mRect.bottom = (mBook->mSections.back().mRect.bottom + margin);
}
}
void setSectionAlignment(Alignment sectionAlignment) override
{
add_partial_text();
if (mSection != nullptr)
mSectionAlignment.back() = sectionAlignment;
mCurrentAlignment = sectionAlignment;
}
TypesetBook::Ptr complete() override
{
int curPageStart = 0;
int curPageStop = 0;
add_partial_text();
std::vector<Alignment>::iterator sa = mSectionAlignment.begin();
for (Sections::iterator i = mBook->mSections.begin(); i != mBook->mSections.end(); ++i, ++sa)
{
// apply alignment to individual lines...
for (Lines::iterator j = i->mLines.begin(); j != i->mLines.end(); ++j)
{
int width = j->mRect.width();
int excess = mPageWidth - width;
switch (*sa)
{
default:
case AlignLeft:
j->mRect.left = 0;
break;
case AlignCenter:
j->mRect.left = excess / 2;
break;
case AlignRight:
j->mRect.left = excess;
break;
}
j->mRect.right = j->mRect.left + width;
}
if (curPageStop == curPageStart)
{
curPageStart = i->mRect.top;
curPageStop = i->mRect.top;
}
int spaceLeft = mPageHeight - (curPageStop - curPageStart);
int sectionHeight = i->mRect.height();
// This is NOT equal to i->mRect.height(), which doesn't account for section breaks.
int spaceRequired = (i->mRect.bottom - curPageStop);
if (curPageStart == curPageStop) // If this is a new page, the section break is not needed
spaceRequired = i->mRect.height();
if (spaceRequired <= mPageHeight)
{
if (spaceRequired > spaceLeft)
{
// The section won't completely fit on the current page. Finish the current page and start a new
// one.
assert(curPageStart != curPageStop);
mBook->mPages.push_back(Page(curPageStart, curPageStop));
curPageStart = i->mRect.top;
curPageStop = i->mRect.bottom;
}
else
curPageStop = i->mRect.bottom;
}
else
{
// The section won't completely fit on the current page. Finish the current page and start a new
// one.
mBook->mPages.push_back(Page(curPageStart, curPageStop));
curPageStart = i->mRect.top;
curPageStop = i->mRect.bottom;
// split section
int sectionHeightLeft = sectionHeight;
while (sectionHeightLeft >= mPageHeight)
{
// Adjust to the top of the first line that does not fit on the current page anymore
int splitPos = curPageStop;
for (Lines::iterator j = i->mLines.begin(); j != i->mLines.end(); ++j)
{
if (j->mRect.bottom > curPageStart + mPageHeight)
{
splitPos = j->mRect.top;
break;
}
}
mBook->mPages.push_back(Page(curPageStart, splitPos));
curPageStart = splitPos;
curPageStop = splitPos;
sectionHeightLeft = (i->mRect.bottom - splitPos);
}
curPageStop = i->mRect.bottom;
}
}
if (curPageStart != curPageStop)
mBook->mPages.push_back(Page(curPageStart, curPageStop));
return mBook;
}
void writeImpl(StyleImpl* style, Utf8Stream::Point _begin, Utf8Stream::Point _end)
{
Utf8Stream stream(_begin, _end);
while (!stream.eof())
{
if (ucsLineBreak(stream.peek()))
{
add_partial_text();
stream.consume();
mLine = nullptr;
mRun = nullptr;
continue;
}
if (ucsBreakingSpace(stream.peek()) && !mPartialWord.empty())
add_partial_text();
int word_width = 0;
int space_width = 0;
Utf8Stream::Point lead = stream.current();
while (!stream.eof() && !ucsLineBreak(stream.peek()) && ucsBreakingSpace(stream.peek()))
{
MWGui::GlyphInfo info = GlyphInfo(style->mFont, stream.peek());
if (info.charFound)
space_width += static_cast<int>(info.advance + info.bearingX);
stream.consume();
}
Utf8Stream::Point origin = stream.current();
while (!stream.eof() && !ucsLineBreak(stream.peek()) && !ucsBreakingSpace(stream.peek()))
{
MWGui::GlyphInfo info = GlyphInfo(style->mFont, stream.peek());
if (info.charFound)
word_width += static_cast<int>(info.advance + info.bearingX);
stream.consume();
}
Utf8Stream::Point extent = stream.current();
if (lead == extent)
break;
if (lead != origin)
mPartialWhitespace.emplace_back(style, lead, origin, space_width);
if (origin != extent)
mPartialWord.emplace_back(style, origin, extent, word_width);
}
}
void add_partial_text()
{
if (mPartialWhitespace.empty() && mPartialWord.empty())
return;
int fontHeight = MWBase::Environment::get().getWindowManager()->getFontHeight();
int space_width = 0;
int word_width = 0;
for (PartialTextConstIterator i = mPartialWhitespace.begin(); i != mPartialWhitespace.end(); ++i)
space_width += i->mWidth;
for (PartialTextConstIterator i = mPartialWord.begin(); i != mPartialWord.end(); ++i)
word_width += i->mWidth;
int left = mLine ? mLine->mRect.right : 0;
if (left + space_width + word_width > mPageWidth)
{
mLine = nullptr;
mRun = nullptr;
left = 0;
}
else
{
for (PartialTextConstIterator i = mPartialWhitespace.begin(); i != mPartialWhitespace.end(); ++i)
{
int top = mLine ? mLine->mRect.top : mBook->mRect.bottom;
append_run(i->mStyle, i->mBegin, i->mEnd, 0, left + i->mWidth, top + fontHeight);
left = mLine->mRect.right;
}
}
for (PartialTextConstIterator i = mPartialWord.begin(); i != mPartialWord.end(); ++i)
{
int top = mLine ? mLine->mRect.top : mBook->mRect.bottom;
append_run(i->mStyle, i->mBegin, i->mEnd, i->mEnd - i->mBegin, left + i->mWidth, top + fontHeight);
left = mLine->mRect.right;
}
mPartialWhitespace.clear();
mPartialWord.clear();
}
void append_run(StyleImpl* style, Utf8Stream::Point begin, Utf8Stream::Point end, int pc, int right, int bottom)
{
if (mSection == nullptr)
{
mBook->mSections.push_back(Section());
mSection = &mBook->mSections.back();
mSection->mRect = MyGUI::IntRect(0, mBook->mRect.bottom, 0, mBook->mRect.bottom);
mSectionAlignment.push_back(mCurrentAlignment);
}
if (mLine == nullptr)
{
mSection->mLines.push_back(Line());
mLine = &mSection->mLines.back();
mLine->mRect = MyGUI::IntRect(0, mSection->mRect.bottom, 0, mBook->mRect.bottom);
}
if (mBook->mRect.right < right)
mBook->mRect.right = right;
if (mBook->mRect.bottom < bottom)
mBook->mRect.bottom = bottom;
if (mSection->mRect.right < right)
mSection->mRect.right = right;
if (mSection->mRect.bottom < bottom)
mSection->mRect.bottom = bottom;
if (mLine->mRect.right < right)
mLine->mRect.right = right;
if (mLine->mRect.bottom < bottom)
mLine->mRect.bottom = bottom;
if (mRun == nullptr || mRun->mStyle != style || mRun->mRange.second != begin)
{
int left = mRun ? mRun->mRight : mLine->mRect.left;
mLine->mRuns.push_back(Run());
mRun = &mLine->mRuns.back();
mRun->mStyle = style;
mRun->mLeft = left;
mRun->mRight = right;
mRun->mRange.first = begin;
mRun->mRange.second = end;
mRun->mPrintableChars = pc;
// Run->Locale = Locale;
}
else
{
mRun->mRight = right;
mRun->mRange.second = end;
mRun->mPrintableChars += pc;
}
}
};
BookTypesetter::Ptr BookTypesetter::create(int pageWidth, int pageHeight)
{
return std::make_shared<TypesetBookImpl::Typesetter>(pageWidth, pageHeight);
}
namespace
{
struct RenderXform
{
public:
float clipTop;
float clipLeft;
float clipRight;
float clipBottom;
float absoluteLeft;
float absoluteTop;
float leftOffset;
float topOffset;
float pixScaleX;
float pixScaleY;
float hOffset;
float vOffset;
RenderXform(MyGUI::ICroppedRectangle* croppedParent, MyGUI::RenderTargetInfo const& renderTargetInfo)
{
clipTop = static_cast<float>(croppedParent->_getMarginTop());
clipLeft = static_cast<float>(croppedParent->_getMarginLeft());
clipRight = static_cast<float>(croppedParent->getWidth() - croppedParent->_getMarginRight());
clipBottom = static_cast<float>(croppedParent->getHeight() - croppedParent->_getMarginBottom());
absoluteLeft = static_cast<float>(croppedParent->getAbsoluteLeft());
absoluteTop = static_cast<float>(croppedParent->getAbsoluteTop());
leftOffset = static_cast<float>(renderTargetInfo.leftOffset);
topOffset = static_cast<float>(renderTargetInfo.topOffset);
pixScaleX = renderTargetInfo.pixScaleX;
pixScaleY = renderTargetInfo.pixScaleY;
hOffset = renderTargetInfo.hOffset;
vOffset = renderTargetInfo.vOffset;
}
bool clip(MyGUI::FloatRect& vr, MyGUI::FloatRect& tr)
{
if (vr.bottom <= clipTop || vr.right <= clipLeft || vr.left >= clipRight || vr.top >= clipBottom)
return false;
if (vr.top < clipTop)
{
tr.top += tr.height() * (clipTop - vr.top) / vr.height();
vr.top = clipTop;
}
if (vr.left < clipLeft)
{
tr.left += tr.width() * (clipLeft - vr.left) / vr.width();
vr.left = clipLeft;
}
if (vr.right > clipRight)
{
tr.right -= tr.width() * (vr.right - clipRight) / vr.width();
vr.right = clipRight;
}
if (vr.bottom > clipBottom)
{
tr.bottom -= tr.height() * (vr.bottom - clipBottom) / vr.height();
vr.bottom = clipBottom;
}
return true;
}
MyGUI::FloatPoint operator()(MyGUI::FloatPoint pt)
{
pt.left = absoluteLeft - leftOffset + pt.left;
pt.top = absoluteTop - topOffset + pt.top;
pt.left = +(((pixScaleX * pt.left + hOffset) * 2.0f) - 1.0f);
pt.top = -(((pixScaleY * pt.top + vOffset) * 2.0f) - 1.0f);
return pt;
}
};
struct GlyphStream
{
float mZ;
uint32_t mC;
MyGUI::IFont* mFont;
MyGUI::FloatPoint mOrigin;
MyGUI::FloatPoint mCursor;
MyGUI::Vertex* mVertices;
RenderXform mRenderXform;
MyGUI::VertexColourType mVertexColourType;
GlyphStream(MyGUI::IFont* font, float left, float top, float Z, MyGUI::Vertex* vertices,
RenderXform const& renderXform)
: mZ(Z)
, mC(0)
, mFont(font)
, mOrigin(left, top)
, mVertices(vertices)
, mRenderXform(renderXform)
{
assert(font != nullptr);
mVertexColourType = MyGUI::RenderManager::getInstance().getVertexFormat();
}
~GlyphStream() = default;
MyGUI::Vertex* end() const { return mVertices; }
void reset(float left, float top, MyGUI::Colour colour)
{
mC = MyGUI::texture_utility::toColourARGB(colour) | 0xFF000000;
MyGUI::texture_utility::convertColour(mC, mVertexColourType);
mCursor.left = mOrigin.left + left;
mCursor.top = mOrigin.top + top;
}
void emitGlyph(wchar_t ch)
{
MWGui::GlyphInfo info = GlyphInfo(mFont, ch);
if (!info.charFound)
return;
MyGUI::FloatRect vr;
vr.left = mCursor.left + info.bearingX;
vr.top = mCursor.top + info.bearingY;
vr.right = vr.left + info.width;
vr.bottom = vr.top + info.height;
MyGUI::FloatRect tr = info.uvRect;
if (mRenderXform.clip(vr, tr))
quad(vr, tr);
mCursor.left += static_cast<int>(info.bearingX + info.advance);
}
void emitSpace(wchar_t ch)
{
MWGui::GlyphInfo info = GlyphInfo(mFont, ch);
if (info.charFound)
mCursor.left += static_cast<int>(info.bearingX + info.advance);
}
private:
void quad(const MyGUI::FloatRect& vr, const MyGUI::FloatRect& tr)
{
vertex(vr.left, vr.top, tr.left, tr.top);
vertex(vr.right, vr.top, tr.right, tr.top);
vertex(vr.left, vr.bottom, tr.left, tr.bottom);
vertex(vr.right, vr.top, tr.right, tr.top);
vertex(vr.left, vr.bottom, tr.left, tr.bottom);
vertex(vr.right, vr.bottom, tr.right, tr.bottom);
}
void vertex(float x, float y, float u, float v)
{
MyGUI::FloatPoint pt = mRenderXform(MyGUI::FloatPoint(x, y));
mVertices->x = pt.left;
mVertices->y = pt.top;
mVertices->z = mZ;
mVertices->u = u;
mVertices->v = v;
mVertices->colour = mC;
++mVertices;
}
};
}
class PageDisplay final : public MyGUI::ISubWidgetText
{
MYGUI_RTTI_DERIVED(PageDisplay)
protected:
typedef TypesetBookImpl::Section Section;
typedef TypesetBookImpl::Line Line;
typedef TypesetBookImpl::Run Run;
bool mIsPageReset;
size_t mPage;
struct TextFormat : ISubWidget
{
typedef MyGUI::IFont* Id;
Id mFont;
int mCountVertex;
MyGUI::ITexture* mTexture;
MyGUI::RenderItem* mRenderItem;
PageDisplay* mDisplay;
TextFormat(MyGUI::IFont* id, PageDisplay* display)
: mFont(id)
, mCountVertex(0)
, mTexture(nullptr)
, mRenderItem(nullptr)
, mDisplay(display)
{
}
void createDrawItem(MyGUI::ILayerNode* node)
{
assert(mRenderItem == nullptr);
if (mTexture != nullptr)
{
mRenderItem = node->addToRenderItem(mTexture, false, false);
mRenderItem->addDrawItem(this, mCountVertex);
}
}
void destroyDrawItem(MyGUI::ILayerNode* node)
{
assert(mTexture != nullptr ? mRenderItem != nullptr : mRenderItem == nullptr);
if (mTexture != nullptr)
{
mRenderItem->removeDrawItem(this);
mRenderItem = nullptr;
}
}
void doRender() override { mDisplay->doRender(*this); }
// this isn't really a sub-widget, its just a "drawitem" which
// should have its own interface
void createDrawItem(MyGUI::ITexture* _texture, MyGUI::ILayerNode* _node) override {}
void destroyDrawItem() override {}
};
void resetPage()
{
mIsPageReset = true;
mPage = 0;
}
void setPage(size_t page)
{
mIsPageReset = false;
mPage = page;
}
bool isPageDifferent(size_t page) { return mIsPageReset || (mPage != page); }
std::optional<MyGUI::IntPoint> getAdjustedPos(int left, int top, bool move = false)
{
if (!mBook)
return {};
if (mPage >= mBook->mPages.size())
return {};
MyGUI::IntPoint pos(left, top);
pos.left -= mCroppedParent->getAbsoluteLeft();
pos.top -= mCroppedParent->getAbsoluteTop();
pos.top += mViewTop;
return pos;
}
public:
typedef TypesetBookImpl::StyleImpl Style;
typedef std::map<TextFormat::Id, std::unique_ptr<TextFormat>> ActiveTextFormats;
int mViewTop;
int mViewBottom;
Style* mFocusItem;
bool mItemActive;
MyGUI::MouseButton mLastDown;
std::function<void(intptr_t)> mLinkClicked;
std::shared_ptr<TypesetBookImpl> mBook;
MyGUI::ILayerNode* mNode;
ActiveTextFormats mActiveTextFormats;
PageDisplay()
{
resetPage();
mViewTop = 0;
mViewBottom = 0;
mFocusItem = nullptr;
mItemActive = false;
mNode = nullptr;
}
void dirtyFocusItem()
{
if (mFocusItem != nullptr)
{
MyGUI::IFont* Font = mBook->affectedFont(mFocusItem);
ActiveTextFormats::iterator i = mActiveTextFormats.find(Font);
if (mNode)
mNode->outOfDate(i->second->mRenderItem);
}
}
void onMouseLostFocus()
{
if (!mBook)
return;
if (mPage >= mBook->mPages.size())
return;
dirtyFocusItem();
mFocusItem = nullptr;
mItemActive = false;
}
void onMouseMove(int left, int top)
{
Style* hit = nullptr;
if (auto pos = getAdjustedPos(left, top, true))
if (pos->top <= mViewBottom)
hit = mBook->hitTestWithMargin(pos->left, pos->top);
if (mLastDown == MyGUI::MouseButton::None)
{
if (hit != mFocusItem)
{
dirtyFocusItem();
mFocusItem = hit;
mItemActive = false;
dirtyFocusItem();
}
}
else if (mFocusItem != nullptr)
{
bool newItemActive = hit == mFocusItem;
if (newItemActive != mItemActive)
{
mItemActive = newItemActive;
dirtyFocusItem();
}
}
}
void onMouseButtonPressed(int left, int top, MyGUI::MouseButton id)
{
auto pos = getAdjustedPos(left, top);
if (pos && mLastDown == MyGUI::MouseButton::None)
{
mFocusItem = pos->top <= mViewBottom ? mBook->hitTestWithMargin(pos->left, pos->top) : nullptr;
mItemActive = true;
dirtyFocusItem();
mLastDown = id;
}
}
void onMouseButtonReleased(int left, int top, MyGUI::MouseButton id)
{
auto pos = getAdjustedPos(left, top);
if (pos && mLastDown == id)
{
Style* item = pos->top <= mViewBottom ? mBook->hitTestWithMargin(pos->left, pos->top) : nullptr;
bool clicked = mFocusItem == item;
mItemActive = false;
dirtyFocusItem();
mLastDown = MyGUI::MouseButton::None;
if (clicked && mLinkClicked && item && item->mInteractiveId != 0)
mLinkClicked(item->mInteractiveId);
}
}
void showPage(TypesetBook::Ptr book, size_t newPage)
{
std::shared_ptr<TypesetBookImpl> newBook = std::dynamic_pointer_cast<TypesetBookImpl>(book);
if (mBook != newBook)
{
mFocusItem = nullptr;
mItemActive = 0;
for (ActiveTextFormats::iterator i = mActiveTextFormats.begin(); i != mActiveTextFormats.end(); ++i)
{
if (mNode != nullptr && i->second != nullptr)
i->second->destroyDrawItem(mNode);
i->second.reset();
}
mActiveTextFormats.clear();
if (newBook != nullptr)
{
createActiveFormats(newBook);
mBook = newBook;
setPage(newPage);
if (newPage < mBook->mPages.size())
{
mViewTop = mBook->mPages[newPage].first;
mViewBottom = mBook->mPages[newPage].second;
}
else
{
mViewTop = 0;
mViewBottom = 0;
}
}
else
{
mBook.reset();
resetPage();
mViewTop = 0;
mViewBottom = 0;
}
}
else if (mBook && isPageDifferent(newPage))
{
if (mNode != nullptr)
for (ActiveTextFormats::iterator i = mActiveTextFormats.begin(); i != mActiveTextFormats.end(); ++i)
mNode->outOfDate(i->second->mRenderItem);
setPage(newPage);
if (newPage < mBook->mPages.size())
{
mViewTop = mBook->mPages[newPage].first;
mViewBottom = mBook->mPages[newPage].second;
}
else
{
mViewTop = 0;
mViewBottom = 0;
}
}
}
struct CreateActiveFormat
{
PageDisplay* this_;
CreateActiveFormat(PageDisplay* this_)
: this_(this_)
{
}
void operator()(Section const& section, Line const& line, Run const& run) const
{
MyGUI::IFont* Font = run.mStyle->mFont;
ActiveTextFormats::iterator j = this_->mActiveTextFormats.find(Font);
if (j == this_->mActiveTextFormats.end())
{
auto textFormat = std::make_unique<TextFormat>(Font, this_);
textFormat->mTexture = Font->getTextureFont();
j = this_->mActiveTextFormats.insert(std::make_pair(Font, std::move(textFormat))).first;
}
j->second->mCountVertex += run.mPrintableChars * 6;
}
};
void createActiveFormats(std::shared_ptr<TypesetBookImpl> newBook)
{
newBook->visitRuns(0, 0x7FFFFFFF, CreateActiveFormat(this));
if (mNode != nullptr)
for (ActiveTextFormats::iterator i = mActiveTextFormats.begin(); i != mActiveTextFormats.end(); ++i)
i->second->createDrawItem(mNode);
}
void setVisible(bool newVisible) override
{
if (mVisible == newVisible)
return;
mVisible = newVisible;
if (mVisible)
{
// reset input state
mLastDown = MyGUI::MouseButton::None;
mFocusItem = nullptr;
mItemActive = 0;
}
if (nullptr != mNode)
{
for (ActiveTextFormats::iterator i = mActiveTextFormats.begin(); i != mActiveTextFormats.end(); ++i)
mNode->outOfDate(i->second->mRenderItem);
}
}
void createDrawItem(MyGUI::ITexture* texture, MyGUI::ILayerNode* node) override
{
mNode = node;
for (ActiveTextFormats::iterator i = mActiveTextFormats.begin(); i != mActiveTextFormats.end(); ++i)
i->second->createDrawItem(node);
}
struct RenderRun
{
PageDisplay* this_;
GlyphStream& glyphStream;
RenderRun(PageDisplay* this_, GlyphStream& glyphStream)
: this_(this_)
, glyphStream(glyphStream)
{
}
void operator()(Section const& section, Line const& line, Run const& run) const
{
bool isActive = run.mStyle->mInteractiveId && (run.mStyle == this_->mFocusItem);
MyGUI::Colour colour = isActive
? (this_->mItemActive ? run.mStyle->mActiveColour : run.mStyle->mHotColour)
: run.mStyle->mNormalColour;
glyphStream.reset(static_cast<float>(section.mRect.left + line.mRect.left + run.mLeft),
static_cast<float>(line.mRect.top), colour);
Utf8Stream stream(run.mRange);
while (!stream.eof())
{
Utf8Stream::UnicodeChar code_point = stream.consume();
if (ucsCarriageReturn(code_point))
continue;
if (!ucsSpace(code_point))
glyphStream.emitGlyph(code_point);
else
glyphStream.emitSpace(code_point);
}
}
};
/*
queue up rendering operations for this text format
*/
void doRender(TextFormat& textFormat)
{
if (!mVisible)
return;
MyGUI::Vertex* vertices = textFormat.mRenderItem->getCurrentVertexBuffer();
RenderXform renderXform(mCroppedParent, textFormat.mRenderItem->getRenderTarget()->getInfo());
float z = SceneUtil::AutoDepth::isReversed() ? 1.f : -1.f;
GlyphStream glyphStream(textFormat.mFont, static_cast<float>(mCoord.left),
static_cast<float>(mCoord.top - mViewTop), z /*mNode->getNodeDepth()*/, vertices, renderXform);
int visit_top = (std::max)(mViewTop, mViewTop + int(renderXform.clipTop));
int visit_bottom = (std::min)(mViewBottom, mViewTop + int(renderXform.clipBottom));
mBook->visitRuns(visit_top, visit_bottom, textFormat.mFont, RenderRun(this, glyphStream));
textFormat.mRenderItem->setLastVertexCount(glyphStream.end() - vertices);
}
// ISubWidget should not necessarily be a drawitem
// in this case, it is not...
void doRender() override {}
void _updateView() override
{
_checkMargin();
if (mNode != nullptr)
for (ActiveTextFormats::iterator i = mActiveTextFormats.begin(); i != mActiveTextFormats.end(); ++i)
mNode->outOfDate(i->second->mRenderItem);
}
void _correctView() override
{
_checkMargin();
if (mNode != nullptr)
for (ActiveTextFormats::iterator i = mActiveTextFormats.begin(); i != mActiveTextFormats.end(); ++i)
mNode->outOfDate(i->second->mRenderItem);
}
void destroyDrawItem() override
{
for (ActiveTextFormats::iterator i = mActiveTextFormats.begin(); i != mActiveTextFormats.end(); ++i)
i->second->destroyDrawItem(mNode);
mNode = nullptr;
}
};
class BookPageImpl final : public BookPage
{
MYGUI_RTTI_DERIVED(BookPage)
public:
BookPageImpl()
: mPageDisplay(nullptr)
{
}
void showPage(TypesetBook::Ptr book, size_t page) override { mPageDisplay->showPage(book, page); }
void adviseLinkClicked(std::function<void(InteractiveId)> linkClicked) override
{
mPageDisplay->mLinkClicked = linkClicked;
}
void unadviseLinkClicked() override { mPageDisplay->mLinkClicked = std::function<void(InteractiveId)>(); }
protected:
void initialiseOverride() override
{
Base::initialiseOverride();
if (getSubWidgetText())
{
mPageDisplay = getSubWidgetText()->castType<PageDisplay>();
}
else
{
throw std::runtime_error("BookPage unable to find page display sub widget");
}
}
void onMouseLostFocus(Widget* _new) override
{
// NOTE: MyGUI also fires eventMouseLostFocus for widgets that are about to be destroyed (if they had
// focus). Child widgets may already be destroyed! So be careful.
mPageDisplay->onMouseLostFocus();
}
void onMouseMove(int left, int top) override { mPageDisplay->onMouseMove(left, top); }
void onMouseButtonPressed(int left, int top, MyGUI::MouseButton id) override
{
mPageDisplay->onMouseButtonPressed(left, top, id);
}
void onMouseButtonReleased(int left, int top, MyGUI::MouseButton id) override
{
mPageDisplay->onMouseButtonReleased(left, top, id);
}
PageDisplay* mPageDisplay;
};
void BookPage::registerMyGUIComponents()
{
MyGUI::FactoryManager& factory = MyGUI::FactoryManager::getInstance();
factory.registerFactory<BookPageImpl>("Widget");
factory.registerFactory<PageDisplay>("BasisSkin");
}
static bool ucsLineBreak(int codePoint)
{
return codePoint == '\n';
}
static bool ucsCarriageReturn(int codePoint)
{
return codePoint == '\r';
}
static bool ucsSpace(int codePoint)
{
switch (codePoint)
{
case 0x0020: // SPACE
case 0x00A0: // NO-BREAK SPACE
case 0x1680: // OGHAM SPACE MARK
case 0x180E: // MONGOLIAN VOWEL SEPARATOR
case 0x2000: // EN QUAD
case 0x2001: // EM QUAD
case 0x2002: // EN SPACE
case 0x2003: // EM SPACE
case 0x2004: // THREE-PER-EM SPACE
case 0x2005: // FOUR-PER-EM SPACE
case 0x2006: // SIX-PER-EM SPACE
case 0x2007: // FIGURE SPACE
case 0x2008: // PUNCTUATION SPACE
case 0x2009: // THIN SPACE
case 0x200A: // HAIR SPACE
case 0x200B: // ZERO WIDTH SPACE
case 0x202F: // NARROW NO-BREAK SPACE
case 0x205F: // MEDIUM MATHEMATICAL SPACE
case 0x3000: // IDEOGRAPHIC SPACE
case 0xFEFF: // ZERO WIDTH NO-BREAK SPACE
return true;
default:
return false;
}
}
static bool ucsBreakingSpace(int codePoint)
{
switch (codePoint)
{
case 0x0020: // SPACE
// case 0x00A0: // NO-BREAK SPACE
case 0x1680: // OGHAM SPACE MARK
case 0x180E: // MONGOLIAN VOWEL SEPARATOR
case 0x2000: // EN QUAD
case 0x2001: // EM QUAD
case 0x2002: // EN SPACE
case 0x2003: // EM SPACE
case 0x2004: // THREE-PER-EM SPACE
case 0x2005: // FOUR-PER-EM SPACE
case 0x2006: // SIX-PER-EM SPACE
case 0x2007: // FIGURE SPACE
case 0x2008: // PUNCTUATION SPACE
case 0x2009: // THIN SPACE
case 0x200A: // HAIR SPACE
case 0x200B: // ZERO WIDTH SPACE
case 0x202F: // NARROW NO-BREAK SPACE
case 0x205F: // MEDIUM MATHEMATICAL SPACE
case 0x3000: // IDEOGRAPHIC SPACE
// case 0xFEFF: // ZERO WIDTH NO-BREAK SPACE
return true;
default:
return false;
}
}
}