MayaFlux 1.0.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
TypeSetter.hpp
Go to the documentation of this file.
1#pragma once
2
3#include "GlyphAtlas.hpp"
4
6
7/**
8 * @struct GlyphQuad
9 * @brief Screen-space quad for one rasterized glyph.
10 *
11 * All coordinates are in pixels relative to the pen origin passed to
12 * lay_out(). The caller is responsible for converting to NDC or whatever
13 * coordinate system the render path expects.
14 *
15 * UV coordinates are in normalised [0, 1] atlas space, matching the values
16 * stored in GlyphMetrics.
17 */
18struct GlyphQuad {
19 float x0, y0; ///< Top-left pixel position.
20 float x1, y1; ///< Bottom-right pixel position.
21 float uv_x0, uv_y0; ///< Top-left UV in atlas space.
22 float uv_x1, uv_y1; ///< Bottom-right UV in atlas space.
23 uint32_t codepoint { 0 }; ///< Unicode codepoint that produced this quad.
24 size_t byte_offset { 0 }; ///< Byte offset of codepoint's first byte in the source text passed to lay_out().
25};
26
27/**
28 * @brief Result of lay_out(), carrying the quads and the final pen position.
29 *
30 * final_pen_x and final_pen_y are the pen coordinates immediately after the
31 * last codepoint was processed. impress() uses these directly to update the
32 * TextBuffer cursor, avoiding re-deriving the position from quad geometry
33 * (which is incorrect after a newline resets pen_x mid-run).
34 */
36 std::vector<GlyphQuad> quads;
37 float final_pen_x { 0.F };
38 float final_pen_y { 0.F };
39};
40
41/**
42 * @brief Lay out a UTF-8 string into a sequence of screen-space quads.
43 *
44 * Every codepoint this text needs is rasterized (cached, if not already)
45 * before any quad is emitted, so a GlyphAtlas growth triggered partway
46 * through this text cannot invalidate the UV coordinates already baked into
47 * quads emitted earlier in the same call. This guarantee is per-call only:
48 * a LayoutResult held across later, unrelated lay_out() calls on the same
49 * atlas is not protected if the atlas grows afterward from something else.
50 *
51 * Every codepoint for which is_control() is true is never rasterized or
52 * counted as printable content. Among those, \t advances pen_x to the next
53 * tab stop and \n advances pen_y by atlas.line_height() and resets pen_x;
54 * every other control codepoint (including \r) is skipped with no side
55 * effect. This is the same is_control() identification raw-string text
56 * handling uses; only the reaction to it differs here.
57 *
58 * Consecutive non-control glyphs are kerned via FT_Get_Kerning() when the
59 * producing face (GlyphMetrics::face) has a kerning table. The previous
60 * glyph index resets to none across any control codepoint, across a
61 * wrap-induced line break, and whenever the producing face changes between
62 * consecutive glyphs: a glyph index is only meaningful within the face
63 * that assigned it, so a codepoint resolved through GlyphAtlas's fallback
64 * chain never gets kerned against a glyph from a different face.
65 *
66 * Wrapping at wrap_w is a pure render-bounds guarantee, not word-aware line
67 * breaking: it breaks mid-word freely, with no whitespace lookback. This is
68 * deliberate - correctness here means no glyph ever renders past wrap_w, not
69 * that words stay intact. Each glyph is checked before it is placed, against
70 * its own advance width times a 1.5x margin, so the decision to wrap accounts
71 * for the glyph about to be drawn rather than reacting one glyph late to
72 * cumulative pen_x history - the margin absorbs the gap between a glyph's
73 * advance and its true rendered width so a glyph is never placed half past
74 * the boundary. A tab is checked the same way against its own tab-stop
75 * advance (no margin, since it draws nothing itself) immediately after
76 * computing its destination, not deferred to the next glyph's turn.
77 *
78 * Bidirectional reordering and shaping are not performed. HarfBuzz slots in
79 * before the glyph index step when needed; the quad assembly loop and
80 * GlyphAtlas remain unchanged.
81 *
82 * Each quad's byte_offset is the position of its codepoint's first byte in
83 * @p text, letting a caller correlate a quad back to source text (styling,
84 * hit-testing, extraction) without re-decoding UTF-8 itself.
85 *
86 * @param text UTF-8 encoded input string.
87 * @param atlas GlyphAtlas to query and populate. May be modified (dirty
88 * flag set) if new glyphs are rasterized.
89 * @param pen_x Starting horizontal pen position in pixels.
90 * @param pen_y Starting vertical pen position in pixels (baseline).
91 * @param wrap_w Column to wrap at in pixels, or 0 to disable wrapping.
92 * @param tab_width Tab stop width in space-glyph-widths.
93 * @return LayoutResult containing quads and final pen position.
94 */
95[[nodiscard]] MAYAFLUX_API LayoutResult lay_out(
96 std::string_view text,
97 GlyphAtlas& atlas,
98 float pen_x = 0.F,
99 float pen_y = 0.F,
100 uint32_t wrap_w = 0,
101 uint32_t tab_width = 4);
102
103/**
104 * @brief True if a codepoint must never be rasterized or treated as
105 * printable content.
106 *
107 * Covers Unicode categories Cc (control), Cf (format), Cs (surrogate), and
108 * Co (private use) via utf8proc_category(). This is the one fact every
109 * Portal::Text consumer (layout, raw-string editing) shares about a
110 * codepoint; what each does in response - skip, reposition a pen, refuse an
111 * insertion - is theirs to decide.
112 *
113 * @param codepoint Unicode codepoint to classify.
114 * @return true if the codepoint must never produce a glyph.
115 */
116[[nodiscard]] MAYAFLUX_API bool is_control(uint32_t codepoint) noexcept;
117
118/**
119 * @brief Encode a Unicode codepoint as a UTF-8 byte sequence.
120 *
121 * Counterpart to the decode step lay_out() performs via utf8proc_iterate.
122 * Invalid codepoints (surrogates, > 0x10FFFF) return an empty string.
123 *
124 * @param codepoint Unicode codepoint to encode.
125 * @return 1-4 byte UTF-8 sequence, or empty on invalid input.
126 */
127[[nodiscard]] MAYAFLUX_API std::string encode_utf8(uint32_t codepoint);
128
129/**
130 * @brief Byte offset of the codepoint boundary after @p byte_offset.
131 *
132 * Steps forward exactly one codepoint using the same utf8proc decode lay_out()
133 * uses, so malformed sequences are skipped identically rather than by a
134 * separate continuation-byte mask. Returns text.size() if already at or past
135 * the end.
136 *
137 * @param text UTF-8 encoded string.
138 * @param byte_offset Current byte offset, must be a codepoint boundary.
139 * @return Byte offset of the next codepoint boundary.
140 */
141[[nodiscard]] MAYAFLUX_API size_t next_codepoint_offset(std::string_view text, size_t byte_offset);
142
143/**
144 * @brief Byte offset of the codepoint boundary before @p byte_offset.
145 *
146 * Walks backward over continuation-pattern bytes to the start of the
147 * preceding codepoint. Returns 0 if @p byte_offset is already 0.
148 *
149 * @param text UTF-8 encoded string.
150 * @param byte_offset Current byte offset, must be a codepoint boundary.
151 * @return Byte offset of the previous codepoint boundary.
152 */
153[[nodiscard]] MAYAFLUX_API size_t previous_codepoint_offset(std::string_view text, size_t byte_offset);
154
155/**
156 * @struct CopiedText
157 * @brief Result of copy(): a byte range's text plus the quads that render it.
158 */
160 std::string text; ///< Substring of the source text in [start, end).
161 size_t start { 0 }; ///< Normalized, clamped byte range actually used.
162 size_t end { 0 };
163 std::vector<GlyphQuad> quads; ///< Matching quads, at their original layout positions.
164};
165
166/**
167 * @brief Copy a byte range: the text it spans, and the quads that render it.
168 *
169 * start/end are normalized (min/max) and clamped to [0, text.size()]. Both
170 * are assumed to already be codepoint boundaries, the same assumption
171 * next_codepoint_offset()/previous_codepoint_offset() maintain for a
172 * caller's cursor; this does not re-snap them to one.
173 *
174 * Quads are taken from @p layout, matched by GlyphQuad::byte_offset falling
175 * in the normalized range, at whatever position they were laid out at.
176 * Shift them yourself if the copy needs to be positioned relative to its
177 * own origin rather than the source layout's. @p layout must be the result
178 * of laying out this same @p text (or one whose byte offsets line up with
179 * it); passing a layout for unrelated text returns a meaningless, but not
180 * unsafe, quads list.
181 *
182 * @param text Source UTF-8 string the byte range indexes into.
183 * @param layout LayoutResult produced from that same text.
184 * @param a One end of the byte range.
185 * @param b The other end of the byte range.
186 * @return The extracted text, its normalized range, and matching quads.
187 */
188[[nodiscard]] MAYAFLUX_API CopiedText copy(
189 std::string_view text,
190 const LayoutResult& layout,
191 size_t a,
192 size_t b);
193
194/**
195 * @struct PenPosition
196 * @brief Pen coordinates returned by x_at(), in the same space as GlyphQuad.
197 */
199 float x { 0.F };
200 float y { 0.F };
201};
202
203/**
204 * @brief Byte offset of the codepoint boundary nearest a screen-space point.
205 *
206 * Scans layout.quads for the line whose quads sit closest to @p y, then
207 * within that line for the ink quad @p x falls into (resolving to its left
208 * or right half), or, failing that, for the two quads bracketing @p x.
209 * lay_out() never emits a quad for a space, tab, or newline, so a point
210 * landing in the gap between two quads - or before the first / after the
211 * last quad on a line - has no ink to test against directly: that gap is
212 * resolved exactly by walking its few codepoint boundaries through x_at()
213 * (same pen_x/pen_y/wrap_w/tab_width as the original lay_out() call) and
214 * keeping whichever pen position lands closest to @p x, rather than
215 * guessing from ink edges that were never meant to mark it.
216 *
217 * A line with no quads at all (blank, or entirely whitespace) has nothing
218 * for the first scan to anchor on and is still not distinguishable from an
219 * adjacent line by this function - a real, if narrower, residual gap.
220 * Empty layout.quads (the whole text is blank or all-whitespace) returns 0.
221 *
222 * @param text Source UTF-8 string layout was produced from.
223 * @param layout LayoutResult to hit-test against.
224 * @param atlas GlyphAtlas layout was produced with.
225 * @param x Screen-space x coordinate, same space as layout's quads.
226 * @param y Screen-space y coordinate, same space as layout's quads.
227 * @param pen_x Pen origin x, matching the original lay_out() call.
228 * @param pen_y Pen origin y, matching the original lay_out() call.
229 * @param wrap_w Wrap column, matching the original lay_out() call.
230 * @param tab_width Tab stop width, matching the original lay_out() call.
231 * @return Byte offset of the nearest codepoint boundary.
232 */
233[[nodiscard]] MAYAFLUX_API size_t index_at(
234 std::string_view text,
235 const LayoutResult& layout,
236 GlyphAtlas& atlas,
237 float x,
238 float y,
239 float pen_x = 0.F,
240 float pen_y = 0.F,
241 uint32_t wrap_w = 0,
242 uint32_t tab_width = 4);
243
244/**
245 * @brief Pen position immediately before @p byte_offset, in lay_out()'s space.
246 *
247 * Re-lays-out the text up to @p byte_offset (clamped to text.size()) with
248 * the same pen origin, wrap_w, and tab_width the caller used to produce the
249 * original layout, and returns the resulting final_pen_x/final_pen_y. Wrap
250 * decisions before a given offset only depend on codepoints before it, so
251 * this reproduces the exact pen position lay_out() would have held at that
252 * point in the original call - including on a space, tab, newline, wrapped
253 * line start, or the very end of the text, none of which index_at()'s
254 * quad-based sibling has a quad to answer from.
255 *
256 * This re-walks the prefix on every call rather than caching intermediate
257 * pen positions; fine for interactive caret placement, not meant for a tight
258 * per-frame loop over long text.
259 *
260 * @param text Source UTF-8 string to re-lay-out a prefix of.
261 * @param atlas GlyphAtlas to query (same one the original layout used).
262 * @param byte_offset Codepoint-boundary byte offset to resolve.
263 * @param pen_x Pen origin x, matching the original lay_out() call.
264 * @param pen_y Pen origin y, matching the original lay_out() call.
265 * @param wrap_w Wrap column, matching the original lay_out() call.
266 * @param tab_width Tab stop width, matching the original lay_out() call.
267 * @return Pen position immediately before byte_offset.
268 */
269[[nodiscard]] MAYAFLUX_API PenPosition x_at(
270 std::string_view text,
271 GlyphAtlas& atlas,
272 size_t byte_offset,
273 float pen_x = 0.F,
274 float pen_y = 0.F,
275 uint32_t wrap_w = 0,
276 uint32_t tab_width = 4);
277
278} // namespace MayaFlux::Portal::Text
size_t a
size_t b
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.
std::string text
Substring of the source text in [start, end).
size_t start
Normalized, clamped byte range actually used.
std::vector< GlyphQuad > quads
Matching quads, at their original layout positions.
Result of copy(): a byte range's text plus the quads that render it.
float y0
Top-left pixel position.
float uv_y1
Bottom-right UV in atlas space.
float uv_y0
Top-left UV in atlas space.
size_t byte_offset
Byte offset of codepoint's first byte in the source text passed to lay_out().
float y1
Bottom-right pixel position.
uint32_t codepoint
Unicode codepoint that produced this quad.
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.