/* * Copyright (c) 2018-2021, Andreas Kling * Copyright (c) 2022, Tobias Christiansen * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include #include #include #include #include namespace Web::Layout { TextNode::TextNode(DOM::Document& document, DOM::Text& text) : Node(document, &text) { set_inline(true); } TextNode::~TextNode() { } static bool is_all_whitespace(StringView string) { for (size_t i = 0; i < string.length(); ++i) { if (!is_ascii_space(string[i])) return false; } return true; } void TextNode::paint_text_decoration(Gfx::Painter& painter, LineBoxFragment const& fragment) const { Gfx::IntPoint line_start_point {}; Gfx::IntPoint line_end_point {}; auto& font = fragment.layout_node().font(); auto fragment_box = enclosing_int_rect(fragment.absolute_rect()); auto glyph_height = font.glyph_height(); auto baseline = fragment_box.height() / 2 - (glyph_height + 4) / 2 + glyph_height; switch (computed_values().text_decoration_line()) { case CSS::TextDecorationLine::None: return; case CSS::TextDecorationLine::Underline: line_start_point = fragment_box.top_left().translated(0, baseline + 2); line_end_point = fragment_box.top_right().translated(0, baseline + 2); break; case CSS::TextDecorationLine::Overline: line_start_point = fragment_box.top_left().translated(0, baseline - glyph_height); line_end_point = fragment_box.top_right().translated(0, baseline - glyph_height); break; case CSS::TextDecorationLine::LineThrough: { auto x_height = font.x_height(); line_start_point = fragment_box.top_left().translated(0, baseline - x_height / 2); line_end_point = fragment_box.top_right().translated(0, baseline - x_height / 2); break; } case CSS::TextDecorationLine::Blink: // Conforming user agents may simply not blink the text return; } auto line_color = computed_values().text_decoration_color(); switch (computed_values().text_decoration_style()) { // FIXME: Implement the other styles case CSS::TextDecorationStyle::Solid: case CSS::TextDecorationStyle::Double: case CSS::TextDecorationStyle::Dashed: case CSS::TextDecorationStyle::Dotted: painter.draw_line(line_start_point, line_end_point, line_color); break; case CSS::TextDecorationStyle::Wavy: // FIXME: There is a thing called text-decoration-thickness which also affects the amplitude here. painter.draw_triangle_wave(line_start_point, line_end_point, line_color, 2); break; } } void TextNode::paint_fragment(PaintContext& context, const LineBoxFragment& fragment, PaintPhase phase) const { auto& painter = context.painter(); if (phase == PaintPhase::Foreground) { auto fragment_absolute_rect = fragment.absolute_rect(); painter.set_font(font()); if (document().inspected_node() == &dom_node()) context.painter().draw_rect(enclosing_int_rect(fragment_absolute_rect), Color::Magenta); paint_text_decoration(painter, fragment); // FIXME: text-transform should be done already in layout, since uppercase glyphs may be wider than lowercase, etc. auto text = m_text_for_rendering; auto text_transform = computed_values().text_transform(); if (text_transform == CSS::TextTransform::Uppercase) text = m_text_for_rendering.to_uppercase(); if (text_transform == CSS::TextTransform::Lowercase) text = m_text_for_rendering.to_lowercase(); // FIXME: This is a hack to prevent text clipping when painting a bitmap font into a too-small box. auto draw_rect = enclosing_int_rect(fragment_absolute_rect); draw_rect.set_height(max(draw_rect.height(), font().glyph_height())); painter.draw_text(draw_rect, text.substring_view(fragment.start(), fragment.length()), Gfx::TextAlignment::CenterLeft, computed_values().color()); auto selection_rect = fragment.selection_rect(font()); if (!selection_rect.is_empty()) { painter.fill_rect(enclosing_int_rect(selection_rect), context.palette().selection()); Gfx::PainterStateSaver saver(painter); painter.add_clip_rect(enclosing_int_rect(selection_rect)); painter.draw_text(enclosing_int_rect(fragment_absolute_rect), text.substring_view(fragment.start(), fragment.length()), Gfx::TextAlignment::CenterLeft, context.palette().selection_text()); } paint_cursor_if_needed(context, fragment); } } void TextNode::paint_cursor_if_needed(PaintContext& context, const LineBoxFragment& fragment) const { if (!browsing_context().is_focused_context()) return; if (!browsing_context().cursor_blink_state()) return; if (browsing_context().cursor_position().node() != &dom_node()) return; // 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. if (browsing_context().cursor_position().offset() < (unsigned)fragment.start() || browsing_context().cursor_position().offset() > (unsigned)(fragment.start() + fragment.length())) return; if (!fragment.layout_node().dom_node() || !fragment.layout_node().dom_node()->is_editable()) return; auto fragment_rect = fragment.absolute_rect(); float cursor_x = fragment_rect.x() + font().width(fragment.text().substring_view(0, browsing_context().cursor_position().offset() - fragment.start())); float cursor_top = fragment_rect.top(); float cursor_height = fragment_rect.height(); Gfx::IntRect cursor_rect(cursor_x, cursor_top, 1, cursor_height); context.painter().draw_rect(cursor_rect, computed_values().color()); } // 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. void TextNode::compute_text_for_rendering(bool collapse, bool previous_is_empty_or_ends_in_whitespace) { auto& data = dom_node().data(); if (!collapse || data.is_empty()) { m_text_for_rendering = data; return; } // NOTE: A couple fast returns to avoid unnecessarily allocating a StringBuilder. if (data.length() == 1) { if (is_ascii_space(data[0])) { if (previous_is_empty_or_ends_in_whitespace) m_text_for_rendering = String::empty(); else { static String s_single_space_string = " "; m_text_for_rendering = s_single_space_string; } } else { m_text_for_rendering = data; } return; } bool contains_space = false; for (auto& c : data) { if (is_ascii_space(c)) { contains_space = true; break; } } if (!contains_space) { m_text_for_rendering = data; return; } StringBuilder builder(data.length()); size_t index = 0; auto skip_over_whitespace = [&index, &data] { while (index < data.length() && is_ascii_space(data[index])) ++index; }; if (previous_is_empty_or_ends_in_whitespace) skip_over_whitespace(); while (index < data.length()) { if (is_ascii_space(data[index])) { builder.append(' '); ++index; skip_over_whitespace(); } else { builder.append(data[index]); ++index; } } m_text_for_rendering = builder.to_string(); } bool TextNode::wants_mouse_events() const { return first_ancestor_of_type