TextNode.cpp 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. /*
  2. * Copyright (c) 2018-2021, Andreas Kling <kling@serenityos.org>
  3. * Copyright (c) 2022, Tobias Christiansen <tobyase@serenityos.org>
  4. *
  5. * SPDX-License-Identifier: BSD-2-Clause
  6. */
  7. #include <AK/CharacterTypes.h>
  8. #include <AK/StringBuilder.h>
  9. #include <LibGfx/Painter.h>
  10. #include <LibWeb/DOM/Document.h>
  11. #include <LibWeb/HTML/BrowsingContext.h>
  12. #include <LibWeb/Layout/BlockContainer.h>
  13. #include <LibWeb/Layout/InlineFormattingContext.h>
  14. #include <LibWeb/Layout/Label.h>
  15. #include <LibWeb/Layout/TextNode.h>
  16. #include <LibWeb/Painting/TextPaintable.h>
  17. namespace Web::Layout {
  18. TextNode::TextNode(DOM::Document& document, DOM::Text& text)
  19. : Node(document, &text)
  20. {
  21. set_inline(true);
  22. }
  23. TextNode::~TextNode()
  24. {
  25. }
  26. static bool is_all_whitespace(StringView string)
  27. {
  28. for (size_t i = 0; i < string.length(); ++i) {
  29. if (!is_ascii_space(string[i]))
  30. return false;
  31. }
  32. return true;
  33. }
  34. void TextNode::paint_text_decoration(Gfx::Painter& painter, LineBoxFragment const& fragment) const
  35. {
  36. Gfx::IntPoint line_start_point {};
  37. Gfx::IntPoint line_end_point {};
  38. auto& font = fragment.layout_node().font();
  39. auto fragment_box = enclosing_int_rect(fragment.absolute_rect());
  40. auto glyph_height = font.glyph_height();
  41. auto baseline = fragment_box.height() / 2 - (glyph_height + 4) / 2 + glyph_height;
  42. switch (computed_values().text_decoration_line()) {
  43. case CSS::TextDecorationLine::None:
  44. return;
  45. case CSS::TextDecorationLine::Underline:
  46. line_start_point = fragment_box.top_left().translated(0, baseline + 2);
  47. line_end_point = fragment_box.top_right().translated(0, baseline + 2);
  48. break;
  49. case CSS::TextDecorationLine::Overline:
  50. line_start_point = fragment_box.top_left().translated(0, baseline - glyph_height);
  51. line_end_point = fragment_box.top_right().translated(0, baseline - glyph_height);
  52. break;
  53. case CSS::TextDecorationLine::LineThrough: {
  54. auto x_height = font.x_height();
  55. line_start_point = fragment_box.top_left().translated(0, baseline - x_height / 2);
  56. line_end_point = fragment_box.top_right().translated(0, baseline - x_height / 2);
  57. break;
  58. }
  59. case CSS::TextDecorationLine::Blink:
  60. // Conforming user agents may simply not blink the text
  61. return;
  62. }
  63. auto line_color = computed_values().text_decoration_color();
  64. int line_thickness = [this] {
  65. CSS::Length computed_thickness = computed_values().text_decoration_thickness().resolved(*this, CSS::Length(1, CSS::Length::Type::Em));
  66. if (computed_thickness.is_auto())
  67. return CSS::InitialValues::text_decoration_thickness().to_px(*this);
  68. return computed_thickness.to_px(*this);
  69. }();
  70. switch (computed_values().text_decoration_style()) {
  71. case CSS::TextDecorationStyle::Solid:
  72. painter.draw_line(line_start_point, line_end_point, line_color, line_thickness, Gfx::Painter::LineStyle::Solid);
  73. break;
  74. case CSS::TextDecorationStyle::Double:
  75. switch (computed_values().text_decoration_line()) {
  76. case CSS::TextDecorationLine::Underline:
  77. break;
  78. case CSS::TextDecorationLine::Overline:
  79. line_start_point.translate_by(0, -line_thickness - 1);
  80. line_end_point.translate_by(0, -line_thickness - 1);
  81. break;
  82. case CSS::TextDecorationLine::LineThrough:
  83. line_start_point.translate_by(0, -line_thickness / 2);
  84. line_end_point.translate_by(0, -line_thickness / 2);
  85. break;
  86. default:
  87. VERIFY_NOT_REACHED();
  88. }
  89. painter.draw_line(line_start_point, line_end_point, line_color, line_thickness);
  90. painter.draw_line(line_start_point.translated(0, line_thickness + 1), line_end_point.translated(0, line_thickness + 1), line_color, line_thickness);
  91. break;
  92. case CSS::TextDecorationStyle::Dashed:
  93. painter.draw_line(line_start_point, line_end_point, line_color, line_thickness, Gfx::Painter::LineStyle::Dashed);
  94. break;
  95. case CSS::TextDecorationStyle::Dotted:
  96. painter.draw_line(line_start_point, line_end_point, line_color, line_thickness, Gfx::Painter::LineStyle::Dotted);
  97. break;
  98. case CSS::TextDecorationStyle::Wavy:
  99. painter.draw_triangle_wave(line_start_point, line_end_point, line_color, line_thickness + 1, line_thickness);
  100. break;
  101. }
  102. }
  103. void TextNode::paint_fragment(PaintContext& context, const LineBoxFragment& fragment, Painting::PaintPhase phase) const
  104. {
  105. auto& painter = context.painter();
  106. if (phase == Painting::PaintPhase::Foreground) {
  107. auto fragment_absolute_rect = fragment.absolute_rect();
  108. painter.set_font(font());
  109. if (document().inspected_node() == &dom_node())
  110. context.painter().draw_rect(enclosing_int_rect(fragment_absolute_rect), Color::Magenta);
  111. // FIXME: text-transform should be done already in layout, since uppercase glyphs may be wider than lowercase, etc.
  112. auto text = m_text_for_rendering;
  113. auto text_transform = computed_values().text_transform();
  114. if (text_transform == CSS::TextTransform::Uppercase)
  115. text = m_text_for_rendering.to_uppercase();
  116. if (text_transform == CSS::TextTransform::Lowercase)
  117. text = m_text_for_rendering.to_lowercase();
  118. // FIXME: This is a hack to prevent text clipping when painting a bitmap font into a too-small box.
  119. auto draw_rect = enclosing_int_rect(fragment_absolute_rect);
  120. draw_rect.set_height(max(draw_rect.height(), font().glyph_height()));
  121. painter.draw_text(draw_rect, text.substring_view(fragment.start(), fragment.length()), Gfx::TextAlignment::CenterLeft, computed_values().color());
  122. auto selection_rect = fragment.selection_rect(font());
  123. if (!selection_rect.is_empty()) {
  124. painter.fill_rect(enclosing_int_rect(selection_rect), context.palette().selection());
  125. Gfx::PainterStateSaver saver(painter);
  126. painter.add_clip_rect(enclosing_int_rect(selection_rect));
  127. painter.draw_text(enclosing_int_rect(fragment_absolute_rect), text.substring_view(fragment.start(), fragment.length()), Gfx::TextAlignment::CenterLeft, context.palette().selection_text());
  128. }
  129. paint_text_decoration(painter, fragment);
  130. paint_cursor_if_needed(context, fragment);
  131. }
  132. }
  133. void TextNode::paint_cursor_if_needed(PaintContext& context, const LineBoxFragment& fragment) const
  134. {
  135. if (!browsing_context().is_focused_context())
  136. return;
  137. if (!browsing_context().cursor_blink_state())
  138. return;
  139. if (browsing_context().cursor_position().node() != &dom_node())
  140. return;
  141. // NOTE: This checks if the cursor is before the start or after the end of the fragment. If it is at the end, after all text, it should still be painted.
  142. if (browsing_context().cursor_position().offset() < (unsigned)fragment.start() || browsing_context().cursor_position().offset() > (unsigned)(fragment.start() + fragment.length()))
  143. return;
  144. if (!fragment.layout_node().dom_node() || !fragment.layout_node().dom_node()->is_editable())
  145. return;
  146. auto fragment_rect = fragment.absolute_rect();
  147. float cursor_x = fragment_rect.x() + font().width(fragment.text().substring_view(0, browsing_context().cursor_position().offset() - fragment.start()));
  148. float cursor_top = fragment_rect.top();
  149. float cursor_height = fragment_rect.height();
  150. Gfx::IntRect cursor_rect(cursor_x, cursor_top, 1, cursor_height);
  151. context.painter().draw_rect(cursor_rect, computed_values().color());
  152. }
  153. // NOTE: This collapes whitespace into a single ASCII space if collapse is true. If previous_is_empty_or_ends_in_whitespace, it also strips leading whitespace.
  154. void TextNode::compute_text_for_rendering(bool collapse, bool previous_is_empty_or_ends_in_whitespace)
  155. {
  156. auto& data = dom_node().data();
  157. if (!collapse || data.is_empty()) {
  158. m_text_for_rendering = data;
  159. return;
  160. }
  161. // NOTE: A couple fast returns to avoid unnecessarily allocating a StringBuilder.
  162. if (data.length() == 1) {
  163. if (is_ascii_space(data[0])) {
  164. if (previous_is_empty_or_ends_in_whitespace)
  165. m_text_for_rendering = String::empty();
  166. else {
  167. static String s_single_space_string = " ";
  168. m_text_for_rendering = s_single_space_string;
  169. }
  170. } else {
  171. m_text_for_rendering = data;
  172. }
  173. return;
  174. }
  175. bool contains_space = false;
  176. for (auto& c : data) {
  177. if (is_ascii_space(c)) {
  178. contains_space = true;
  179. break;
  180. }
  181. }
  182. if (!contains_space) {
  183. m_text_for_rendering = data;
  184. return;
  185. }
  186. StringBuilder builder(data.length());
  187. size_t index = 0;
  188. auto skip_over_whitespace = [&index, &data] {
  189. while (index < data.length() && is_ascii_space(data[index]))
  190. ++index;
  191. };
  192. if (previous_is_empty_or_ends_in_whitespace)
  193. skip_over_whitespace();
  194. while (index < data.length()) {
  195. if (is_ascii_space(data[index])) {
  196. builder.append(' ');
  197. ++index;
  198. skip_over_whitespace();
  199. } else {
  200. builder.append(data[index]);
  201. ++index;
  202. }
  203. }
  204. m_text_for_rendering = builder.to_string();
  205. }
  206. TextNode::ChunkIterator::ChunkIterator(StringView text, LayoutMode layout_mode, bool wrap_lines, bool respect_linebreaks)
  207. : m_layout_mode(layout_mode)
  208. , m_wrap_lines(wrap_lines)
  209. , m_respect_linebreaks(respect_linebreaks)
  210. , m_utf8_view(text)
  211. , m_iterator(m_utf8_view.begin())
  212. {
  213. m_last_was_space = !text.is_empty() && is_ascii_space(*m_utf8_view.begin());
  214. }
  215. Optional<TextNode::Chunk> TextNode::ChunkIterator::next()
  216. {
  217. if (m_iterator == m_utf8_view.end())
  218. return {};
  219. auto start_of_chunk = m_iterator;
  220. while (m_iterator != m_utf8_view.end()) {
  221. ++m_iterator;
  222. if (m_last_was_newline) {
  223. // NOTE: This expression looks out for the case where we have
  224. // multiple newlines in a row. Because every output next()
  225. // that's a newline newline must be prepared for in advance by
  226. // the previous next() call, we need to check whether the next
  227. // character is a newline here as well. Otherwise, the newline
  228. // becomes part of the next expression and causes rendering
  229. // issues.
  230. m_last_was_newline = m_iterator != m_utf8_view.end() && *m_iterator == '\n';
  231. if (auto result = try_commit_chunk(start_of_chunk, m_iterator, true); result.has_value())
  232. return result.release_value();
  233. }
  234. // NOTE: The checks after this need to look at the current iterator
  235. // position, which depends on not being at the end.
  236. if (m_iterator == m_utf8_view.end())
  237. break;
  238. // NOTE: When we're supposed to stop on linebreaks, we're actually
  239. // supposed to output two chunks: "content" and "\n". Since we
  240. // can't output two chunks at once, we store this information as a
  241. // flag to output the newline immediately at the earliest
  242. // opportunity.
  243. if (m_respect_linebreaks && *m_iterator == '\n') {
  244. m_last_was_newline = true;
  245. if (auto result = try_commit_chunk(start_of_chunk, m_iterator, false); result.has_value()) {
  246. return result.release_value();
  247. }
  248. }
  249. if (m_wrap_lines || m_layout_mode == LayoutMode::AllPossibleLineBreaks) {
  250. bool is_space = is_ascii_space(*m_iterator);
  251. if (is_space != m_last_was_space) {
  252. m_last_was_space = is_space;
  253. if (auto result = try_commit_chunk(start_of_chunk, m_iterator, false); result.has_value()) {
  254. return result.release_value();
  255. }
  256. }
  257. }
  258. }
  259. if (start_of_chunk != m_utf8_view.end()) {
  260. // Try to output whatever's left at the end of the text node.
  261. if (auto result = try_commit_chunk(start_of_chunk, m_utf8_view.end(), false, true); result.has_value())
  262. return result.release_value();
  263. }
  264. return {};
  265. }
  266. Optional<TextNode::Chunk> TextNode::ChunkIterator::try_commit_chunk(Utf8View::Iterator const& start, Utf8View::Iterator const& end, bool has_breaking_newline, bool must_commit) const
  267. {
  268. if (m_layout_mode == LayoutMode::OnlyRequiredLineBreaks && !must_commit)
  269. return {};
  270. auto byte_offset = m_utf8_view.byte_offset_of(start);
  271. auto byte_length = m_utf8_view.byte_offset_of(end) - byte_offset;
  272. if (byte_length > 0) {
  273. auto chunk_view = m_utf8_view.substring_view(byte_offset, byte_length);
  274. return Chunk {
  275. .view = chunk_view,
  276. .start = byte_offset,
  277. .length = byte_length,
  278. .has_breaking_newline = has_breaking_newline,
  279. .is_all_whitespace = is_all_whitespace(chunk_view.as_string()),
  280. };
  281. }
  282. return {};
  283. }
  284. RefPtr<Painting::Paintable> TextNode::create_paintable() const
  285. {
  286. return Painting::TextPaintable::create(*this);
  287. }
  288. }