// Copyright (C) 2002-2012 Nikolaus Gebhardt // This file is part of the "Irrlicht Engine". // For conditions of distribution and use, see copyright notice in irrlicht.h #ifndef __GLYPH_LAYOUT_H_INCLUDED__ #define __GLYPH_LAYOUT_H_INCLUDED__ #include "irrTypes.h" #include "dimension2d.h" #include "rect.h" #include #include #include #include #include namespace irr { namespace gui { enum ShapeFlag { SF_DISABLE_CACHE = 1, /* Disable caching glyph layouts. */ SF_ENABLE_CLUSTER_TEST = 2, /* If on getCluster will work on these layouts, which the original string will be stored, it will be turned on too if any URL is found. */ }; enum GlyphLayoutFlag { GLF_RTL_LINE = 1, /* This line from this glyph is RTL. */ GLF_RTL_CHAR = 2, /* This character(s) from this glyph is RTL. */ GLF_BREAKABLE = 4, /* This glyph is breakable when line breaking. */ GLF_QUICK_DRAW = 8, /* This glyph is not created by libraqm, which get x_advance_x directly from font. */ GLF_NEWLINE = 16, /* This glyph will start a newline. */ GLF_COLORED = 32, /* This glyph is a colored one (for example emoji). */ GLF_URL = 64, /* This glyph contains clickable url (https or http atm). */ GLF_BREAKTEXT_NEWLINE = 128 /* Newline added via breakGlyphLayouts. */ }; enum GlyphLayoutDraw { GLD_NONE = 0, /* Default flag. */ GLD_MARKED = 1, /* This glyph will be drawn with background marked for marked text. */ GLD_COMPOSING = 2 /* This glyph will be drawn with underline (for example composing text). */ }; //! GlyphLayout copied from libraqm. struct GlyphLayout { u32 index; s32 x_advance; //s32 y_advance; we don't use y_advance atm s32 x_offset; s32 y_offset; /* Above variable is same for raqm_glyph_t */ // If some characters share the same glyph std::vector cluster; std::vector draw_flags; //! used to sorting back the visual order after line breaking u32 original_index; u16 flags; //! this is the face_idx used in stk face ttf u16 face_idx; //! original string, which is used to map with cluster above std::shared_ptr orig_string; }; namespace Private { inline void breakLine(std::vector gls, f32 max_line_width, f32 inverse_shaping, f32 scale, std::vector >& result); } inline void eraseTopLargerThan(std::vector& gls, f32 height_per_line, f32 max_height) { if (max_height < height_per_line * 2.0f) return; std::vector newline_positions; for (unsigned i = 0; i < gls.size(); i++) { const GlyphLayout& glyph = gls[i]; if ((glyph.flags & GLF_NEWLINE) != 0) { newline_positions.push_back(i); continue; } } // Handle the first line f32 total_height = height_per_line + (f32)newline_positions.size() * height_per_line; if (total_height > max_height) { u32 idx = (u32)((total_height - max_height) / height_per_line); if (idx < newline_positions.size()) { auto end_it = gls.begin() + newline_positions[idx] + 1; if (end_it != gls.end()) gls.erase(gls.begin(), end_it); } } } inline core::dimension2d getGlyphLayoutsDimension( const std::vector& gls, s32 height_per_line, f32 inverse_shaping, f32 scale, s32 cluster = -1) { core::dimension2d dim(0.0f, 0.0f); core::dimension2d this_line(0.0f, (f32)height_per_line); for (unsigned i = 0; i < gls.size(); i++) { const GlyphLayout& glyph = gls[i]; if ((glyph.flags & GLF_NEWLINE) != 0) { dim.Height += this_line.Height; if (dim.Width < this_line.Width) dim.Width = this_line.Width; this_line.Width = 0; continue; } f32 cur_width = (s32)(glyph.x_advance * inverse_shaping) * scale; bool found_cluster = false; // Cursor positioning if (cluster != -1) { auto it = std::find(glyph.cluster.begin(), glyph.cluster.end(), cluster); if (it != glyph.cluster.end() && (i == gls.size() - 1 || cluster != gls[i + 1].cluster.front())) { found_cluster = true; // Get cluster ratio to total glyph width, so for example // cluster 0 in ffi glyph will be 0.333 f32 ratio = f32(it - glyph.cluster.begin() + 1) / (f32)glyph.cluster.size(); // Show cursor in left side, so no need to add width if ((glyph.flags & GLF_RTL_CHAR) != 0) cur_width = 0; else cur_width *= ratio; } } this_line.Width += cur_width; if (found_cluster) break; } dim.Height += this_line.Height; if (dim.Width < this_line.Width) dim.Width = this_line.Width; core::dimension2d ret_dim(0, 0); ret_dim.Width = (u32)(dim.Width + 0.9f); // round up ret_dim.Height = (u32)(dim.Height + 0.9f); return ret_dim; } inline s32 getCurosrFromDimension(f32 x, f32 y, const std::vector& gls, s32 height_per_line, f32 inverse_shaping, f32 scale) { if (gls.empty()) return 0; f32 total_width = 0.0f; for (unsigned i = 0; i < gls.size(); i++) { const GlyphLayout& glyph = gls[i]; if ((glyph.flags & GLF_NEWLINE) != 0) { // TODO: handling newline break; } f32 cur_width = (s32)(glyph.x_advance * inverse_shaping) * scale; if (glyph.cluster.size() == 1) { // One more character threshold because we show the cursor position // opposite side for RTL character if (glyph.flags & GLF_RTL_CHAR) { if (i == 0 && cur_width * 0.5 > x) return glyph.cluster.front() + 1; if (total_width + cur_width * 1.5 > x) return glyph.cluster.front(); } else { if (i == 0 && cur_width * 0.5 > x) return 0; if (total_width + cur_width * 0.5 > x) return glyph.cluster.front(); } } else if (total_width + cur_width > x) { // Handle glyph like 'ffi' f32 each_cluster_width = cur_width / (f32)glyph.cluster.size(); f32 remain_width = x - total_width; total_width = 0.0f; for (unsigned j = 0; j < glyph.cluster.size(); j++) { if (total_width + each_cluster_width * 0.5 > remain_width) return glyph.cluster[j]; total_width += each_cluster_width; } return glyph.cluster.back(); } total_width += cur_width; } return gls.back().flags & GLF_RTL_CHAR ? 0 : gls.back().cluster.back() + 1; } inline std::vector getGlyphLayoutsWidthPerLine( const std::vector& gls, f32 inverse_shaping, f32 scale) { std::vector result; f32 cur_width = 0.0f; for (auto& glyph : gls) { if ((glyph.flags & GLF_NEWLINE) != 0) { result.push_back(cur_width); cur_width = 0; continue; } cur_width += (s32)(glyph.x_advance * inverse_shaping) * scale; } result.push_back(cur_width); return result; } inline void breakGlyphLayouts(std::vector& gls, f32 max_line_width, f32 inverse_shaping, f32 scale) { if (gls.size() < 2) return; auto it = gls.begin(); while (it != gls.end()) { if ((it->flags & GLF_BREAKTEXT_NEWLINE) != 0) { it = gls.erase(it); continue; } it++; } std::vector > broken_line; u32 start = 0; for (u32 i = 0; i < gls.size(); i++) { GlyphLayout& glyph = gls[i]; if ((glyph.flags & GLF_NEWLINE) != 0) { Private::breakLine({ gls.begin() + start, gls.begin() + i }, max_line_width, inverse_shaping, scale, broken_line); start = i + 1; broken_line.push_back(std::vector()); } } if (gls.size() > start) { Private::breakLine({ gls.begin() + start, gls.begin() + gls.size() }, max_line_width, inverse_shaping, scale, broken_line); } gls.clear(); // Sort glyphs in original order for (u32 i = 0; i < broken_line.size(); i++) { auto& line = broken_line[i]; std::sort(line.begin(), line.end(), [] (const irr::gui::GlyphLayout& a_gi, const irr::gui::GlyphLayout& b_gi) { return a_gi.original_index < b_gi.original_index; }); for (auto& glyph : line) gls.push_back(glyph); if (i != broken_line.size() - 1) { gui::GlyphLayout gl = { 0 }; gl.flags = gui::GLF_NEWLINE | gui::GLF_BREAKTEXT_NEWLINE; if (broken_line.at(i + 1).empty()) { gl.flags &= ~gui::GLF_BREAKTEXT_NEWLINE; i += 1; } gls.push_back(gl); } } } inline bool getDrawOffset(const core::rect& position, bool hcenter, bool vcenter, const std::vector& gls, f32 inverse_shaping, s32 font_max_height, s32 glyph_max_height, f32 scale, const core::rect* clip, core::position2d* out_offset, f32* out_next_line_height, std::vector* out_width_per_line) { core::position2d offset(f32(position.UpperLeftCorner.X), f32(position.UpperLeftCorner.Y)); core::dimension2d text_dimension; std::vector width_per_line = gui::getGlyphLayoutsWidthPerLine(gls, inverse_shaping, scale); if (width_per_line.empty()) return false; bool too_long_broken_text = false; f32 next_line_height = font_max_height * scale; if (width_per_line.size() > 1 && width_per_line.size() * next_line_height > position.getHeight()) { // Make too long broken text draw as fit as possible next_line_height = (f32)position.getHeight() / width_per_line.size(); too_long_broken_text = true; } // The offset must be round to integer when setting the offests // or * inverse_shaping, so the glyph is drawn without blurring effects if (hcenter || vcenter || clip) { text_dimension = gui::getGlyphLayoutsDimension( gls, next_line_height, inverse_shaping, scale); if (hcenter) { offset.X += (s32)( (position.getWidth() - width_per_line[0]) / 2.0f); } if (vcenter) { if (too_long_broken_text) offset.Y -= (s32) ((font_max_height - glyph_max_height) * scale); else { offset.Y += (s32)( (position.getHeight() - text_dimension.Height) / 2.0f); } } if (clip) { core::rect clippedRect(core::position2d (s32(offset.X), s32(offset.Y)), text_dimension); clippedRect.clipAgainst(*clip); if (!clippedRect.isValid()) return false; } } *out_offset = offset; *out_next_line_height = next_line_height; *out_width_per_line = width_per_line; return true; } inline void removeHighlightedURL(std::vector& gls) { for (GlyphLayout& gl : gls) gl.flags &= ~gui::GLF_URL; } inline int getGlyphIndexFromCluster(const std::vector& gls, int cluster, std::shared_ptr orig_string_in_gls = nullptr) { if (gls.empty()) return -1; if (!orig_string_in_gls) orig_string_in_gls = gls[0].orig_string; if (!orig_string_in_gls) return -1; for (int i = 0; i < (int)gls.size(); i++) { const GlyphLayout& gl = gls[i]; if (gl.orig_string != orig_string_in_gls) continue; if ((gl.flags & gui::GLF_NEWLINE) != 0) continue; for (int c : gl.cluster) { if (c == cluster) return i; } } return -1; } inline std::u32string extractURLFromGlyphLayouts(const std::vector& gls, unsigned url_glyph, int* start_cluster = NULL) { if (gls.empty() || url_glyph >= gls.size()) return U""; const gui::GlyphLayout& gl = gls[url_glyph]; if ((gl.flags & gui::GLF_URL) == 0 || !gl.orig_string || gl.cluster.empty()) return U""; std::shared_ptr orig_string_in_gls = gl.orig_string; int start = gl.cluster.back(); while (start != 0) { char32_t ch = (*orig_string_in_gls)[start - 1]; if (ch == U'\n' || ch == U'\t' || ch == U'\r') break; int gi = getGlyphIndexFromCluster(gls, start - 1, orig_string_in_gls); if (gi == -1 || gi >= (int)gls.size()) return U""; const gui::GlyphLayout& cur_gl = gls[gi]; if ((cur_gl.flags & gui::GLF_URL) == 0) break; start--; } int end = gl.cluster.front(); while ((int)orig_string_in_gls->size() - end > 1) { size_t next_end = end + 1; char32_t ch = (*orig_string_in_gls)[next_end]; if (ch == U'\n' || ch == U'\t' || ch == U'\r') break; int gi = getGlyphIndexFromCluster(gls, next_end, orig_string_in_gls); if (gi == -1 || gi >= (int)gls.size()) return U""; const gui::GlyphLayout& cur_gl = gls[gi]; if ((cur_gl.flags & gui::GLF_URL) == 0) break; end = next_end; } std::u32string url = orig_string_in_gls->substr(start, end - start + 1); if (start_cluster) *start_cluster = start; return url; } namespace Private { /** Used it only for single line (ie without line breaking mark). */ inline f32 getGlyphLayoutsWidth(const std::vector& gls, f32 inverse_shaping, f32 scale) { return std::accumulate(gls.begin(), gls.end(), 0.0f, [inverse_shaping, scale] (const f32 previous, const irr::gui::GlyphLayout& cur_gi) { return previous + (s32)(cur_gi.x_advance * inverse_shaping) * scale; }); } inline void breakLine(std::vector gls, f32 max_line_width, f32 inverse_shaping, f32 scale, std::vector >& result) { const f32 line_size = getGlyphLayoutsWidth(gls, inverse_shaping, scale); if (line_size <= max_line_width) { result.emplace_back(std::move(gls)); return; } // Sort glyphs in logical order std::sort(gls.begin(), gls.end(), [] (const GlyphLayout& a_gi, const GlyphLayout& b_gi) { return a_gi.cluster.front() < b_gi.cluster.front(); }); u32 end = 0; s32 start = 0; f32 total_width = 0.0f; for (; end < gls.size(); end++) { f32 cur_width = (s32)(gls[end].x_advance * inverse_shaping) * scale; if (cur_width > max_line_width) { // Very large glyph result.push_back({gls[end]}); start = end; } else if (cur_width + total_width <= max_line_width) { total_width += cur_width; } else { int break_point = end - 1; do { if (break_point <= 0 || break_point == start) { // Forcely break at line ending position if no break // mark, this fix text without space of out network // lobby result.push_back( {gls.begin() + start, gls.begin() + end}); start = end; total_width = (s32)(gls[end].x_advance * inverse_shaping) * scale; break; } if ((gls[break_point].flags & GLF_BREAKABLE) != 0) { result.push_back( {gls.begin() + start, gls.begin() + break_point + 1}); end = start = break_point + 1; total_width = (s32)(gls[end].x_advance * inverse_shaping) * scale; break; } break_point--; } while (break_point >= start); } } if (gls.begin() + start != gls.end()) { result.push_back({gls.begin() + start, gls.end()}); } } } } // end namespace gui } // end namespace irr #endif