Unravel Engine C++ Reference
Loading...
Searching...
No Matches
text_component.cpp
Go to the documentation of this file.
1#include "text_component.h"
2#include <cstdlib> // for std::strtof
3#include <hpp/string_view.hpp>
4
6
7#include <libunibreak/graphemebreak.h>
8#include <libunibreak/linebreak.h>
9#include <libunibreak/linebreakdef.h>
10#include <libunibreak/unibreakdef.h>
11
12namespace unravel
13{
14
15namespace
16{
17
18// Conversion ratio: 1 meter = 10 pixel units
19constexpr float PIXELS_PER_METER = 10.0f;
20
21constexpr float METERS_PER_PIXEL = 1.0f / PIXELS_PER_METER;
22
23auto fade(uint32_t c, float alphaMultiplier) -> uint32_t
24{
25 math::color c0(c);
26 math::color result = c0.value * alphaMultiplier;
27 return result;
28};
29// Only these three drive uniform changes in submit_text_buffer
30auto can_batch_with(const text_style& lhs, const text_style& rhs) -> bool
31{
32 constexpr float EPS = 1e-6f;
33 auto feq = [&](float a, float b)
34 {
35 return std::fabs(a - b) < EPS;
36 };
37
38 return feq(lhs.outline_width, rhs.outline_width) && feq(lhs.shadow_softener, rhs.shadow_softener) &&
39 fade(lhs.shadow_color, lhs.opacity) == fade(rhs.shadow_color, rhs.opacity);
40}
41// Applies all style settings from a rich_state to the text buffer.
42void apply_style(gfx::text_buffer_manager& manager, gfx::text_buffer_handle tb, const text_style& state)
43{
44 manager.set_text_color(tb, fade(state.text_color, state.opacity));
45 manager.set_background_color(tb, fade(state.background_color, state.opacity));
46 manager.set_foreground_color(tb, fade(state.foreground_color, state.opacity));
47 manager.set_overline_color(tb, fade(state.overline_color, state.opacity));
48 manager.set_underline_color(tb, fade(state.underline_color, state.opacity));
49 manager.set_strike_through_color(tb, fade(state.strike_color, state.opacity));
50 manager.set_outline_width(tb, state.outline_width);
51 manager.set_outline_color(tb, fade(state.outline_color, state.opacity));
52 manager.set_drop_shadow_offset(tb, state.shadow_offsets.x, state.shadow_offsets.y);
53 manager.set_drop_shadow_color(tb, fade(state.shadow_color, state.opacity));
54 manager.set_drop_shadow_softener(tb, state.shadow_softener);
55 manager.set_style(tb, state.style_flags);
56}
57
58// safe float parser: tries from_chars, then strtof
59auto safe_parse_float(hpp::string_view const& s, float def = 0.0f) -> float
60{
61 float value = def;
62
63 // 2) fallback: copy into a std::string (which *is* NUL-terminated)
64 {
65 std::string tmp(s.data(), s.size());
66 char* endptr = nullptr;
67 errno = 0;
68 float v = std::strtof(tmp.c_str(), &endptr);
69 if(errno == 0 && endptr == tmp.c_str() + tmp.size())
70 {
71 return v;
72 }
73 }
74
75 // 3) neither worked → default
76 return def;
77}
78
79// -------------------------------------------------
80// 1) super‐fast color parser
81// -------------------------------------------------
82// clang-format off
83static constexpr std::pair<hpp::string_view, uint32_t> k_named_colors[] = {
84 { "black", 0xFF000000u },
85 { "white", 0xFFFFFFFFu },
86 { "red", 0xFF0000FFu },
87 { "green", 0xFF00FF00u },
88 { "blue", 0xFFFF0000u },
89 { "yellow", 0xFF00FFFFu },
90 { "cyan", 0xFFFFFF00u },
91 { "magenta", 0xFFFF00FFu },
92 { "gray", 0xFF808080u },
93 { "grey", 0xFF808080u },
94 { "orange", 0xFF00A5FFu },
95 { "purple", 0xFF800080u },
96 { "pink", 0xFFCBC0FFu },
97 { "brown", 0xFF2A2AFFu },
98 { "maroon", 0xFF000080u },
99 { "olive", 0xFF008080u },
100 { "navy", 0xFF800000u },
101 { "teal", 0xFF808000u },
102 { "silver", 0xFFC0C0C0u },
103 { "gold", 0xFF00D7FFu },
104};
105// clang-format on
106auto hex_nib(char c) -> uint8_t
107{
108 if(c >= '0' && c <= '9')
109 {
110 return c - '0';
111 }
112 if(c >= 'a' && c <= 'f')
113 {
114 return c - 'a' + 10;
115 }
116 if(c >= 'A' && c <= 'F')
117 {
118 return c - 'A' + 10;
119 }
120 return 0;
121}
122auto parse_color(hpp::string_view s) -> uint32_t
123{
124 // 1) hash-hex
125 if(!s.empty() && s[0] == '#')
126 {
127 auto nib = [&](char c)
128 {
129 if(c >= '0' && c <= '9')
130 {
131 return uint8_t(c - '0');
132 }
133 if(c >= 'a' && c <= 'f')
134 {
135 return uint8_t(c - 'a' + 10);
136 }
137 if(c >= 'A' && c <= 'F')
138 {
139 return uint8_t(c - 'A' + 10);
140 }
141 return uint8_t(0);
142 };
143
144 const char* p = s.data() + 1;
145 uint8_t r = nib(p[0]) << 4 | nib(p[1]);
146 uint8_t g = nib(p[2]) << 4 | nib(p[3]);
147 uint8_t b = nib(p[4]) << 4 | nib(p[5]);
148 uint8_t a = (s.size() == 9) ? (nib(p[6]) << 4 | nib(p[7])) : 0xFFu;
149
150 // construct via your color struct…
151 math::color col(r, g, b, a);
152 return static_cast<uint32_t>(col);
153 }
154
155 // 2) named lookup
156 for(auto& kv : k_named_colors)
157 {
158 if(kv.first == s)
159 {
160 return kv.second;
161 }
162 }
163
164 // 3) fallback: opaque white
165 return static_cast<uint32_t>(math::color::white());
166}
167
168// -------------------------------------------------
169// 2) parse segments (unchanged)
170// -------------------------------------------------
171
172// Splits `in` into a sequence of RichSegments, respecting nested tags
173
174auto parse_rich_segments(const hpp::string_view& in, const text_style& main_style, bool is_rich) -> segment_list
175{
177 if(!is_rich)
178 {
179 out.emplace_back(rich_segment{.text = in, .state = {.style = main_style}});
180 return out;
181 }
182
183 // Stack of (state, tag_name) so we can pop by name
185 open_tags.reserve(16);
186 open_tags.emplace_back(rich_state{.style = main_style}, hpp::string_view{}); // base
187
188 size_t pos = 0, text_start = 0, len = in.size();
189 while(pos < len)
190 {
191 // 1) Find next '<'
192 size_t open = in.find('<', pos);
193 if(open == hpp::string_view::npos)
194 {
195 break;
196 }
197
198 // 2) Emit text before it
199 if(open > text_start)
200 {
201 out.push_back({hpp::string_view(in.data() + text_start, open - text_start), open_tags.back().first});
202 }
203
204 // 3) Try to find matching '>'
205 size_t close = in.find('>', open + 1);
206 if(close == hpp::string_view::npos)
207 {
208 // No closing '>' → treat this '<' as literal
209 out.push_back({.text=hpp::string_view(in.data() + open, 1), .state=open_tags.back().first});
210 pos = open + 1;
211 text_start = pos;
212 continue;
213 }
214
215 // 4) If there's another '<' before that '>', it's not a tag
216 size_t stray = in.find('<', open + 1);
217 if(stray != hpp::string_view::npos && stray < close)
218 {
219 // treat the first '<' as literal
220 out.push_back({.text = hpp::string_view(in.data() + open, 1), .state = open_tags.back().first});
221 pos = open + 1;
222 text_start = pos;
223 continue;
224 }
225
226 // 5) We have a well-formed tag [open..close]
227 auto inner = in.substr(open + 1, close - open - 1);
228 pos = close + 1;
229 text_start = pos;
230
231 if(inner.empty())
232 {
233 // "<>" → literal
234 out.push_back({.text = hpp::string_view(in.data() + open, 2), .state = open_tags.back().first});
235 continue;
236 }
237
238 // 6) Closing tag?
239 if(inner[0] == '/')
240 {
241 bool found = false;
242 auto name = inner.substr(1);
243 if(!name.empty())
244 {
245 // Pop the last matching tag by name
246 for(auto it = open_tags.rbegin(); it != open_tags.rend(); ++it)
247 {
248 if(it->second == name)
249 {
250 open_tags.erase(std::next(it).base());
251 found = true;
252 break;
253 }
254 }
255 }
256 if(!found)
257 {
258 // emit literal "</...>"
259 out.push_back(
260 {.text = hpp::string_view(in.data() + open, close - open + 1), .state = open_tags.back().first});
261 }
262 continue;
263 }
264
265 // 7) Opening/inline tag
266 size_t eq = inner.find('=');
267 hpp::string_view key = eq == hpp::string_view::npos ? inner : inner.substr(0, eq);
268 hpp::string_view val = eq == hpp::string_view::npos ? hpp::string_view{} : inner.substr(eq + 1);
269
270 rich_state ns = open_tags.back().first;
271 bool recognized = true;
272
273 if(key == "color")
274 {
275 ns.style.text_color = parse_color(val);
276 }
277 else if(key == "alpha" || key == "opacity")
278 {
279 // parse as float 0.0–1.0, defaulting to 1
280 float f = safe_parse_float(val, 1.0f);
281 f = math::clamp(f, 0.0f, 1.0f);
282 ns.style.opacity *= f;
283 }
284 else if(key == "background-color" || key == "bgcolor")
285 {
286 ns.style.background_color = parse_color(val);
287 ns.style.style_flags |= gfx::style_background;
288 }
289 else if(key == "foreground-color" || key == "fgcolor")
290 {
291 ns.style.foreground_color = parse_color(val);
292 ns.style.style_flags |= gfx::style_foreground;
293 }
294 else if(key == "overline-color")
295 {
296 ns.style.overline_color = parse_color(val);
297 ns.style.style_flags |= gfx::style_overline;
298 }
299 else if(key == "overline" || key == "o")
300 {
301 ns.style.overline_color = ns.style.text_color;
302 ns.style.style_flags |= gfx::style_overline;
303 }
304 else if(key == "underline-color")
305 {
306 ns.style.underline_color = parse_color(val);
307 ns.style.style_flags |= gfx::style_underline;
308 }
309 else if(key == "underline" || key == "u")
310 {
311 ns.style.underline_color = ns.style.text_color;
312 ns.style.style_flags |= gfx::style_underline;
313 }
314 else if(key == "strikethrough-color" || key == "strike-color")
315 {
316 ns.style.strike_color = parse_color(val);
317 ns.style.style_flags |= gfx::style_strike_through;
318 }
319 else if(key == "strikethrough" || key == "s")
320 {
321 ns.style.strike_color = ns.style.text_color;
322 ns.style.style_flags |= gfx::style_strike_through;
323 }
324 else if(key == "outline-width")
325 {
326 ns.style.outline_width = safe_parse_float(val);
327 }
328 else if(key == "outline-color")
329 {
330 ns.style.outline_color = parse_color(val);
331 }
332 else if(key == "shadow-offset" || key == "drop-shadow-offset")
333 {
334 std::string tmp(val);
335 std::replace(tmp.begin(), tmp.end(), ',', ' ');
336 std::istringstream ss(tmp);
337 ss >> ns.style.shadow_offsets.x >> ns.style.shadow_offsets.y;
338 }
339 else if(key == "shadow-color" || key == "drop-shadow-color")
340 {
341 ns.style.shadow_color = parse_color(val);
342 }
343 else if(key == "shadow-softener" || key == "drop-shadow-softener")
344 {
345 ns.style.shadow_softener = safe_parse_float(val);
346 }
347 else if(key == "nobr")
348 {
349 ns.no_break = true;
350 }
351 else if(key == "style")
352 {
353 uint32_t f = 0;
354 size_t p = 0;
355 while(p < val.size())
356 {
357 auto c = val.find_first_of("|,", p);
358 auto sub = val.substr(p, c == hpp::string_view::npos ? hpp::string_view::npos : c - p);
359 if(sub == "underline")
360 {
362 }
363 else if(sub == "overline")
364 {
366 }
367 else if(sub == "strikethrough" || sub == "strike")
368 {
370 }
371 else if(sub == "background")
372 {
374 }
375 else if(sub == "foreground")
376 {
378 }
379 if(c == hpp::string_view::npos)
380 {
381 break;
382 }
383 p = c + 1;
384 }
385 ns.style.style_flags = f;
386 }
387 else
388 {
389 // unrecognized → emit literally
390 out.push_back(
391 {.text = hpp::string_view(in.data() + open, close - open + 1), .state = open_tags.back().first});
392 continue;
393 }
394
395 // 8) push new tag state
396 open_tags.emplace_back(ns, key);
397 }
398
399 // 9) Emit any trailing text
400 if(text_start < len)
401 {
402 out.push_back(
403 {.text = hpp::string_view(in.data() + text_start, len - text_start), .state = open_tags.back().first});
404 }
405
406 return out;
407}
408
409void measure_all_widths(text_vector<word_frag>& frags, const scaled_font& base_font)
410{
411 for(auto& f : frags)
412 {
413 text_metrics m;
414 m.metrics.append_text(base_font.handle, f.txt.data(), f.txt.data() + f.txt.size());
415 f.base_width = m.metrics.get_width();
416 f.scaled_width = f.base_width;
417 }
418}
419
420auto measure_line_width(segment_list& frags, const scaled_font& base_font) -> float
421{
422 float w = 0.0f;
423 for(auto& f : frags)
424 {
425 text_metrics m;
426 m.metrics.append_text(base_font.handle, f.text.data(), f.text.data() + f.text.size());
427 w += m.metrics.get_width();
428 }
429
430 return w;
431}
432
433auto measure_text_width(const hpp::string_view& txt, const scaled_font& base_font) -> float
434{
435 text_metrics m;
436 m.metrics.append_text(base_font.handle, txt.data(), txt.data() + txt.size());
437 return m.metrics.get_width();
438}
439
440struct linebreak_ctx
441{
443 const text_vector<size_t>* offsets; // size = segments.size()+1
444 size_t total_len; // = offsets.back()
445};
446// -------------------------------------------------
447// 1) Build a small context object holding your fragments
448// -------------------------------------------------
449// context passed into set_linebreaks
450
451// — your callback from before —
452auto get_next_char_frag(const void* ctx_void,
453 size_t /*len*/,
454 size_t* ip // in/out byte‐offset into the whole virtual stream
455 ) -> utf32_t
456{
457 auto const* ctx = static_cast<const linebreak_ctx*>(ctx_void);
458 size_t pos = *ip;
459 if(pos >= ctx->total_len)
460 {
461 return EOS;
462 }
463
464 auto& offsets = *ctx->offsets;
465 auto& segments = *ctx->segments;
466
467 // figure out which segment contains byte 'pos'
468 auto it = std::upper_bound(offsets.begin(), offsets.end(), pos);
469 size_t seg_idx = (it - offsets.begin()) - 1;
470 size_t seg_start = offsets[seg_idx];
471 auto const& txt = segments[seg_idx].text;
472
473 size_t local_ip = pos - seg_start;
474 assert(local_ip <= txt.size());
475
476 auto txt_data = txt.data();
477 auto txt_size = txt.size();
478
479 // delegate UTF-8 decoding:
480 utf32_t cp = ub_get_next_char_utf8(reinterpret_cast<const utf8_t*>(txt_data), txt_size, &local_ip);
481
482 // advance global position
483 *ip = seg_start + local_ip;
484 return cp;
485}
486
487auto tokenize_fragments_and_measure(const segment_list& segments,
489 const scaled_font& font,
490 scratch_cache& cache) -> fragment_list&
491{
492 // fragment_list frags;
493 auto& frags = cache.frags;
494 frags.clear();
495 frags.reserve(segments.size() * 4);
496
497 cache.offsets.resize(segments.size() + 1);
498 cache.offsets[0] = 0;
499 for(size_t i = 0; i < segments.size(); ++i)
500 {
501 cache.offsets[i + 1] = cache.offsets[i] + segments[i].text.size();
502 }
503
504 // 1) build offsets[] and compute total length
505 linebreak_ctx ctx;
506 ctx.segments = &segments;
507 ctx.offsets = &cache.offsets;
508 ctx.total_len = cache.offsets.back();
509
510 // 2) allocate global break‐map
511 cache.lb.resize(ctx.total_len);
512
514 {
515 cache.wb.resize(ctx.total_len);
516 }
517
518 // 3) ask libunibreak to fill it, using our callback
519 set_linebreaks(&ctx,
520 ctx.total_len,
521 /*lang=*/nullptr,
522 LBOT_PER_CODE_UNIT,
523 cache.lb.data(),
524 get_next_char_frag);
525
526 // 4) If grapheme‐mode, also fill grapheme map
528 {
529 set_graphemebreaks(&ctx, ctx.total_len, cache.wb.data(), get_next_char_frag);
530 }
531
532 // 5) now scan each segment exactly as before, but consult cache.brks[global_i]
533 for(size_t seg_i = 0; seg_i < segments.size(); ++seg_i)
534 {
535 auto const& seg = segments[seg_i];
536 auto const& state = seg.state;
537 auto const& s = seg.text;
538 size_t const base = cache.offsets[seg_i];
539 size_t n = s.size();
540
541 size_t start = 0;
542 while(start < n)
543 {
544 // scan forward looking for next break point
545 size_t idx = start;
546 bool found_lb = false;
547 bool found_wb = false;
548 size_t break_end = 0;
549
550 while(idx < n)
551 {
552 // decode one code‐unit
553 unsigned char b0 = s[idx];
554 size_t cp_len = (b0 < 0x80 ? 1 : (b0 < 0xE0 ? 2 : (b0 < 0xF0 ? 3 : 4)));
555 if(idx + cp_len > n)
556 {
557 cp_len = n - idx;
558 }
559
560 size_t global_cp_end = base + idx + cp_len;
561 char lbv = cache.lb[global_cp_end - 1];
562 if(lbv == LINEBREAK_MUSTBREAK)
563 {
564 found_lb = true;
565
566 break_end = idx + cp_len;
567 break;
568 }
570 {
571 if(lbv == LINEBREAK_ALLOWBREAK)
572 {
573 found_wb = true;
574 break_end = idx + cp_len;
575 break;
576 }
577 }
579 {
580 char wbv = cache.wb[global_cp_end - 1];
581 if(wbv == GRAPHEMEBREAK_BREAK)
582 {
583 found_wb = true;
584 break_end = idx + cp_len;
585 break;
586 }
587 }
588
589 idx += cp_len;
590 }
591
592 size_t frag_len = 0;
594
595 if(!found_lb && !found_wb)
596 {
597 // no more breaks → emit the tail
598 frag_len = n - start;
600
601 // extract slice
602 std::string_view slice(s.data() + start, frag_len);
603 std::string_view break_slice(s.data() + start + frag_len, 0);
604
605 auto w = measure_text_width(slice, font);
606 frags.push_back({slice, break_slice, state, brk, w, w});
607
608 start = n;
609 }
610 else
611 {
612 // need to backtrack to codepoint boundary
613 size_t cp0 = break_end - 1;
614 while(cp0 > start && (uint8_t(s[cp0]) & 0xC0) == 0x80)
615 {
616 --cp0;
617 }
618
619 if(found_lb)
620 {
621 // drop the break code-point itself
622 frag_len = cp0 - start;
624 }
625 else // foundWB
626 {
627 // include the break code-point
628 frag_len = break_end - start;
629 }
630
631 // extract slice
632 std::string_view slice(s.data() + start, frag_len);
633 std::string_view break_slice(s.data() + start + frag_len, break_end - (start + frag_len));
634
635 auto w = measure_text_width(slice, font);
636 frags.push_back({slice, break_slice, state, brk, w, w});
637
638 start = break_end;
639 }
640 }
641 }
642
643 return frags;
644}
645
646// --------------------------------------------------------------------
647// A) Given the raw total height (n·line_h), subtract off the extra
648// leading above the capline and the extra descent below the baseline.
649// That gives you the distance from capline…baseline.
650// --------------------------------------------------------------------
651auto compute_typographic_height(float total_h, float above_capline, float below_baseline, uint32_t alignment) -> float
652{
653 bool typographic = (alignment & align::typographic_mask) != 0;
654 if(!typographic)
655 {
656 return total_h;
657 }
658
659 // remove the extra space above the first capline and below the last baseline
660 return total_h - (above_capline + below_baseline);
661}
662
663auto apply_typographic_adjustment(float total_h, float scale, const scaled_font& fnt, uint32_t alignment) -> float
664{
665 const auto& info = fnt.get_info();
666 float above_capline = info.ascender - info.capline;
667 ;
668 float below_baseline = -info.descender;
669
670 return compute_typographic_height(total_h, above_capline * scale, below_baseline * scale, alignment);
671}
672
673// --------------------------------------------
674// helper: merge into same-state run
675// --------------------------------------------
676void merge_into_line(segment_list& line, const word_frag& f)
677{
678 if(!line.empty() && can_batch_with(line.back().state.style, f.state.style) &&
679 line.back().text.data() + line.back().text.size() == f.txt.data())
680 {
681 // extend previous
682 auto& back = line.back();
683 back.text = {back.text.data(), back.text.size() + f.txt.size()};
684 }
685 else
686 {
687 line.push_back({f.txt, f.state});
688 }
689}
690
691// -------------------------------------------------
692// 4) scale + wrap, store per‐line width
693// -------------------------------------------------
694
695auto wrap_fragments(const fragment_list& frags, float max_width_px, scratch_cache& cache) -> text_layout
696{
697 auto& atoms = cache.atoms;
698 atoms.clear();
699 atoms.reserve(frags.size());
700
701 {
702 frag_atom cur;
703 cur.parts.reserve(4);
704 for(auto const& f : frags)
705 {
706 cur.parts.push_back(f);
707 cur.width += f.scaled_width;
709 {
710 cur.brk = f.brk;
711 atoms.push_back(std::move(cur));
712 cur = {};
713 cur.parts.reserve(4);
714 }
715 }
716 if(!cur.parts.empty())
717 {
718 cur.brk = break_type::allowbreak;
719 atoms.push_back(std::move(cur));
720 }
721 }
722
723 // --- Greedy line‐fitting of atoms ---
724 text_layout lines;
725 lines.reserve(atoms.size());
726
727 wrapped_line cur_line;
728 cur_line.segments.reserve(16);
729 float cur_w = 0;
730
731 for(auto& atom : atoms)
732 {
733 // (1) If this atom would overflow, flush current line first:
734 if(cur_w + atom.width > max_width_px && !cur_line.segments.empty())
735 {
736 cur_line.width = cur_w;
737 lines.push_back(std::move(cur_line));
738 cur_line = {};
739 cur_line.segments.reserve(16);
740 cur_w = 0;
741 }
742
743 // (2) Append the atom:
744 for(auto& frag : atom.parts)
745 {
746 merge_into_line(cur_line.segments, frag);
747 }
748 cur_w += atom.width;
749
750 // (3) If it was a forced break, now flush:
751 if(atom.brk == break_type::mustbreak)
752 {
753 cur_line.width = cur_w;
754 if(!atom.parts.empty())
755 {
756 cur_line.brk_symbol = atom.parts.back().brk_symbol;
757 }
758 lines.push_back(std::move(cur_line));
759 cur_line = {};
760 cur_line.segments.reserve(16);
761 cur_w = 0;
762 }
763 }
764
765 // --- Final flush ---
766 if(!cur_line.segments.empty())
767 {
768 cur_line.width = cur_w;
769 lines.push_back(std::move(cur_line));
770 }
771
772 return lines;
773}
774// -------------------------------------------------
775// 5) top-level API: one tokenize + one measure + O(log N) cheap wraps
776// reuses the last “good” layout instead of recomputing
777// -------------------------------------------------
778auto wrap_lines(text_component::overflow_type type,
779 uint32_t alignment,
780 const segment_list& segments,
781 scratch_cache& cache,
782 uint32_t& calculated_font_size, // out param
784 const urange32_t& auto_size_range,
785 float bound_w_px,
786 float bound_h_px) -> text_layout
787{
788 // a) measure at base size
789 uint32_t base_size = auto_size_range.min;
790 auto base_font = font.get()->get_scaled_font(base_size);
791
792 // b) tokenize fragments and measure
793 auto& frags = tokenize_fragments_and_measure(segments, type, *base_font, cache);
794
795 text_layout best_layout = wrap_fragments(frags, bound_w_px, cache);
796 uint32_t best = base_size;
797
798 // c) binary search…
799 uint32_t lo = base_size + 1, hi = auto_size_range.max;
800 while(lo <= hi)
801 {
802 uint32_t mid = (lo + hi) >> 1;
803 float scale = float(mid) / float(base_size);
804
805 // scale every frag
806 for(auto& f : frags)
807 {
808 f.scaled_width = f.base_width * scale;
809 }
810
811 // wrap at this scale
812 auto layout_mid = wrap_fragments(frags, bound_w_px, cache);
813
814 // vertical fit?
815 float total_h = layout_mid.size() * (base_font->get_line_height() * scale);
816 total_h = apply_typographic_adjustment(total_h, scale, *base_font, alignment);
817
818 if(total_h > bound_h_px)
819 {
820 hi = mid - 1;
821 continue;
822 }
823
824 // horizontal fit?
825 bool ok_h = true;
826 for(auto& wl : layout_mid)
827 {
828 if(wl.width > bound_w_px)
829 {
830 ok_h = false;
831 break;
832 }
833 }
834
835 if(ok_h)
836 {
837 // success: record and try larger
838 best = mid;
839 best_layout = std::move(layout_mid);
840 lo = mid + 1;
841 }
842 else
843 {
844 // too wide: shrink
845 hi = mid - 1;
846 }
847 }
848
849 // d) emit
850 calculated_font_size = best;
851 return best_layout;
852}
853
854// tokenize + measure at one fixed font size, then wrap
855auto wrap_fixed_size(text_component::overflow_type type,
856 const segment_list& segments,
857 scratch_cache& cache,
858 const scaled_font& font,
859 float max_width_px) -> text_layout
860{
861 // 1) tokenize & base-measure
862 auto& frags = tokenize_fragments_and_measure(segments, type, font, cache);
863
864 // 2) single greedy wrap (width + must_break)
865 return wrap_fragments(frags, max_width_px, cache);
866}
867
868// --------------------------------------------------------------------
869// Compute the Y-offset (pen_y for the first line) so that the
870// block of text (either its full total_h or its usable height)
871// is positioned according to your chosen alignment.
872// --------------------------------------------------------------------
873auto compute_vertical_offset(uint32_t alignment,
874 float bounds_h_m,
875 float total_h,
876 float above_capline,
877 float below_baseline) -> float
878{
879 float bounds_h_px = bounds_h_m * PIXELS_PER_METER;
880
881 float usable_h = compute_typographic_height(total_h, above_capline, below_baseline, alignment);
882
883 float offset_px{};
884 switch(alignment & align::vertical_text_mask)
885 {
886 case align::top:
887 return 0.0f;
888 case align::middle:
889 return (bounds_h_px - total_h) * 0.5f;
890 case align::bottom:
891 return (bounds_h_px - total_h);
892 case align::capline:
893 return -above_capline;
894 case align::midline:
895 return -above_capline + (bounds_h_px - usable_h) * 0.5f;
896 case align::baseline:
897 return below_baseline + (bounds_h_px - total_h);
898 default:
899 return 0.0f;
900 }
901}
902// Compute horizontal offset (left, center, right), converting bounds from meters to pixels then back.
903auto compute_horizontal_offset(uint32_t alignment, float bounds_width_m, float line_width_px) -> float
904{
905 float bounds_width_px = bounds_width_m * PIXELS_PER_METER;
906 float offset_px{};
907 switch(alignment & (align::horizontal_mask))
908 {
909 case align::center:
910 offset_px = (bounds_width_px - line_width_px) * 0.5f;
911 break;
912 case align::right:
913 offset_px = (bounds_width_px - line_width_px);
914 break;
915 default:
916 offset_px = 0.0f;
917 break; // Left
918 }
919 return offset_px;
920}
921} // namespace
922
923void text_component::set_text(const std::string& text)
924{
925 if(text_ == text)
926 {
927 return;
928 }
929 text_ = text;
930 text_dirty_ = true;
931}
932
933auto text_component::get_text() const -> const std::string&
934{
935 return text_;
936}
937
939{
940 if(style_ == style)
941 {
942 return;
943 }
944 style_ = style;
945 text_dirty_ = true;
946}
947
949{
950 return style_;
951}
952
954{
955 if(type_ == type)
956 {
957 return;
958 }
959 type_ = type;
960 text_dirty_ = true;
961}
963{
964 return type_;
965}
966
968{
969 if(overflow_type_ == type)
970 {
971 return;
972 }
973 overflow_type_ = type;
974 text_dirty_ = true;
975}
977{
978 return overflow_type_;
979}
980
982{
983 if(font_ == font && font_version_ == font.version())
984 {
985 return;
986 }
987 font_ = font;
988
989 scaled_font_dirty_ = true;
990}
991
993{
994 return font_;
995}
996
998{
999 if(font_.version() != font_version_ || scaled_font_dirty_)
1000 {
1001 recreate_scaled_font();
1002 }
1003
1004 if(scaled_font_)
1005 {
1006 return *scaled_font_;
1007 }
1008
1009 static const scaled_font empty;
1010 return empty;
1011}
1012
1013auto text_component::get_builder() const -> text_buffer_builder&
1014{
1015 bool dirty = text_dirty_ || scaled_font_dirty_;
1016 // nothing to do if clean or font isn’t ready
1017 if(!dirty || !get_scaled_font().is_valid())
1018 {
1019 return *builder_;
1020 }
1021
1022 // APPLOG_INFO_PERF(std::chrono::microseconds);
1023 uint32_t alignment = align_.flags;
1024
1025 auto buf_type =
1028 : (type_ == buffer_type::dynamic_buffer ? gfx::buffer_type::Dynamic : gfx::buffer_type::Transient);
1029
1030 // 1) parse rich segments once
1031 auto segments = parse_rich_segments(text_, style_, is_rich_);
1032
1033 // 2) compute our pixel bounds
1034 float bound_w = area_.width * PIXELS_PER_METER;
1035 float bound_h = area_.height * PIXELS_PER_METER;
1036
1037 text_layout& layout = scratch_.layout;
1038
1039 if(!auto_size_)
1040 {
1041 // --- NO AUTO-FIT PATH ---
1042 // pick the fixed font size:
1043 calculated_font_size_ = font_size_;
1044 scaled_font_ = font_.get()->get_scaled_font(calculated_font_size_);
1045 auto& fixed_font = *scaled_font_;
1046
1047 layout = wrap_fixed_size(overflow_type_, segments, scratch_, fixed_font, bound_w);
1048 }
1049 else
1050 {
1051 // 3) run the unified wrap+auto-fit routine
1052 // this will:
1053 // * tokenize & base-measure at auto_size_range_.min
1054 // * binary-search the best size in [min..max]
1055 // * return a text_layout with each line’s .width already set
1056 layout = wrap_lines(overflow_type_,
1057 alignment,
1058 segments,
1059 scratch_,
1060 calculated_font_size_,
1061 font_,
1062 auto_size_font_range_,
1063 bound_w,
1064 bound_h);
1065 // wrap_lines will update calculated_font_size_ for you
1066 scaled_font_ = font_.get()->get_scaled_font(calculated_font_size_);
1067 }
1068
1069 auto& final_font = *scaled_font_;
1070
1071 // 4) compute vertical offset once
1072 const auto& info = final_font.get_info();
1073 float line_h = final_font.get_line_height();
1074 float above_capline = info.ascender - info.capline;
1075 float below_baseline = -info.descender;
1076
1077 float total_h = float(layout.size()) * line_h;
1078 float offset_y = compute_vertical_offset(alignment, area_.height, total_h, above_capline, below_baseline);
1079
1080 // 5) lay out each line
1081 float pen_y = offset_y;
1082
1083 // 0) clear out old buffers
1084 builder_->destroy_buffers();
1085 debug_builder_->destroy_buffers();
1086
1087 rich_segment* last_segment{};
1088 for(auto& wl : layout)
1089 {
1090 float offset_x = compute_horizontal_offset(alignment, area_.width, wl.width);
1091 float pen_x = offset_x;
1092
1093 for(auto& seg : wl.segments)
1094 {
1095 bool create_new = !(last_segment && can_batch_with(last_segment->state.style, seg.state.style));
1096 if(create_new)
1097 {
1098 auto buf = builder_->manager.create_text_buffer(FONT_TYPE_DISTANCE_OUTLINE_DROP_SHADOW_IMAGE, buf_type);
1099 builder_->buffers.push_back({buf});
1100 }
1101
1102 auto& buf = builder_->buffers.back().handle;
1103 apply_style(builder_->manager, buf, seg.state.style);
1104 builder_->manager.set_apply_kerning(buf, apply_kerning_);
1105 builder_->manager.set_pen_origin(buf, offset_x, offset_y);
1106 builder_->manager.set_pen_position(buf, pen_x, pen_y);
1107 builder_->manager.append_text(buf, final_font.handle, seg.text.data(), seg.text.data() + seg.text.size());
1108 builder_->manager.get_pen_position(buf, &pen_x, &pen_y);
1109
1110 last_segment = &seg;
1111 }
1112
1113 pen_y += line_h;
1114 }
1115
1116 // {
1117 // debug_builder_->destroy_buffers();
1118 // auto buf = debug_builder_->manager.create_text_buffer(FONT_TYPE_DISTANCE, buf_type);
1119
1120 // debug_builder_->manager.set_background_color(buf, 0xffffffff);
1121 // for(size_t i = 0; i < 6; ++i)
1122 // {
1123 // debug_builder_->manager.append_atlas_face(buf, i);
1124 // }
1125 // debug_builder_->buffers.push_back({buf});
1126 // }
1127
1128 // 6) mark clean
1129 text_dirty_ = false;
1130 scaled_font_dirty_ = false;
1131 return *builder_;
1132}
1133
1134void text_component::set_font_size(uint32_t font_size)
1135{
1136 if(font_size_ == font_size)
1137 {
1138 return;
1139 }
1140 font_size_ = font_size;
1141
1142 scaled_font_dirty_ = true;
1143}
1144
1145auto text_component::get_font_size() const -> uint32_t
1146{
1147 return font_size_;
1148}
1149
1151{
1152 if(auto_size_ == auto_size)
1153 {
1154 return;
1155 }
1156
1157 auto_size_ = auto_size;
1158 text_dirty_ = true;
1159}
1160
1162{
1163 return auto_size_;
1164}
1165
1167{
1168 return calculated_font_size_;
1169}
1170
1172{
1173 if(is_rich_ == is_rich)
1174 {
1175 return;
1176 }
1177
1178 is_rich_ = is_rich;
1179 text_dirty_ = true;
1180}
1181
1183{
1184 return is_rich_;
1185}
1186
1188{
1189 if(apply_kerning_ == apply_kerning)
1190 {
1191 return;
1192 }
1193
1194 apply_kerning_ = apply_kerning;
1195 text_dirty_ = true;
1196}
1197
1199{
1200 return apply_kerning_;
1201}
1202
1204{
1205 if(align_.flags == align.flags)
1206 {
1207 return;
1208 }
1209
1210 align_ = align;
1211 text_dirty_ = true;
1212}
1213
1215{
1216 return align_;
1217}
1218
1220{
1221 if(area_ == area)
1222 {
1223 return;
1224 }
1225
1226 area_ = area;
1227 text_dirty_ = true;
1228}
1229
1230auto text_component::get_area() const -> const fsize_t&
1231{
1232 return area_;
1233}
1234
1236{
1237 if(auto_size_font_range_ == range)
1238 {
1239 return;
1240 }
1241
1242 auto_size_font_range_ = range;
1243 text_dirty_ = true;
1244}
1245
1247{
1248 return auto_size_font_range_;
1249}
1250
1252{
1253 const auto& area = get_area();
1254 math::bbox bbox;
1255 bbox.min.x = -area.width * 0.5f;
1256 bbox.min.y = area.height * 0.5f;
1257 bbox.min.z = 0;
1258
1259 bbox.max.x = area.width * 0.5f;
1260 bbox.max.y = -area.height * 0.5f;
1261 bbox.max.z = 0.001f;
1262
1263 return bbox;
1264}
1265
1267{
1268 const auto& area = get_render_area();
1269 math::bbox bbox;
1270 bbox.min.x = -area.width * 0.5f;
1271 bbox.min.y = area.height * 0.5f;
1272 bbox.min.z = 0;
1273
1274 bbox.max.x = area.width * 0.5f;
1275 bbox.max.y = -area.height * 0.5f;
1276 bbox.max.z = 0.001f;
1277
1278 return bbox;
1279}
1280
1282{
1283 return get_builder().buffers.size();
1284}
1285
1286auto text_component::get_lines(bool include_breaks) const -> text_vector<text_line>
1287{
1288 auto& builder = get_builder();
1289 auto& layout = scratch_.layout;
1290
1292 lines.reserve(layout.size());
1293 for(const auto& layout_line : layout)
1294 {
1295 auto& line = lines.emplace_back();
1296
1297 for(auto& seg : layout_line.segments)
1298 {
1299 line.line += std::string(seg.text);
1300 }
1301
1302 if(include_breaks)
1303 {
1304 line.break_symbol = std::string(layout_line.brk_symbol);
1305 }
1306 }
1307
1308 return lines;
1309}
1310
1311auto text_component::meters_to_px(float meters) const -> float
1312{
1313 return meters * PIXELS_PER_METER;
1314}
1315auto text_component::px_to_meters(float px) const -> float
1316{
1317 return px * METERS_PER_PIXEL;
1318}
1319
1321{
1322 const auto& font = get_scaled_font();
1323 const auto font_scaled_size = get_font_size();
1324
1325 return font_scaled_size > 0 && font.is_valid();
1326}
1327
1329{
1330 const auto& builder = get_builder();
1331 fsize_t result;
1332 for(auto& sb : builder.buffers)
1333 {
1334 auto r = builder.manager.get_rectangle(sb.handle);
1335
1336 result.width = std::max(result.width, r.width);
1337 result.height = std::max(result.height, r.height);
1338 }
1339
1340 auto area = get_area();
1341 result.width = std::max(result.width * METERS_PER_PIXEL, area.width);
1342 result.height = std::max(result.height * METERS_PER_PIXEL, area.height);
1343 return result;
1344}
1345
1346void text_component::recreate_scaled_font() const
1347{
1348 font_version_ = font_.version();
1349
1350 if(!font_)
1351 {
1352 scaled_font_.reset();
1353 return;
1354 }
1355 auto font = font_.get();
1356 if(!font)
1357 {
1358 scaled_font_.reset();
1359 return;
1360 }
1361 scaled_font_ = font->get_scaled_font(get_font_size());
1362}
1363
1364void text_component::submit(gfx::view_id id, const math::transform& world, uint64_t state)
1365{
1366 if(!can_be_rendered())
1367 {
1368 return;
1369 }
1370 auto fit_m = get_area();
1371 float fit_px_w = fit_m.width * PIXELS_PER_METER;
1372 float fit_px_h = fit_m.height * PIXELS_PER_METER;
1373
1374 math::transform pivot;
1375 pivot.translate(-fit_px_w * 0.5f, -fit_px_h * 0.5f);
1376
1377 static const auto unit_scale = math::transform::scaling({METERS_PER_PIXEL, -METERS_PER_PIXEL, 1.0f});
1378
1379 auto text_transform = world * unit_scale * pivot;
1380 auto& builder = get_builder();
1381
1382 const auto& font = get_scaled_font();
1383
1384 for(auto& sb : builder.buffers)
1385 {
1386 gfx::set_transform((const float*)text_transform);
1387 builder.manager.submit_text_buffer(sb.handle, font.handle, id, state);
1388 }
1389
1390 for(auto& sb : debug_builder_->buffers)
1391 {
1392 gfx::set_transform((const float*)text_transform);
1393 debug_builder_->manager.submit_text_buffer(sb.handle, font.handle, id, state);
1394 }
1395}
1396} // namespace unravel
entt::handle b
manifold_type type
entt::handle a
void set_foreground_color(text_buffer_handle handle, uint32_t rgba=0x000000FF)
void set_strike_through_color(text_buffer_handle handle, uint32_t rgba=0x000000FF)
void set_drop_shadow_softener(text_buffer_handle handle, float smoother=1.0f)
void set_style(text_buffer_handle handle, uint32_t flags=style_normal)
void set_outline_width(text_buffer_handle handle, float outline_width=3.0f)
void set_drop_shadow_offset(text_buffer_handle handle, float u, float v)
void set_drop_shadow_color(text_buffer_handle handle, uint32_t rgba=0x000000FF)
void set_text_color(text_buffer_handle handle, uint32_t rgba=0x000000FF)
void set_background_color(text_buffer_handle handle, uint32_t rgba=0x000000FF)
void set_underline_color(text_buffer_handle handle, uint32_t rgba=0x000000FF)
void set_outline_color(text_buffer_handle handle, uint32_t rgba=0x000000FF)
void set_overline_color(text_buffer_handle handle, uint32_t rgba=0x000000FF)
General purpose transformation class designed to maintain each component of the transformation separa...
Definition transform.hpp:27
void translate(T x, T y, T z=T(0)) noexcept
Translate the transform.
static auto scaling(const vec2_t &scale) noexcept -> transform_t
Create a scaling transform.
void set_buffer_type(const buffer_type &type)
Sets the buffer type for text rendering.
auto get_style() const -> const text_style &
Gets the current text style settings.
auto get_apply_kerning() const -> bool
Checks if kerning is enabled.
auto get_area() const -> const fsize_t &
Gets the current text area bounds.
void set_alignment(const alignment &align)
Sets the text alignment properties.
auto get_render_font_size() const -> uint32_t
Gets the actual font size being used for rendering.
void set_font(const asset_handle< font > &font)
Sets the font to be used for rendering text.
auto get_auto_size() const -> bool
Checks if auto-sizing is enabled.
auto get_auto_size_range() const -> const urange32_t &
Gets the current auto-size range.
auto get_font_size() const -> uint32_t
Gets the current font size.
auto get_bounds() const -> math::bbox
Gets the bounding box of the text.
auto get_lines(bool include_breaks=true) const -> text_vector< text_line >
Gets the text content split into lines.
auto get_font() const -> const asset_handle< font > &
Gets the current font.
void set_area(const fsize_t &area)
Sets the area bounds for text rendering.
void set_is_rich_text(bool is_rich)
Enables or disables rich text processing.
auto get_text() const -> const std::string &
Gets the current text content.
void set_overflow_type(const overflow_type &type)
Sets how text should overflow when it exceeds its bounds.
auto get_alignment() const -> const alignment &
Gets the current text alignment settings.
auto get_is_rich_text() const -> bool
Checks if rich text processing is enabled.
auto meters_to_px(float meters) const -> float
Converts meters to pixels based on current font metrics.
auto get_render_area() const -> fsize_t
Gets the actual area used for rendering.
void set_text(const std::string &text)
Sets the text content to be rendered.
auto get_scaled_font() const -> const scaled_font &
Gets the scaled font instance used for rendering.
auto px_to_meters(float px) const -> float
Converts pixels to meters based on current font metrics.
void submit(gfx::view_id id, const math::transform &world, uint64_t state)
Submits the text for rendering.
void set_auto_size(bool auto_size)
Enables or disables automatic font sizing.
void set_font_size(uint32_t font_size)
Sets the font size in pixels.
auto get_render_bounds() const -> math::bbox
Gets the bounding box used for rendering.
auto get_overflow_type() const -> const overflow_type &
Gets the current overflow handling type.
void set_auto_size_range(const urange32_t &range)
Sets the range for automatic font sizing.
auto get_render_buffers_count() const -> size_t
Gets the number of render buffers being used.
auto get_buffer_type() const -> const buffer_type &
Gets the current buffer type.
auto can_be_rendered() const -> bool
Checks if the text can be rendered.
void set_style(const text_style &style)
Sets the text styling properties.
void set_apply_kerning(bool apply_kerning)
Enables or disables kerning in text rendering.
#define FONT_TYPE_DISTANCE_OUTLINE_DROP_SHADOW_IMAGE
float scale
Definition hub.cpp:25
ImGui::Font::Enum font
Definition hub.cpp:24
std::string name
Definition hub.cpp:27
uint32_t set_transform(const void *_mtx, uint16_t _num)
Definition graphics.cpp:788
bgfx::ViewId view_id
Definition graphics.h:20
@ style_strike_through
Definition bbox.cpp:5
void close(const std::string &scope)
Closes the specified scope.
Definition seq.cpp:139
auto start(seq_action action, const seq_scope_policy &scope_policy, hpp::source_location location) -> seq_id_t
Starts a new action.
Definition seq.cpp:8
text_vector< word_frag > fragment_list
text_vector< rich_segment > segment_list
@ vertical_text_mask
hpp::small_vector< T, StaticCapacity > text_vector
text_vector< wrapped_line > text_layout
const segment_list * segments
const text_vector< size_t > * offsets
size_t total_len
Represents a handle to an asset, providing access and management functions.
Storage for box vector values and wraps up common functionality.
Definition bbox.h:21
vec3 min
The minimum vector value of the bounding box.
Definition bbox.h:306
static color white()
Definition math.h:287
vec4 value
Definition math.h:354
T width
Definition basetypes.hpp:55
T height
Definition basetypes.hpp:56
gfx::font_handle handle
Definition font.h:26
auto is_valid() const -> bool
Definition font.cpp:844
cache_t cache
Definition uniform.cpp:15