3#include <hpp/string_view.hpp>
7#include <libunibreak/graphemebreak.h>
8#include <libunibreak/linebreak.h>
9#include <libunibreak/linebreakdef.h>
10#include <libunibreak/unibreakdef.h>
19constexpr float PIXELS_PER_METER = 10.0f;
21constexpr float METERS_PER_PIXEL = 1.0f / PIXELS_PER_METER;
23auto fade(uint32_t c,
float alphaMultiplier) -> uint32_t
30auto can_batch_with(
const text_style& lhs,
const text_style& rhs) ->
bool
32 constexpr float EPS = 1e-6f;
33 auto feq = [&](
float a,
float b)
35 return std::fabs(
a -
b) < EPS;
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);
59auto safe_parse_float(hpp::string_view
const& s,
float def = 0.0f) ->
float
65 std::string tmp(
s.data(),
s.size());
66 char* endptr =
nullptr;
68 float v = std::strtof(tmp.c_str(), &endptr);
69 if(errno == 0 && endptr == tmp.c_str() + tmp.size())
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 },
106auto hex_nib(
char c) -> uint8_t
108 if(c >=
'0' && c <=
'9')
112 if(c >=
'a' && c <=
'f')
116 if(c >=
'A' && c <=
'F')
122auto parse_color(hpp::string_view s) -> uint32_t
125 if(!
s.empty() && s[0] ==
'#')
127 auto nib = [&](
char c)
129 if(c >=
'0' && c <=
'9')
131 return uint8_t(c -
'0');
133 if(c >=
'a' && c <=
'f')
135 return uint8_t(c -
'a' + 10);
137 if(c >=
'A' && c <=
'F')
139 return uint8_t(c -
'A' + 10);
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;
152 return static_cast<uint32_t
>(col);
156 for(
auto& kv : k_named_colors)
174auto parse_rich_segments(
const hpp::string_view& in,
const text_style& main_style,
bool is_rich) ->
segment_list
179 out.emplace_back(rich_segment{.text = in, .state = {.style = main_style}});
185 open_tags.reserve(16);
186 open_tags.emplace_back(rich_state{.style = main_style}, hpp::string_view{});
188 size_t pos = 0, text_start = 0, len = in.size();
192 size_t open = in.find(
'<', pos);
193 if(open == hpp::string_view::npos)
199 if(open > text_start)
201 out.push_back({hpp::string_view(in.data() + text_start, open - text_start), open_tags.back().first});
205 size_t close = in.find(
'>', open + 1);
206 if(close == hpp::string_view::npos)
209 out.push_back({.text=hpp::string_view(in.data() + open, 1), .state=open_tags.back().first});
216 size_t stray = in.find(
'<', open + 1);
217 if(stray != hpp::string_view::npos && stray < close)
220 out.push_back({.text = hpp::string_view(in.data() + open, 1), .state = open_tags.back().first});
227 auto inner = in.substr(open + 1, close - open - 1);
234 out.push_back({.text = hpp::string_view(in.data() + open, 2), .state = open_tags.back().first});
242 auto name = inner.substr(1);
246 for(
auto it = open_tags.rbegin(); it != open_tags.rend(); ++it)
248 if(it->second ==
name)
250 open_tags.erase(std::next(it).base());
260 {.text = hpp::string_view(in.data() + open, close - open + 1), .state = open_tags.back().first});
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);
270 rich_state ns = open_tags.back().first;
271 bool recognized =
true;
275 ns.style.text_color = parse_color(val);
277 else if(key ==
"alpha" || key ==
"opacity")
280 float f = safe_parse_float(val, 1.0f);
281 f = math::clamp(f, 0.0f, 1.0f);
282 ns.style.opacity *=
f;
284 else if(key ==
"background-color" || key ==
"bgcolor")
286 ns.style.background_color = parse_color(val);
289 else if(key ==
"foreground-color" || key ==
"fgcolor")
291 ns.style.foreground_color = parse_color(val);
294 else if(key ==
"overline-color")
296 ns.style.overline_color = parse_color(val);
299 else if(key ==
"overline" || key ==
"o")
301 ns.style.overline_color = ns.style.text_color;
304 else if(key ==
"underline-color")
306 ns.style.underline_color = parse_color(val);
309 else if(key ==
"underline" || key ==
"u")
311 ns.style.underline_color = ns.style.text_color;
314 else if(key ==
"strikethrough-color" || key ==
"strike-color")
316 ns.style.strike_color = parse_color(val);
319 else if(key ==
"strikethrough" || key ==
"s")
321 ns.style.strike_color = ns.style.text_color;
324 else if(key ==
"outline-width")
326 ns.style.outline_width = safe_parse_float(val);
328 else if(key ==
"outline-color")
330 ns.style.outline_color = parse_color(val);
332 else if(key ==
"shadow-offset" || key ==
"drop-shadow-offset")
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;
339 else if(key ==
"shadow-color" || key ==
"drop-shadow-color")
341 ns.style.shadow_color = parse_color(val);
343 else if(key ==
"shadow-softener" || key ==
"drop-shadow-softener")
345 ns.style.shadow_softener = safe_parse_float(val);
347 else if(key ==
"nobr")
351 else if(key ==
"style")
355 while(p < val.size())
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")
363 else if(sub ==
"overline")
367 else if(sub ==
"strikethrough" || sub ==
"strike")
371 else if(sub ==
"background")
375 else if(sub ==
"foreground")
379 if(c == hpp::string_view::npos)
385 ns.style.style_flags =
f;
391 {.text = hpp::string_view(in.data() + open, close - open + 1), .state = open_tags.back().first});
396 open_tags.emplace_back(ns, key);
403 {.text = hpp::string_view(in.data() + text_start, len - text_start), .state = open_tags.back().first});
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;
420auto measure_line_width(
segment_list& frags,
const scaled_font& base_font) ->
float
426 m.metrics.append_text(base_font.handle,
f.text.data(),
f.text.data() +
f.text.size());
427 w +=
m.metrics.get_width();
433auto measure_text_width(
const hpp::string_view& txt,
const scaled_font& base_font) ->
float
436 m.metrics.append_text(base_font.handle, txt.data(), txt.data() + txt.size());
437 return m.metrics.get_width();
452auto get_next_char_frag(
const void* ctx_void,
457 auto const* ctx =
static_cast<const linebreak_ctx*
>(ctx_void);
459 if(pos >= ctx->total_len)
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;
473 size_t local_ip = pos - seg_start;
474 assert(local_ip <= txt.size());
476 auto txt_data = txt.data();
477 auto txt_size = txt.size();
480 utf32_t cp = ub_get_next_char_utf8(
reinterpret_cast<const utf8_t*
>(txt_data), txt_size, &local_ip);
483 *ip = seg_start + local_ip;
489 const scaled_font&
font,
493 auto& frags =
cache.frags;
498 cache.offsets[0] = 0;
507 ctx.offsets = &
cache.offsets;
508 ctx.total_len =
cache.offsets.back();
511 cache.lb.resize(ctx.total_len);
515 cache.wb.resize(ctx.total_len);
529 set_graphemebreaks(&ctx, ctx.total_len,
cache.wb.data(), get_next_char_frag);
533 for(
size_t seg_i = 0; seg_i <
segments.size(); ++seg_i)
536 auto const& state = seg.state;
537 auto const&
s = seg.text;
538 size_t const base =
cache.offsets[seg_i];
546 bool found_lb =
false;
547 bool found_wb =
false;
548 size_t break_end = 0;
553 unsigned char b0 =
s[idx];
554 size_t cp_len = (b0 < 0x80 ? 1 : (b0 < 0xE0 ? 2 : (b0 < 0xF0 ? 3 : 4)));
560 size_t global_cp_end = base + idx + cp_len;
561 char lbv =
cache.lb[global_cp_end - 1];
562 if(lbv == LINEBREAK_MUSTBREAK)
566 break_end = idx + cp_len;
571 if(lbv == LINEBREAK_ALLOWBREAK)
574 break_end = idx + cp_len;
580 char wbv =
cache.wb[global_cp_end - 1];
581 if(wbv == GRAPHEMEBREAK_BREAK)
584 break_end = idx + cp_len;
595 if(!found_lb && !found_wb)
602 std::string_view slice(
s.data() +
start, frag_len);
603 std::string_view break_slice(
s.data() +
start + frag_len, 0);
605 auto w = measure_text_width(slice,
font);
606 frags.push_back({slice, break_slice, state, brk,
w,
w});
613 size_t cp0 = break_end - 1;
614 while(cp0 >
start && (uint8_t(s[cp0]) & 0xC0) == 0x80)
622 frag_len = cp0 -
start;
628 frag_len = break_end -
start;
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));
635 auto w = measure_text_width(slice,
font);
636 frags.push_back({slice, break_slice, state, brk,
w,
w});
651auto compute_typographic_height(
float total_h,
float above_capline,
float below_baseline, uint32_t alignment) ->
float
660 return total_h - (above_capline + below_baseline);
663auto apply_typographic_adjustment(
float total_h,
float scale,
const scaled_font& fnt, uint32_t alignment) ->
float
665 const auto& info = fnt.get_info();
666 float above_capline = info.ascender - info.capline;
668 float below_baseline = -info.descender;
670 return compute_typographic_height(total_h, above_capline *
scale, below_baseline *
scale, alignment);
676void merge_into_line(
segment_list& line,
const word_frag& f)
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())
682 auto&
back = line.back();
683 back.text = {
back.text.data(),
back.text.size() +
f.txt.size()};
687 line.push_back({
f.txt,
f.state});
697 auto& atoms =
cache.atoms;
699 atoms.reserve(frags.size());
703 cur.parts.reserve(4);
704 for(
auto const& f : frags)
706 cur.parts.push_back(f);
707 cur.width +=
f.scaled_width;
711 atoms.push_back(std::move(cur));
713 cur.parts.reserve(4);
716 if(!cur.parts.empty())
719 atoms.push_back(std::move(cur));
725 lines.reserve(atoms.size());
727 wrapped_line cur_line;
728 cur_line.segments.reserve(16);
731 for(
auto& atom : atoms)
734 if(cur_w + atom.width > max_width_px && !cur_line.segments.empty())
736 cur_line.width = cur_w;
737 lines.push_back(std::move(cur_line));
739 cur_line.segments.reserve(16);
744 for(
auto& frag : atom.parts)
746 merge_into_line(cur_line.segments, frag);
753 cur_line.width = cur_w;
754 if(!atom.parts.empty())
756 cur_line.brk_symbol = atom.parts.back().brk_symbol;
758 lines.push_back(std::move(cur_line));
760 cur_line.segments.reserve(16);
766 if(!cur_line.segments.empty())
768 cur_line.width = cur_w;
769 lines.push_back(std::move(cur_line));
781 scratch_cache&
cache,
782 uint32_t& calculated_font_size,
789 uint32_t base_size = auto_size_range.min;
790 auto base_font =
font.get()->get_scaled_font(base_size);
796 uint32_t best = base_size;
799 uint32_t lo = base_size + 1, hi = auto_size_range.max;
802 uint32_t mid = (lo + hi) >> 1;
803 float scale = float(mid) / float(base_size);
808 f.scaled_width =
f.base_width *
scale;
812 auto layout_mid = wrap_fragments(frags, bound_w_px,
cache);
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);
818 if(total_h > bound_h_px)
826 for(
auto& wl : layout_mid)
828 if(wl.width > bound_w_px)
839 best_layout = std::move(layout_mid);
850 calculated_font_size = best;
857 scratch_cache&
cache,
858 const scaled_font&
font,
865 return wrap_fragments(frags, max_width_px,
cache);
873auto compute_vertical_offset(uint32_t alignment,
877 float below_baseline) ->
float
879 float bounds_h_px = bounds_h_m * PIXELS_PER_METER;
881 float usable_h = compute_typographic_height(total_h, above_capline, below_baseline, alignment);
889 return (bounds_h_px - total_h) * 0.5f;
891 return (bounds_h_px - total_h);
893 return -above_capline;
895 return -above_capline + (bounds_h_px - usable_h) * 0.5f;
897 return below_baseline + (bounds_h_px - total_h);
903auto compute_horizontal_offset(uint32_t alignment,
float bounds_width_m,
float line_width_px) ->
float
905 float bounds_width_px = bounds_width_m * PIXELS_PER_METER;
910 offset_px = (bounds_width_px - line_width_px) * 0.5f;
913 offset_px = (bounds_width_px - line_width_px);
969 if(overflow_type_ ==
type)
973 overflow_type_ =
type;
978 return overflow_type_;
983 if(font_ ==
font && font_version_ ==
font.version())
989 scaled_font_dirty_ =
true;
999 if(font_.version() != font_version_ || scaled_font_dirty_)
1001 recreate_scaled_font();
1003 auto font = font_.get();
1007 return *scaled_font_;
1016 bool dirty = text_dirty_ || scaled_font_dirty_;
1024 uint32_t alignment = align_.
flags;
1032 auto segments = parse_rich_segments(text_, style_, is_rich_);
1035 float bound_w = area_.
width * PIXELS_PER_METER;
1036 float bound_h = area_.
height * PIXELS_PER_METER;
1044 calculated_font_size_ = font_size_;
1045 scaled_font_ = font_.get()->get_scaled_font(calculated_font_size_);
1046 auto& fixed_font = *scaled_font_;
1048 layout = wrap_fixed_size(overflow_type_,
segments, scratch_, fixed_font, bound_w);
1057 layout = wrap_lines(overflow_type_,
1061 calculated_font_size_,
1063 auto_size_font_range_,
1067 scaled_font_ = font_.get()->get_scaled_font(calculated_font_size_);
1070 auto& final_font = *scaled_font_;
1073 const auto& info = final_font.get_info();
1074 float line_h = final_font.get_line_height();
1075 float above_capline = info.ascender - info.capline;
1076 float below_baseline = -info.descender;
1078 float total_h = float(layout.size()) * line_h;
1079 float offset_y = compute_vertical_offset(alignment, area_.
height, total_h, above_capline, below_baseline);
1082 float pen_y = offset_y;
1085 builder_->destroy_buffers();
1086 debug_builder_->destroy_buffers();
1088 rich_segment* last_segment{};
1089 for(
auto& wl : layout)
1091 float offset_x = compute_horizontal_offset(alignment, area_.
width, wl.width);
1092 float pen_x = offset_x;
1094 for(
auto& seg : wl.segments)
1096 bool create_new = !(last_segment && can_batch_with(last_segment->state.style, seg.state.style));
1098 auto buf = buffer.handle;
1099 apply_style(builder_->manager, buf, seg.state.style);
1101 builder_->manager.set_apply_kerning(buf, apply_kerning_);
1102 builder_->manager.set_pen_origin(buf, offset_x, offset_y);
1103 builder_->manager.set_pen_position(buf, pen_x, pen_y);
1104 builder_->manager.append_text(buf, final_font.handle, seg.text.data(), seg.text.data() + seg.text.size());
1105 builder_->manager.get_pen_position(buf, &pen_x, &pen_y);
1107 last_segment = &seg;
1126 text_dirty_ =
false;
1127 scaled_font_dirty_ =
false;
1133 if(font_size_ == font_size)
1137 font_size_ = font_size;
1139 scaled_font_dirty_ =
true;
1149 if(auto_size_ == auto_size)
1154 auto_size_ = auto_size;
1165 return calculated_font_size_;
1170 if(is_rich_ == is_rich)
1186 if(apply_kerning_ == apply_kerning)
1191 apply_kerning_ = apply_kerning;
1197 return apply_kerning_;
1234 if(auto_size_font_range_ ==
range)
1239 auto_size_font_range_ =
range;
1245 return auto_size_font_range_;
1252 bbox.
min.x = -area.width * 0.5f;
1253 bbox.min.y = area.height * 0.5f;
1256 bbox.max.x = area.width * 0.5f;
1257 bbox.max.y = -area.height * 0.5f;
1258 bbox.max.z = 0.001f;
1267 bbox.
min.x = -area.width * 0.5f;
1268 bbox.min.y = area.height * 0.5f;
1271 bbox.max.x = area.width * 0.5f;
1272 bbox.max.y = -area.height * 0.5f;
1273 bbox.max.z = 0.001f;
1280 return get_builder().buffers.size();
1285 auto& builder = get_builder();
1286 auto& layout = scratch_.layout;
1289 lines.reserve(layout.size());
1290 for(
const auto& layout_line : layout)
1292 auto& line = lines.emplace_back();
1294 for(
auto& seg : layout_line.segments)
1296 line.line += std::string(seg.text);
1301 line.break_symbol = std::string(layout_line.brk_symbol);
1310 return meters * PIXELS_PER_METER;
1314 return px * METERS_PER_PIXEL;
1327 const auto& builder = get_builder();
1329 for(
auto& sb : builder.buffers)
1331 auto r = builder.manager.get_rectangle(sb.handle);
1333 result.
width = std::max(result.
width, r.width);
1338 result.
width = std::max(result.
width * METERS_PER_PIXEL, area.width);
1339 result.
height = std::max(result.
height * METERS_PER_PIXEL, area.height);
1343void text_component::recreate_scaled_font()
const
1345 font_version_ = font_.version();
1349 scaled_font_.reset();
1352 auto font = font_.get();
1355 scaled_font_.reset();
1368 float fit_px_w = fit_m.width * PIXELS_PER_METER;
1369 float fit_px_h = fit_m.height * PIXELS_PER_METER;
1372 pivot.
translate(-fit_px_w * 0.5f, -fit_px_h * 0.5f);
1376 auto text_transform = world * unit_scale * pivot;
1377 auto& builder = get_builder();
1381 for(
auto& sb : builder.buffers)
1384 builder.manager.submit_text_buffer(sb.handle,
font.
handle,
id, state);
1387 for(
auto& sb : debug_builder_->buffers)
1390 debug_builder_->manager.submit_text_buffer(sb.handle,
font.
handle,
id, state);
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)
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
uint32_t set_transform(const void *_mtx, uint16_t _num)
void close(const std::string &scope)
Closes the specified scope.
Hash specialization for batch_key to enable use in std::unordered_map.
text_vector< word_frag > fragment_list
text_vector< rich_segment > segment_list
hpp::small_vector< T, StaticCapacity > text_vector
text_vector< wrapped_line > text_layout
std::vector< float > scale
std::vector< math::vec3 > start
const segment_list * segments
const text_vector< size_t > * offsets
Thread-safe handle to an asset.
Storage for box vector values and wraps up common functionality.
vec3 min
The minimum vector value of the bounding box.
auto is_valid() const -> bool