Files
panopainter/src/font.cpp

453 lines
15 KiB
C++

#include "pch.h"
#include "log.h"
#include "font.h"
#include "shader.h"
#include "asset.h"
#include "util.h"
#include "app.h"
#include "renderer_gl/opengl_capabilities.h"
#include <array>
#include <cstddef>
#include <cstdint>
namespace {
[[nodiscard]] GLint font_atlas_internal_format() noexcept
{
return static_cast<GLint>(pp::renderer::gl::texture_format_for_channel_count(1U).internal_format);
}
[[nodiscard]] GLint font_atlas_pixel_format() noexcept
{
return static_cast<GLint>(pp::renderer::gl::texture_format_for_channel_count(1U).pixel_format);
}
[[nodiscard]] GLenum texture_unit(std::uint32_t unit_index) noexcept
{
return static_cast<GLenum>(pp::renderer::gl::active_texture_unit(unit_index));
}
void gen_buffers_adapter(std::uint32_t count, std::uint32_t* ids) noexcept
{
glGenBuffers(static_cast<GLsizei>(count), ids);
}
void bind_buffer_adapter(std::uint32_t target, std::uint32_t buffer) noexcept
{
glBindBuffer(static_cast<GLenum>(target), static_cast<GLuint>(buffer));
}
void buffer_data_adapter(
std::uint32_t target,
std::intptr_t byte_count,
const void* data,
std::uint32_t usage) noexcept
{
glBufferData(static_cast<GLenum>(target), static_cast<GLsizeiptr>(byte_count), data, static_cast<GLenum>(usage));
}
void gen_vertex_arrays_adapter(std::uint32_t count, std::uint32_t* ids) noexcept
{
glGenVertexArrays(static_cast<GLsizei>(count), ids);
}
void bind_vertex_array_adapter(std::uint32_t vertex_array) noexcept
{
glBindVertexArray(static_cast<GLuint>(vertex_array));
}
void enable_vertex_attrib_array_adapter(std::uint32_t index) noexcept
{
glEnableVertexAttribArray(static_cast<GLuint>(index));
}
void vertex_attrib_pointer_adapter(
std::uint32_t index,
std::int32_t component_count,
std::uint32_t component_type,
std::uint8_t normalized,
std::int32_t stride,
const void* offset) noexcept
{
glVertexAttribPointer(
static_cast<GLuint>(index),
static_cast<GLint>(component_count),
static_cast<GLenum>(component_type),
static_cast<GLboolean>(normalized),
static_cast<GLsizei>(stride),
offset);
}
void draw_elements_adapter(
std::uint32_t mode,
std::int32_t count,
std::uint32_t index_type,
const void* index_offset) noexcept
{
glDrawElements(
static_cast<GLenum>(mode),
static_cast<GLsizei>(count),
static_cast<GLenum>(index_type),
index_offset);
}
void draw_arrays_adapter(std::uint32_t mode, std::int32_t first, std::int32_t count) noexcept
{
glDrawArrays(static_cast<GLenum>(mode), static_cast<GLint>(first), static_cast<GLsizei>(count));
}
[[nodiscard]] std::span<const pp::renderer::gl::OpenGlVertexAttribute> text_mesh_vertex_attributes() noexcept
{
static const std::array<pp::renderer::gl::OpenGlVertexAttribute, 2> attributes {
pp::renderer::gl::OpenGlVertexAttribute {
.index = 0U,
.component_count = 2,
.component_type = pp::renderer::gl::vertex_attribute_float_component_type(),
.normalized = static_cast<std::uint8_t>(pp::renderer::gl::vertex_attribute_not_normalized()),
.stride = static_cast<std::int32_t>(sizeof(glm::vec4)),
.offset = 0U,
},
pp::renderer::gl::OpenGlVertexAttribute {
.index = 1U,
.component_count = 2,
.component_type = pp::renderer::gl::vertex_attribute_float_component_type(),
.normalized = static_cast<std::uint8_t>(pp::renderer::gl::vertex_attribute_not_normalized()),
.stride = static_cast<std::int32_t>(sizeof(glm::vec4)),
.offset = static_cast<std::uintptr_t>(sizeof(float) * 2),
},
};
return attributes;
}
[[nodiscard]] pp::renderer::gl::OpenGlMeshCreateDispatch text_mesh_create_dispatch() noexcept
{
return pp::renderer::gl::OpenGlMeshCreateDispatch {
.gen_buffers = gen_buffers_adapter,
.bind_buffer = bind_buffer_adapter,
.buffer_data = buffer_data_adapter,
.gen_vertex_arrays = gen_vertex_arrays_adapter,
.bind_vertex_array = bind_vertex_array_adapter,
.enable_vertex_attrib_array = enable_vertex_attrib_array_adapter,
.vertex_attrib_pointer = vertex_attrib_pointer_adapter,
};
}
[[nodiscard]] pp::renderer::gl::OpenGlBufferUploadDispatch text_buffer_upload_dispatch() noexcept
{
return pp::renderer::gl::OpenGlBufferUploadDispatch {
.bind_buffer = bind_buffer_adapter,
.buffer_data = buffer_data_adapter,
};
}
[[nodiscard]] pp::renderer::gl::OpenGlMeshDrawDispatch text_mesh_draw_dispatch() noexcept
{
return pp::renderer::gl::OpenGlMeshDrawDispatch {
.bind_vertex_array = bind_vertex_array_adapter,
.draw_elements = draw_elements_adapter,
.draw_arrays = draw_arrays_adapter,
};
}
}
std::map<std::string, Font> FontManager::m_fonts;
Sampler FontManager::m_sampler;
Font::Font(Font&& other) noexcept
{
w = other.w;
h = other.h;
bounds = other.bounds;
size = other.size;
scale = other.size;
font_tex = std::move(other.font_tex);
path = std::move(other.path);
id = std::move(other.id);
chars = std::move(other.chars);
}
void Font::operator=(Font&& other) noexcept
{
w = other.w;
h = other.h;
bounds = other.bounds;
size = other.size;
scale = other.size;
font_tex = std::move(other.font_tex);
path = std::move(other.path);
id = std::move(other.id);
chars = std::move(other.chars);
}
bool Font::load(const std::string& ttf, int font_size, float font_scale)
{
Asset file;
LOG("Font::load %s", ttf.c_str());
if (file.open(ttf.c_str()) && file.read_all())
{
w = h = 512 * (int)ceilf(font_scale);
path = ttf;
scale = font_scale;
LOG("Font::load loaded");
auto bitmap = std::make_unique<uint8_t[]>(w*h);
chars.resize(num_chars);
//int offset = stbtt_FindMatchingFont(file.m_data, "Arial Bold", STBTT_MACSTYLE_DONTCARE);
//if (offset < 0)
// offset = 0;
stbtt_BakeFontBitmap(file.m_data, 0, (float)font_size*scale, bitmap.get(), w, h, start_char, num_chars, chars.data());
calc_bounds();
font_tex.create(w, h, font_atlas_internal_format(), font_atlas_pixel_format(), bitmap.get());
file.close();
size = font_size;
return true;
}
return false;
}
void Font::change_scale(float scale)
{
load(path, size, scale);
}
void Font::calc_bounds()
{
glm::vec2 bbmin(FLT_MAX);
glm::vec2 bbmax(-FLT_MAX);
for (int i = 0; i < num_chars; i++)
{
stbtt_aligned_quad q;
float x = 0, y = 0;
stbtt_GetBakedQuad(chars.data(), w, h, i, &x, &y, &q, true);
bbmin = glm::min(bbmin, { q.x0 / scale, q.y0 / scale });
bbmax = glm::max(bbmax, { q.x1 / scale, q.y1 / scale });
}
bounds = { glm::vec4(bbmin, bbmax) };
}
void FontManager::init()
{
m_sampler.create();
}
bool FontManager::load(const std::string& id, const std::string& ttf, int sz, float scale)
{
return m_fonts[id].load(ttf, sz, scale);
}
const Font& FontManager::get(const std::string& name, int size, const std::string& weight, bool italic)
{
auto id = fmt::format("{}-{}{}-{}", name, weight, italic ? "-italic" : "", size);
auto it = m_fonts.find(id);
if (it == m_fonts.end())
{
LOG("load font %s", id.c_str());
auto path = fmt::format("data/fonts/{}-{}{}.ttf", name, weight, italic ? "-italic" : "");
m_fonts[id].load(path, size, App::I->display_density * App::I->zoom);
return m_fonts[id];
}
return it->second;
}
void FontManager::change_scale(float scale)
{
for (auto& f : m_fonts)
f.second.change_scale(scale);
}
std::vector<TextMesh::Token> TextMesh::tokenize(const std::string& s, const Font& f) const noexcept
{
std::vector<std::string> parts;
std::array<char, 10> delims = { ' ', '\n', ',', '.', ':', ';', '?', '!', '\\', '/' };
std::string tmp;
bool wrap = false;
for (char c : s)
{
bool is_delim = std::find(delims.begin(), delims.end(), c) != delims.end();
wrap |= is_delim; // set wrap to notify a delimiter has been reached
// when a new non-delim char is detected, start a new token
if (wrap && !is_delim && !tmp.empty())
{
parts.push_back(tmp);
tmp.clear();
wrap = false;
}
tmp.push_back(c);
}
// insert last partial token
if (!tmp.empty())
parts.push_back(tmp);
// measure each token length
std::vector<TextMesh::Token> ret;
for (auto p : parts)
{
float x = 0;
float y = 0;
for (char c : p)
{
if ((uint8_t)c >= 32 && (uint8_t)c < 256) // visible ascii character
{
stbtt_aligned_quad q;
stbtt_GetBakedQuad((stbtt_bakedchar*)f.chars.data(), f.w, f.h, (uint8_t)c - f.start_char, &x, &y, &q, true);
}
}
ret.emplace_back(p, x);
}
return ret;
}
bool TextMesh::create()
{
App::I->render_task([this]
{
const auto mesh = pp::renderer::gl::create_opengl_mesh_objects(
pp::renderer::gl::OpenGlMeshUpload {
.vertex_data = nullptr,
.vertex_byte_count = 0,
.index_data = nullptr,
.index_byte_count = 0,
.indexed = true,
.vertex_array_count = 1U,
.attributes = text_mesh_vertex_attributes(),
},
text_mesh_create_dispatch());
if (mesh.ok()) {
font_buffers[0] = static_cast<GLuint>(mesh.value().vertex_buffer);
font_buffers[1] = static_cast<GLuint>(mesh.value().index_buffer);
font_array = static_cast<GLuint>(mesh.value().vertex_arrays[0]);
}
});
return true;
}
void TextMesh::update(const std::string& text, const std::string& font, int size, const std::string& weight, bool italic)
{
this->font = font;
this->size = size;
this->weight = weight;
this->italic = italic;
auto& f = FontManager::get(font, size, weight, italic);
float spacing = (f.bounds.w - f.bounds.y);
float avg_width = f.bounds.z - f.bounds.x;
cur_box = glm::vec4(0, f.bounds.y, 5, spacing);
glm::vec2 bbmin(FLT_MAX);
glm::vec2 bbmax(-FLT_MAX);
if (text.empty())
{
bbmin = { 0, -spacing * 0.5f };
bbmax = { 1, +spacing * 0.5f };
}
if (f.chars.size())
{
float x = 0;
float y = 0;
std::vector<glm::vec4> v;
std::vector<GLushort> idx;
std::vector<Token> parts = max_width > 0 ?
tokenize(text, f) :
std::vector<Token>{ Token(text, 0.f) };
for (size_t i = 0; i < parts.size(); i++)
{
const auto& p = parts[i];
if (i > 0 && max_width > 0 && (x + p.w) > max_width * f.scale)
{
x = 0;
y += spacing * f.scale;
}
for (char c : p.s)
{
if (c == '\n')
{
x = 0;
y += spacing * f.scale;
cur_box = glm::vec4(x, y / f.scale + f.bounds.y, 5, spacing);
continue;
}
stbtt_aligned_quad q;
float wrap_test_x = x;
stbtt_GetBakedQuad((stbtt_bakedchar*)f.chars.data(), f.w, f.h, (uint8_t)c - f.start_char, &wrap_test_x, &y, &q, true);
if (max_width > 0 && q.x1 > max_width * f.scale)
{
x = 0;
y += spacing * f.scale;
}
stbtt_GetBakedQuad((stbtt_bakedchar*)f.chars.data(), f.w, f.h, (uint8_t)c - f.start_char, &x, &y, &q, true);
auto n = (int)v.size();
v.emplace_back(q.x0 / f.scale, q.y1 / f.scale, q.s0, q.t1);
v.emplace_back(q.x0 / f.scale, q.y0 / f.scale, q.s0, q.t0);
v.emplace_back(q.x1 / f.scale, q.y0 / f.scale, q.s1, q.t0);
v.emplace_back(q.x1 / f.scale, q.y1 / f.scale, q.s1, q.t1);
idx.push_back(n + 0);
idx.push_back(n + 1);
idx.push_back(n + 2);
idx.push_back(n + 0);
idx.push_back(n + 2);
idx.push_back(n + 3);
bbmin = glm::min(bbmin, glm::floor(xy(f.bounds)));
bbmax = glm::max(bbmax, glm::ceil(glm::vec2(q.x1 / f.scale, y / f.scale + f.bounds.w)));
if (max_width > 0 && q.x1 / f.scale + 5 > max_width * f.scale)
cur_box = glm::vec4(0, (y + spacing * f.scale) / f.scale + f.bounds.y, 5, spacing);
else
cur_box = glm::vec4(q.x1 / f.scale + 2, y / f.scale + f.bounds.y, 5, spacing);
}
}
for (auto& vi : v)
vi -= glm::vec4(xy(f.bounds), 0, 0);
cur_box -= glm::vec4(xy(f.bounds), 0, 0);
bb = bbmax - bbmin;
font_array_count = (int)idx.size();
App::I->render_task([&]
{
(void)pp::renderer::gl::upload_opengl_buffer_data(
pp::renderer::gl::OpenGlBufferUpload {
.target = pp::renderer::gl::element_array_buffer_target(),
.buffer_id = font_buffers[1],
.data = idx.data(),
.byte_count = static_cast<std::intptr_t>(idx.size() * sizeof(GLushort)),
.usage = pp::renderer::gl::static_draw_buffer_usage(),
},
text_buffer_upload_dispatch());
(void)pp::renderer::gl::upload_opengl_buffer_data(
pp::renderer::gl::OpenGlBufferUpload {
.target = pp::renderer::gl::array_buffer_target(),
.buffer_id = font_buffers[0],
.data = v.data(),
.byte_count = static_cast<std::intptr_t>(v.size() * sizeof(glm::vec4)),
.usage = pp::renderer::gl::static_draw_buffer_usage(),
},
text_buffer_upload_dispatch());
});
}
}
void TextMesh::draw()
{
auto& f = FontManager::get(font, size, weight, italic);
if (f.font_tex.ready())
{
glActiveTexture(texture_unit(0U));
f.font_tex.bind();
FontManager::m_sampler.bind(0);
(void)pp::renderer::gl::draw_opengl_mesh(
pp::renderer::gl::OpenGlMeshDraw {
.vertex_array = font_array,
.mode = pp::renderer::gl::primitive_mode_for_fill_count(3U),
.count = font_array_count,
.indexed = true,
.index_type = pp::renderer::gl::index_type_for_index_size(sizeof(GLushort)),
.index_offset = nullptr,
},
text_mesh_draw_dispatch());
f.font_tex.unbind();
FontManager::m_sampler.unbind();
}
}