summaryrefslogtreecommitdiff
path: root/ui/font.cpp
diff options
context:
space:
mode:
authorDan Goodliffe <dan@randomdan.homeip.net>2022-01-01 13:01:08 +0000
committerDan Goodliffe <dan@randomdan.homeip.net>2022-01-01 14:40:06 +0000
commit1f68fe78e84f25c8ddacdc37a293a5de31725bab (patch)
tree4537db483391b579387c1bcc00c3f1e3b6adc106 /ui/font.cpp
parentAdd glm::vec concatenation operator|| (diff)
downloadilt-1f68fe78e84f25c8ddacdc37a293a5de31725bab.tar.bz2
ilt-1f68fe78e84f25c8ddacdc37a293a5de31725bab.tar.xz
ilt-1f68fe78e84f25c8ddacdc37a293a5de31725bab.zip
First iteration with font/text support
Diffstat (limited to 'ui/font.cpp')
-rw-r--r--ui/font.cpp182
1 files changed, 182 insertions, 0 deletions
diff --git a/ui/font.cpp b/ui/font.cpp
new file mode 100644
index 0000000..5596f20
--- /dev/null
+++ b/ui/font.cpp
@@ -0,0 +1,182 @@
+#include "font.h"
+#include <algorithm>
+#include <cctype>
+#include <ft2build.h>
+#include FT_FREETYPE_H
+#include <glRef.hpp>
+#include <maths.h>
+#include <memory>
+#include <optional>
+#include <stdexcept>
+#include <unicode.h>
+#include <utility>
+// IWYU pragma: no_forward_declare FT_LibraryRec_
+
+std::string
+FT_Error_StringSafe(FT_Error err)
+{
+ if (const auto errstr = FT_Error_String(err)) {
+ return {errstr};
+ }
+ return std::to_string(err);
+}
+
+template<auto Func, typename... Args>
+void
+FT_Check(Args &&... args)
+{
+ if (const auto err = Func(std::forward<Args>(args)...)) {
+ throw std::runtime_error {std::string {"FreeType error: "} + FT_Error_StringSafe(err)};
+ }
+}
+
+const std::string BASIC_CHARS = []() {
+ std::string chars;
+ for (char c {}; c >= 0; c++) {
+ if (isgraph(c)) {
+ chars += c;
+ }
+ }
+ return chars + "£€²³";
+}();
+
+using FT = glRef<FT_Library,
+ []() {
+ FT_Library ft {};
+ FT_Check<FT_Init_FreeType>(&ft);
+ return ft;
+ },
+ FT_Done_FreeType>;
+
+using Face = glRef<FT_Face,
+ [](FT_Library ft, const char * const name) {
+ FT_Face face {};
+ FT_Check<FT_New_Face>(ft, name, 0, &face);
+ return face;
+ },
+ FT_Done_Face>;
+
+Font::Font(const char * const p, unsigned s) : path {p}, size {getTextureSize(s)}
+{
+ generateChars(BASIC_CHARS);
+}
+
+void
+Font::generateChars(const std::string_view chars) const
+{
+ std::optional<FT> ft;
+ std::optional<Face> face;
+
+ for (auto c = chars.data(); c <= &chars.back(); c = next_char(c)) {
+ const auto codepoint = get_codepoint(c);
+ if (charsData.find(codepoint) == charsData.end()) {
+ if (!ft) {
+ ft.emplace();
+ }
+ if (!face) {
+ face.emplace(*ft, path.c_str());
+ FT_Set_Pixel_Sizes(*face, 0, size.z);
+ }
+ FT_UInt glyph_index = FT_Get_Char_Index(*face, codepoint);
+ if (FT_Load_Glyph(*face, glyph_index, FT_LOAD_RENDER)) {
+ charsData.emplace(codepoint, CharData {});
+ continue;
+ }
+
+ const auto & glyph = (*face)->glyph;
+ const auto textureIdx = getTextureWithSpace(glyph->bitmap.width);
+ auto & texture = fontTextures[textureIdx];
+
+ glTexSubImage2D(GL_TEXTURE_2D, 0, static_cast<GLint>(texture.used), 0,
+ static_cast<GLsizei>(glyph->bitmap.width), static_cast<GLsizei>(glyph->bitmap.rows), GL_RED,
+ GL_UNSIGNED_BYTE, glyph->bitmap.buffer);
+
+ const auto & cd = charsData
+ .emplace(codepoint,
+ CharData {textureIdx, {glyph->bitmap.width, glyph->bitmap.rows},
+ {texture.used, 0}, {glyph->bitmap_left, glyph->bitmap_top},
+ glyph->advance.x >> 6})
+ .first->second;
+ texture.used += cd.size.x;
+ }
+ }
+}
+
+std::size_t
+Font::getTextureWithSpace(unsigned int adv) const
+{
+ if (auto itr = std::find_if(fontTextures.begin(), fontTextures.end(),
+ [adv, this](const FontTexture & ft) {
+ return (ft.used + adv) < size.x;
+ });
+ itr != fontTextures.end()) {
+ glBindTexture(GL_TEXTURE_2D, itr->texture);
+ return static_cast<std::size_t>(itr - fontTextures.begin());
+ }
+
+ auto & texture = fontTextures.emplace_back();
+ glGenTextures(1, &texture.texture);
+ glBindTexture(GL_TEXTURE_2D, texture.texture);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, static_cast<GLsizei>(size.x), static_cast<GLsizei>(size.y), 0, GL_RED,
+ GL_UNSIGNED_BYTE, nullptr);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+
+ return fontTextures.size() - 1;
+}
+
+glm::uvec3
+Font::getTextureSize(unsigned int height)
+{
+ auto pow2 = [](unsigned int target) {
+ unsigned int v {8};
+ do {
+ v <<= 1;
+ } while (v << 1 < GL_MAX_TEXTURE_SIZE && v < target);
+ return v;
+ };
+
+ constexpr const unsigned int WIDTH_PER_HEIGHT {64};
+ return {pow2(height * WIDTH_PER_HEIGHT), pow2(height), height};
+}
+
+Font::TextureQuads
+Font::render(const std::string_view chars) const
+{
+ constexpr static const std::array<std::pair<glm::vec2, glm::vec2>, 4> C {{
+ {{0, 0}, {0, 1}},
+ {{1, 0}, {1, 1}},
+ {{1, 1}, {1, 0}},
+ {{0, 1}, {0, 0}},
+ }};
+
+ generateChars(chars);
+
+ glm::vec2 pos {};
+ TextureQuads out;
+ for (auto c = chars.data(); c <= &chars.back(); c = next_char(c)) {
+ if (isspace(*c)) {
+ pos.x += static_cast<float>(size.y) / 4.F;
+ continue;
+ }
+ const auto & ch = charsData.at(get_codepoint(c));
+ if (!ch.advance) {
+ continue;
+ }
+
+ const auto charPos = pos + glm::vec2 {ch.bearing.x, ch.bearing.y - static_cast<int>(ch.size.y)};
+ const auto size = glm::vec2 {ch.size};
+
+ Quad q;
+ std::transform(C.begin(), C.end(), q.begin(), [&size, &charPos, &ch, this](const auto & c) {
+ return (charPos + (size * c.first))
+ || ((glm::vec2 {ch.position} + (glm::vec2 {ch.size} * c.second)) / glm::vec2 {this->size});
+ });
+ out[fontTextures[ch.textureIdx].texture].emplace_back(q);
+
+ pos.x += static_cast<float>(ch.advance);
+ }
+ return out;
+}