MayaFlux 1.0.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
TypeSetter.cpp
Go to the documentation of this file.
1#include "TypeSetter.hpp"
2
4
5#include <utf8proc.h>
6
8
9bool is_control(uint32_t codepoint) noexcept
10{
11 switch (utf8proc_category(static_cast<utf8proc_int32_t>(codepoint))) {
12 case UTF8PROC_CATEGORY_CC:
13 case UTF8PROC_CATEGORY_CF:
14 case UTF8PROC_CATEGORY_CS:
15 case UTF8PROC_CATEGORY_CO:
16 return true;
17 default:
18 return false;
19 }
20}
21
23 std::string_view text,
24 GlyphAtlas& atlas,
25 float pen_x,
26 float pen_y,
27 uint32_t wrap_w,
28 uint32_t tab_width)
29{
30 if (text.empty()) {
31 return { .quads = {}, .final_pen_x = pen_x, .final_pen_y = pen_y };
32 }
33
35 out.quads.reserve(text.size());
36
37 const float origin_x = 0.F;
38
39 {
40 const auto* prewarm_bytes = reinterpret_cast<const utf8proc_uint8_t*>(text.data());
41 auto prewarm_remaining = static_cast<utf8proc_ssize_t>(text.size());
42 utf8proc_ssize_t prewarm_offset = 0;
43
44 while (prewarm_offset < prewarm_remaining) {
45 utf8proc_int32_t prewarm_codepoint = 0;
46 const utf8proc_ssize_t n = utf8proc_iterate(
47 prewarm_bytes + prewarm_offset, prewarm_remaining - prewarm_offset, &prewarm_codepoint);
48
49 if (n <= 0) {
50 prewarm_offset += 1;
51 continue;
52 }
53 prewarm_offset += n;
54
55 if (prewarm_codepoint >= 0 && !is_control(static_cast<uint32_t>(prewarm_codepoint))) {
56 atlas.get_or_rasterize(static_cast<FT_ULong>(prewarm_codepoint));
57 }
58 }
59
60 if (tab_width > 0) {
61 atlas.get_or_rasterize(static_cast<FT_ULong>(' '));
62 }
63 }
64
65 FT_UInt prev_glyph_index = 0;
66 const FontFace* prev_face = nullptr;
67
68 constexpr float k_wrap_margin = 1.5F;
69
70 const auto apply_wrap = [&](float upcoming_width) -> bool {
71 if (wrap_w > 0 && pen_x + upcoming_width * k_wrap_margin > static_cast<float>(wrap_w)) {
72 pen_x = origin_x;
73 pen_y += static_cast<float>(atlas.line_height());
74 prev_glyph_index = 0;
75 prev_face = nullptr;
76 return true;
77 }
78 return false;
79 };
80
81 const auto* bytes = reinterpret_cast<const utf8proc_uint8_t*>(text.data());
82 auto remaining = static_cast<utf8proc_ssize_t>(text.size());
83 utf8proc_ssize_t offset = 0;
84
85 while (offset < remaining) {
86 const auto codepoint_start = static_cast<size_t>(offset);
87 utf8proc_int32_t codepoint = 0;
88 const utf8proc_ssize_t n = utf8proc_iterate(bytes + offset, remaining - offset, &codepoint);
89
90 if (n <= 0) {
92 "TypeSetter: invalid UTF-8 sequence at byte offset {}, skipping byte",
93 static_cast<size_t>(offset));
94 offset += 1;
95 continue;
96 }
97
98 offset += n;
99
100 if (codepoint < 0) {
101 continue;
102 }
103
104 if (is_control(static_cast<uint32_t>(codepoint))) {
105 if (codepoint == '\t' && tab_width > 0) {
106 const GlyphMetrics* space = atlas.get_or_rasterize(static_cast<FT_ULong>(' '));
107 const float stop_width = space && space->advance_x > 0
108 ? static_cast<float>(space->advance_x) * static_cast<float>(tab_width)
109 : 0.F;
110
111 if (stop_width > 0.F) {
112 pen_x = (std::floor((pen_x - origin_x) / stop_width) + 1.F) * stop_width + origin_x;
113 apply_wrap(0.F);
114 }
115 } else if (codepoint == '\n') {
116 pen_x = origin_x;
117 pen_y += static_cast<float>(atlas.line_height());
118 }
119
120 prev_glyph_index = 0;
121 prev_face = nullptr;
122 continue;
123 }
124
125 const GlyphMetrics* m = atlas.get_or_rasterize(static_cast<FT_ULong>(codepoint));
126 if (!m) {
127 prev_glyph_index = 0;
128 prev_face = nullptr;
129 continue;
130 }
131
132 apply_wrap(static_cast<float>(m->advance_x));
133
134 if (prev_face == m->face && prev_glyph_index != 0 && m->glyph_index != 0
135 && m->face && FT_HAS_KERNING(m->face->get_face())) {
136 FT_Vector delta {};
137 FT_Get_Kerning(m->face->get_face(), prev_glyph_index, m->glyph_index, FT_KERNING_DEFAULT, &delta);
138 pen_x += static_cast<float>(delta.x >> 6);
139 }
140
141 if (m->width > 0 && m->height > 0) {
142 GlyphQuad q {};
143 q.x0 = pen_x + static_cast<float>(m->bearing_x);
144 q.y0 = pen_y - static_cast<float>(m->bearing_y);
145 q.x1 = q.x0 + static_cast<float>(m->width);
146 q.y1 = q.y0 + static_cast<float>(m->height);
147 q.uv_x0 = m->uv_x0;
148 q.uv_y0 = m->uv_y0;
149 q.uv_x1 = m->uv_x1;
150 q.codepoint = static_cast<uint32_t>(codepoint);
151 q.uv_y1 = m->uv_y1;
152 q.byte_offset = codepoint_start;
153 out.quads.push_back(q);
154 }
155
156 pen_x += static_cast<float>(m->advance_x);
157 prev_glyph_index = m->glyph_index;
158 prev_face = m->face;
159 }
160
161 out.final_pen_x = pen_x;
162 out.final_pen_y = pen_y;
163 return out;
164}
165
166std::string encode_utf8(uint32_t codepoint)
167{
168 if (!utf8proc_codepoint_valid(static_cast<utf8proc_int32_t>(codepoint))) {
169 return {};
170 }
171
172 std::array<utf8proc_uint8_t, 4> buf {};
173 const utf8proc_ssize_t n = utf8proc_encode_char(
174 static_cast<utf8proc_int32_t>(codepoint), buf.data());
175
176 if (n <= 0) {
177 return {};
178 }
179
180 return { reinterpret_cast<const char*>(buf.data()), static_cast<size_t>(n) };
181}
182
183size_t next_codepoint_offset(std::string_view text, size_t byte_offset)
184{
185 if (byte_offset >= text.size()) {
186 return text.size();
187 }
188
189 const auto* bytes = reinterpret_cast<const utf8proc_uint8_t*>(text.data());
190 const auto remaining = static_cast<utf8proc_ssize_t>(text.size() - byte_offset);
191
192 utf8proc_int32_t codepoint = 0;
193 const utf8proc_ssize_t n = utf8proc_iterate(bytes + byte_offset, remaining, &codepoint);
194
195 return byte_offset + static_cast<size_t>(n > 0 ? n : 1);
196}
197
198size_t previous_codepoint_offset(std::string_view text, size_t byte_offset)
199{
200 if (byte_offset == 0) {
201 return 0;
202 }
203
204 size_t pos = byte_offset - 1;
205 constexpr size_t max_continuation_bytes = 3;
206 size_t steps = 0;
207
208 while (pos > 0 && steps < max_continuation_bytes
209 && (static_cast<uint8_t>(text[pos]) & 0xC0) == 0x80) {
210 --pos;
211 ++steps;
212 }
213
214 return pos;
215}
216
217CopiedText copy(std::string_view text, const LayoutResult& layout, size_t a, size_t b)
218{
220 out.start = std::min(std::min(a, b), text.size());
221 out.end = std::min(std::max(a, b), text.size());
222
223 if (out.start >= out.end) {
224 return out;
225 }
226
227 out.text = std::string(text.substr(out.start, out.end - out.start));
228
229 for (const auto& q : layout.quads) {
230 if (q.byte_offset >= out.start && q.byte_offset < out.end) {
231 out.quads.push_back(q);
232 }
233 }
234
235 return out;
236}
237
238namespace {
239 float abs_f(float v) { return v < 0.F ? -v : v; }
240} // namespace
241
242size_t index_at(
243 std::string_view text,
244 const LayoutResult& layout,
245 GlyphAtlas& atlas,
246 float x,
247 float y,
248 float pen_x,
249 float pen_y,
250 uint32_t wrap_w,
251 uint32_t tab_width)
252{
253 if (layout.quads.empty()) {
254 return 0;
255 }
256
257 const auto line_h = static_cast<float>(atlas.line_height());
258
259 float line_y = layout.quads.front().y0;
260 float best_line_dy = abs_f(line_y - y);
261 for (const auto& q : layout.quads) {
262 const float dy = abs_f(q.y0 - y);
263 if (dy < best_line_dy) {
264 best_line_dy = dy;
265 line_y = q.y0;
266 }
267 }
268
269 const GlyphQuad* left = nullptr;
270 const GlyphQuad* right = nullptr;
271
272 for (const auto& q : layout.quads) {
273 if (abs_f(q.y0 - line_y) > line_h * 0.5F) {
274 continue;
275 }
276
277 if (x >= q.x0 && x <= q.x1) {
278 return x > (q.x0 + q.x1) * 0.5F
279 ? next_codepoint_offset(text, q.byte_offset)
280 : q.byte_offset;
281 }
282 if (q.x1 <= x && (!left || q.x1 > left->x1)) {
283 left = &q;
284 }
285 if (q.x0 >= x && (!right || q.x0 < right->x0)) {
286 right = &q;
287 }
288 }
289
290 const GlyphQuad* anchor = left ? left : right;
291 const float pen_line_y = x_at(text, atlas, anchor->byte_offset, pen_x, pen_y, wrap_w, tab_width).y;
292
293 const auto same_line = [&](size_t offset) {
294 return x_at(text, atlas, offset, pen_x, pen_y, wrap_w, tab_width).y == pen_line_y;
295 };
296
297 size_t bracket_start = 0;
298 if (left) {
299 bracket_start = next_codepoint_offset(text, left->byte_offset);
300 } else {
301 bracket_start = right->byte_offset;
302 while (bracket_start > 0) {
303 const size_t prev = previous_codepoint_offset(text, bracket_start);
304 if (!same_line(prev)) {
305 break;
306 }
307 bracket_start = prev;
308 }
309 }
310
311 size_t bracket_end = text.size();
312 if (right) {
313 bracket_end = right->byte_offset;
314 } else {
315 bracket_end = next_codepoint_offset(text, left->byte_offset);
316 while (bracket_end < text.size()) {
317 const size_t next = next_codepoint_offset(text, bracket_end);
318 if (!same_line(next)) {
319 break;
320 }
321 bracket_end = next;
322 }
323 }
324
325 if (bracket_start >= bracket_end) {
326 return bracket_start;
327 }
328
329 size_t best_offset = bracket_start;
330 float best_dx = abs_f(x_at(text, atlas, bracket_start, pen_x, pen_y, wrap_w, tab_width).x - x);
331
332 for (size_t off = bracket_start; off < bracket_end;) {
333 const size_t next = next_codepoint_offset(text, off);
334 const float dx = abs_f(x_at(text, atlas, next, pen_x, pen_y, wrap_w, tab_width).x - x);
335 if (dx < best_dx) {
336 best_dx = dx;
337 best_offset = next;
338 }
339 off = next;
340 }
341
342 return best_offset;
343}
344
346 std::string_view text,
347 GlyphAtlas& atlas,
348 size_t byte_offset,
349 float pen_x,
350 float pen_y,
351 uint32_t wrap_w,
352 uint32_t tab_width)
353{
354 const size_t clamped = std::min(byte_offset, text.size());
355 const LayoutResult prefix = lay_out(text.substr(0, clamped), atlas, pen_x, pen_y, wrap_w, tab_width);
356 return { .x = prefix.final_pen_x, .y = prefix.final_pen_y };
357}
358
359} // namespace MayaFlux::Portal::Text
#define MF_WARN(comp, ctx,...)
uint64_t offset
Definition Composite.cpp:10
size_t a
size_t b
double q
std::vector< float > * out
FT_Face get_face() const
Raw FT_Face handle for use by GlyphAtlas.
Definition FontFace.hpp:57
Owns a single FT_Face loaded from a file path.
Definition FontFace.hpp:24
uint32_t line_height() const
Line advance in pixels for this atlas's pixel_size.
const GlyphMetrics * get_or_rasterize(FT_UInt glyph_index)
Return metrics for a glyph, rasterizing it into the atlas if needed.
Rasterizes and packs glyphs from a FontFace into a TextureContainer.
@ API
API calls from external code.
@ Portal
High-level user-facing API layer.
size_t previous_codepoint_offset(std::string_view text, size_t byte_offset)
Byte offset of the codepoint boundary before byte_offset.
std::string encode_utf8(uint32_t codepoint)
Encode a Unicode codepoint as a UTF-8 byte sequence.
bool is_control(uint32_t codepoint) noexcept
True if a codepoint must never be rasterized or treated as printable content.
Definition TypeSetter.cpp:9
size_t next_codepoint_offset(std::string_view text, size_t byte_offset)
Byte offset of the codepoint boundary after byte_offset.
PenPosition x_at(std::string_view text, GlyphAtlas &atlas, size_t byte_offset, float pen_x, float pen_y, uint32_t wrap_w, uint32_t tab_width)
Pen position immediately before byte_offset, in lay_out()'s space.
CopiedText copy(std::string_view text, const LayoutResult &layout, size_t a, size_t b)
Copy a byte range: the text it spans, and the quads that render it.
LayoutResult lay_out(std::string_view text, GlyphAtlas &atlas, float pen_x, float pen_y, uint32_t wrap_w, uint32_t tab_width)
Lay out a UTF-8 string into a sequence of screen-space quads.
size_t index_at(std::string_view text, const LayoutResult &layout, GlyphAtlas &atlas, float x, float y, float pen_x, float pen_y, uint32_t wrap_w, uint32_t tab_width)
Byte offset of the codepoint boundary nearest a screen-space point.
size_t start
Normalized, clamped byte range actually used.
Result of copy(): a byte range's text plus the quads that render it.
FT_UInt glyph_index
Glyph index within face (from FT_Get_Char_Index).
int32_t bearing_x
Horizontal bearing in pixels (from FT_GlyphSlot).
float uv_x0
Left UV edge in atlas texture.
float uv_x1
Right UV edge in atlas texture.
const FontFace * face
Face that produced this glyph (primary or fallback).
uint32_t height
Glyph bitmap height in pixels.
int32_t advance_x
Horizontal advance in pixels (26.6 fixed-point >> 6).
uint32_t width
Glyph bitmap width in pixels.
float uv_y0
Top UV edge in atlas texture.
int32_t bearing_y
Vertical bearing in pixels (from FT_GlyphSlot).
float uv_y1
Bottom UV edge in atlas texture.
Per-glyph layout and UV data produced by GlyphAtlas.
size_t byte_offset
Byte offset of codepoint's first byte in the source text passed to lay_out().
Screen-space quad for one rasterized glyph.
std::vector< GlyphQuad > quads
Result of lay_out(), carrying the quads and the final pen position.
Pen coordinates returned by x_at(), in the same space as GlyphQuad.