TextLayout.cpp 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. /*
  2. * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
  3. * Copyright (c) 2021, sin-ack <sin-ack@protonmail.com>
  4. *
  5. * SPDX-License-Identifier: BSD-2-Clause
  6. */
  7. #include "TextLayout.h"
  8. namespace Gfx {
  9. enum class BlockType {
  10. Newline,
  11. Whitespace,
  12. Word
  13. };
  14. struct Block {
  15. BlockType type;
  16. Utf8View characters;
  17. };
  18. FloatRect TextLayout::bounding_rect(TextWrapping wrapping) const
  19. {
  20. auto lines = wrap_lines(TextElision::None, wrapping);
  21. if (lines.is_empty()) {
  22. return {};
  23. }
  24. FloatRect bounding_rect = {
  25. 0, 0, 0, (static_cast<float>(lines.size()) * (m_font_metrics.ascent + m_font_metrics.descent + m_font_metrics.line_gap)) - m_font_metrics.line_gap
  26. };
  27. for (auto& line : lines) {
  28. auto line_width = m_font.width(line);
  29. if (line_width > bounding_rect.width())
  30. bounding_rect.set_width(line_width);
  31. }
  32. return bounding_rect;
  33. }
  34. Vector<DeprecatedString, 32> TextLayout::wrap_lines(TextElision elision, TextWrapping wrapping) const
  35. {
  36. Vector<Block> blocks;
  37. Optional<BlockType> current_block_type;
  38. size_t block_start_offset = 0;
  39. size_t offset = 0;
  40. for (auto it = m_text.begin(); !it.done(); ++it) {
  41. offset = m_text.iterator_offset(it);
  42. switch (*it) {
  43. case '\t':
  44. case ' ': {
  45. if (current_block_type.has_value() && current_block_type.value() != BlockType::Whitespace) {
  46. blocks.append({
  47. current_block_type.value(),
  48. m_text.substring_view(block_start_offset, offset - block_start_offset),
  49. });
  50. current_block_type.clear();
  51. }
  52. if (!current_block_type.has_value()) {
  53. current_block_type = BlockType::Whitespace;
  54. block_start_offset = offset;
  55. }
  56. continue;
  57. }
  58. case '\n':
  59. case '\r': {
  60. if (current_block_type.has_value()) {
  61. blocks.append({
  62. current_block_type.value(),
  63. m_text.substring_view(block_start_offset, offset - block_start_offset),
  64. });
  65. current_block_type.clear();
  66. }
  67. blocks.append({ BlockType::Newline, Utf8View {} });
  68. continue;
  69. }
  70. default: {
  71. if (current_block_type.has_value() && current_block_type.value() != BlockType::Word) {
  72. blocks.append({
  73. current_block_type.value(),
  74. m_text.substring_view(block_start_offset, offset - block_start_offset),
  75. });
  76. current_block_type.clear();
  77. }
  78. if (!current_block_type.has_value()) {
  79. current_block_type = BlockType::Word;
  80. block_start_offset = offset;
  81. }
  82. }
  83. }
  84. }
  85. if (current_block_type.has_value()) {
  86. blocks.append({
  87. current_block_type.value(),
  88. m_text.substring_view(block_start_offset, m_text.byte_length() - block_start_offset),
  89. });
  90. }
  91. Vector<DeprecatedString> lines;
  92. StringBuilder builder;
  93. float line_width = 0;
  94. size_t current_block = 0;
  95. for (Block& block : blocks) {
  96. switch (block.type) {
  97. case BlockType::Newline: {
  98. lines.append(builder.to_deprecated_string());
  99. builder.clear();
  100. line_width = 0;
  101. current_block++;
  102. continue;
  103. }
  104. case BlockType::Whitespace:
  105. case BlockType::Word: {
  106. float block_width = m_font.width(block.characters);
  107. // FIXME: This should look at the specific advance amount of the
  108. // last character, but we don't support that yet.
  109. if (current_block != blocks.size() - 1) {
  110. block_width += m_font.glyph_spacing();
  111. }
  112. if (wrapping == TextWrapping::Wrap && line_width + block_width > m_rect.width()) {
  113. lines.append(builder.to_deprecated_string());
  114. builder.clear();
  115. line_width = 0;
  116. }
  117. builder.append(block.characters.as_string());
  118. line_width += block_width;
  119. current_block++;
  120. }
  121. }
  122. }
  123. auto last_line = builder.to_deprecated_string();
  124. if (!last_line.is_empty())
  125. lines.append(last_line);
  126. switch (elision) {
  127. case TextElision::None:
  128. break;
  129. case TextElision::Right: {
  130. lines.at(lines.size() - 1) = elide_text_from_right(Utf8View { lines.at(lines.size() - 1) });
  131. break;
  132. }
  133. }
  134. return lines;
  135. }
  136. DeprecatedString TextLayout::elide_text_from_right(Utf8View text) const
  137. {
  138. float text_width = m_font.width(text);
  139. if (text_width > static_cast<float>(m_rect.width())) {
  140. float ellipsis_width = m_font.width("..."sv);
  141. float current_width = ellipsis_width;
  142. size_t glyph_spacing = m_font.glyph_spacing();
  143. // FIXME: This code will break when the font has glyphs with advance
  144. // amounts different from the actual width of the glyph
  145. // (which is the case with many TrueType fonts).
  146. if (ellipsis_width < text_width) {
  147. size_t offset = 0;
  148. for (auto it = text.begin(); !it.done(); ++it) {
  149. auto glyph_width = m_font.glyph_or_emoji_width(it);
  150. // NOTE: Glyph spacing should not be added after the last glyph on the line,
  151. // but since we are here because the last glyph does not actually fit on the line,
  152. // we don't have to worry about spacing.
  153. auto width_with_this_glyph_included = current_width + glyph_width + glyph_spacing;
  154. if (width_with_this_glyph_included > m_rect.width())
  155. break;
  156. current_width += glyph_width + glyph_spacing;
  157. offset = text.iterator_offset(it);
  158. }
  159. StringBuilder builder;
  160. builder.append(text.substring_view(0, offset).as_string());
  161. builder.append("..."sv);
  162. return builder.to_deprecated_string();
  163. }
  164. }
  165. return text.as_string();
  166. }
  167. }