Skip to content

Instantly share code, notes, and snippets.

@sthairno
Last active January 23, 2022 15:46
Show Gist options
  • Save sthairno/fb460eb9ab3f4af9921abe5727b46f15 to your computer and use it in GitHub Desktop.
Save sthairno/fb460eb9ab3f4af9921abe5727b46f15 to your computer and use it in GitHub Desktop.
単語の区切りを考慮した折返し機能付きテキストレンダラー
# include <Siv3D.hpp> // OpenSiv3D v0.6.3
class TextRenderer
{
public:
TextRenderer(Font& font)
:m_font(font)
{
}
public:
static constexpr std::array<char32, 2> newLineChars{ U'\r', U'\n' };
void setText(const String& text)
{
m_text = text;
m_glyphs = m_font.getGlyphs(text);
}
SizeF setWidth(double width)
{
m_newLinePositions.clear();
m_lineWidthList.clear();
m_lineWidthList.push_back(0);
std::deque<Group> groups;
// グループの作成
{
bool inGroup = false;
for (auto [idx, glyph] : Indexed(m_glyphs))
{
bool groupChar = false
|| (U'a' <= glyph.codePoint && glyph.codePoint <= U'z')
|| (U'A' <= glyph.codePoint && glyph.codePoint <= U'Z')
|| glyph.codePoint == U'.'
|| glyph.codePoint == U','
|| glyph.codePoint == U'\'';
if (groupChar != inGroup)
{
if (groupChar)
{
groups.emplace_back(Group{
.begin = idx,
.count = 0,
.xAdvance = 0
});
}
inGroup = groupChar;
}
if (inGroup)
{
Group& group = groups.back();
group.count++;
group.width = group.xAdvance + glyph.left + glyph.width;
group.xAdvance += glyph.xAdvance;
}
}
}
// 改行位置,サイズを計算
{
m_renderSize.x = 0;
double nextX = 0;
for (size_t idx = 0; idx < m_glyphs.size();)
{
Glyph& glyph = m_glyphs[idx];
// グループ
if (not groups.empty() && idx == groups.front().begin)
{
Group& group = groups.front();
if (nextX + group.width > width)
{
nextX = 0;
if (idx != 0)
{
if (m_newLinePositions.empty() || m_newLinePositions.back() != idx - 1)
{
m_newLinePositions.push_back(idx - 1);
m_lineWidthList.push_back(0);
}
}
}
groups.pop_front();
}
if (std::find(newLineChars.cbegin(), newLineChars.cend(), glyph.codePoint) != newLineChars.cend())
{
// 改行文字
if (m_newLinePositions.empty() || m_newLinePositions.back() != idx)
{
m_newLinePositions.push_back(idx);
m_lineWidthList.push_back(0);
}
nextX = 0;
idx += 1;
}
else
{
// その他の文字
double renderWidth = nextX + glyph.left + glyph.width;
if (renderWidth > width)
{
nextX = 0;
renderWidth = nextX + glyph.left + glyph.width;
if (idx != 0)
{
if (m_newLinePositions.empty() || m_newLinePositions.back() != idx - 1)
{
m_newLinePositions.push_back(idx - 1);
m_lineWidthList.push_back(0);
}
}
}
m_lineWidthList.back() = renderWidth;
m_renderSize.x = Max(m_renderSize.x, renderWidth);
nextX += glyph.xAdvance;
idx += 1;
}
}
m_renderSize.y = m_glyphs.empty()
? 0.0
: m_font.height() * (m_newLinePositions.size() + 1);
}
return m_renderSize;
}
RectF draw(Vec2 pos, ColorF color)
{
size_t lineIdx = 0;
size_t beginIdx = 0;
size_t endIdx;
auto nextNewLinePos = m_newLinePositions.cbegin();
double lineY = pos.y + m_font.ascender();
while (nextNewLinePos != m_newLinePositions.cend())
{
endIdx = *nextNewLinePos;
drawLine(
beginIdx,
endIdx,
{
pos.x + (m_renderSize.x - m_lineWidthList[lineIdx++]) / 2,
lineY
},
color
);
lineY += m_font.height();
beginIdx = endIdx + 1;
nextNewLinePos++;
}
endIdx = m_glyphs.size() - 1;
if (beginIdx <= endIdx)
{
drawLine(
beginIdx,
endIdx,
{
pos.x + (m_renderSize.x - m_lineWidthList[lineIdx]) / 2,
lineY
},
color
);
}
return { pos, m_renderSize };
}
private:
struct Group
{
size_t begin;
int64_t count;
double width;
double xAdvance;
};
Font& m_font;
String m_text;
SizeF m_renderSize;
Array<Glyph> m_glyphs;
Array<size_t> m_newLinePositions;
Array<double> m_lineWidthList;
void drawLine(size_t beginIdx, size_t endIdx, Vec2 pos, ColorF color)
{
// 文字の描画
for (size_t idx : Range(beginIdx, endIdx))
{
Glyph& glyph = m_glyphs[idx];
if (std::find(newLineChars.cbegin(), newLineChars.cend(), glyph.codePoint) == newLineChars.cend())
{
glyph.texture
.draw(pos + Vec2(glyph.left, -glyph.top), color);
pos.x += glyph.xAdvance;
}
}
}
};
void Main()
{
Window::SetStyle(WindowStyle::Sizable);
Scene::SetBackground(ColorF(0.6));
constexpr StringView targetFileName = U"text.txt";
const String targetFileDir = FileSystem::CurrentDirectory();
const String targetFilePath = Format(targetFileDir, targetFileName);
Font font(20);
Stopwatch stw;
MillisecondsF setTextTime;
MillisecondsF setWidthTime;
MillisecondsF drawTime;
TextRenderer renderer(font);
{
String text;
if (FileSystem::Exists(targetFilePath))
{
text = TextReader(targetFilePath).readAll();
}
else
{
text = TextReader(U"example/txt/en.txt").readAll();
TextWriter(targetFilePath).write(text);
}
stw.restart();
renderer.setText(text);
setTextTime = stw.elapsed();
}
DirectoryWatcher watcher(targetFileDir);
Size sceneSize = Scene::Size();
RectF drawArea = {
20,
40,
sceneSize.x - 40,
sceneSize.y - 60
};
bool dragging = false;
bool paramChanged = true;
double textHeight = 0.0;
Vec2 start = { 10, 20 };
Vec2 end = { 20, 4 };
Vec2 diff = end - start;
while (System::Update())
{
// ファイルの更新を監視
{
Array<FileChange> changes;
if (watcher.retrieveChanges(changes) &&
changes.includes_if([&](const FileChange& change)
{
switch (change.action)
{
case FileAction::Added:
case FileAction::Modified:
return change.path == targetFilePath;
default:
return false;
}
})
)
{
String text = TextReader(targetFilePath).readAll();
stw.restart();
renderer.setText(text);
setTextTime = stw.elapsed();
paramChanged = true;
}
}
// drawAreaのサイズ変更
if (dragging)
{
if (MouseL.pressed())
{
Cursor::RequestStyle(CursorStyle::ResizeNWSE);
auto delta = Cursor::DeltaF();
if (delta != Vec2::Zero())
{
drawArea.size += delta;
drawArea.w = Max(drawArea.w, 2.0);
drawArea.h = Max(drawArea.h, 2.0);
paramChanged = true;
}
}
else
{
dragging = false;
}
}
// Scene::Size()の監視
if (not dragging)
{
auto newSceneSize = Scene::Size();
if (newSceneSize != sceneSize)
{
sceneSize = newSceneSize;
drawArea = {
20,
40,
sceneSize.x - 40,
sceneSize.y - 60
};
paramChanged = true;
}
}
// 描画
drawArea
.draw(Palette::Whitesmoke)
.drawFrame(0, 4, Palette::Gray);
Circle grip(drawArea.br(), 10);
if (grip.mouseOver())
{
Cursor::RequestStyle(CursorStyle::ResizeNWSE);
if (MouseL.down())
{
dragging = true;
}
}
// TextRendererの更新,描画
if (paramChanged)
{
{
stw.restart();
renderer.setWidth(drawArea.w);
setWidthTime = stw.elapsed();
}
paramChanged = true;
}
RectF textRect;
{
stw.restart();
textRect = renderer.draw(drawArea.pos, Palette::Black);
drawTime = stw.elapsed();
}
textRect.drawFrame(1, 0, Palette::Red);
ClearPrint();
Print.write(U"SetText: ", setTextTime);
Print.write(U", ");
Print.write(U"SetWidth: ", setWidthTime);
Print.write(U", ");
Print.write(U"Draw: ", drawTime);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment