/* * Copyright (c) 2018-2021, Andreas Kling * * SPDX-License-Identifier: BSD-2-Clause */ #include #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(const StringView& string) { for (size_t i = 0; i < string.length(); ++i) { if (!isspace(string[i])) return false; } return true; } void TextNode::paint_fragment(PaintContext& context, const LineBoxFragment& fragment, PaintPhase phase) const { auto& painter = context.painter(); if (phase == PaintPhase::Background) { painter.fill_rect(enclosing_int_rect(fragment.absolute_rect()), computed_values().background_color()); } if (phase == PaintPhase::Foreground) { painter.set_font(font()); if (document().inspected_node() == &dom_node()) context.painter().draw_rect(enclosing_int_rect(fragment.absolute_rect()), Color::Magenta); if (computed_values().text_decoration_line() == CSS::TextDecorationLine::Underline) painter.draw_line(enclosing_int_rect(fragment.absolute_rect()).bottom_left().translated(0, 1), enclosing_int_rect(fragment.absolute_rect()).bottom_right().translated(0, 1), computed_values().color()); // 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(); painter.draw_text(enclosing_int_rect(fragment.absolute_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 (!frame().is_focused_frame()) return; if (!frame().cursor_blink_state()) return; if (frame().cursor_position().node() != &dom_node()) return; if (!(frame().cursor_position().offset() >= (unsigned)fragment.start() && frame().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, frame().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()); } void TextNode::compute_text_for_rendering(bool collapse, bool previous_is_empty_or_ends_in_whitespace) { if (!collapse) { m_text_for_rendering = dom_node().data(); return; } // Collapse whitespace into single spaces auto utf8_view = Utf8View(dom_node().data()); StringBuilder builder(dom_node().data().length()); auto it = utf8_view.begin(); auto skip_over_whitespace = [&] { auto prev = it; while (it != utf8_view.end() && isspace(*it)) { prev = it; ++it; } it = prev; }; if (previous_is_empty_or_ends_in_whitespace) skip_over_whitespace(); for (; it != utf8_view.end(); ++it) { if (!isspace(*it)) { builder.append(utf8_view.as_string().characters_without_null_termination() + utf8_view.byte_offset_of(it), it.code_point_length_in_bytes()); } else { builder.append(' '); skip_over_whitespace(); } } m_text_for_rendering = builder.to_string(); } void TextNode::split_into_lines_by_rules(InlineFormattingContext& context, LayoutMode layout_mode, bool do_collapse, bool do_wrap_lines, bool do_wrap_breaks) { auto& containing_block = context.containing_block(); auto& font = this->font(); auto& line_boxes = containing_block.line_boxes(); containing_block.ensure_last_line_box(); float available_width = context.available_width_at_line(line_boxes.size() - 1) - line_boxes.last().width(); compute_text_for_rendering(do_collapse, line_boxes.last().is_empty_or_ends_in_whitespace()); ChunkIterator iterator(m_text_for_rendering, layout_mode, do_wrap_lines, do_wrap_breaks); for (;;) { auto chunk_opt = iterator.next(); if (!chunk_opt.has_value()) break; auto& chunk = chunk_opt.value(); // Collapse entire fragment into non-existence if previous fragment on line ended in whitespace. if (do_collapse && line_boxes.last().is_empty_or_ends_in_whitespace() && chunk.is_all_whitespace) continue; float chunk_width; if (do_wrap_lines) { if (do_collapse && isspace(*chunk.view.begin()) && line_boxes.last().is_empty_or_ends_in_whitespace()) { // This is a non-empty chunk that starts with collapsible whitespace. // We are at either at the start of a new line, or after something that ended in whitespace, // so we don't need to contribute our own whitespace to the line. Skip over it instead! ++chunk.start; --chunk.length; chunk.view = chunk.view.substring_view(1, chunk.view.byte_length() - 1); } chunk_width = font.width(chunk.view) + font.glyph_spacing(); if (line_boxes.last().width() > 0 && chunk_width > available_width) { containing_block.add_line_box(); available_width = context.available_width_at_line(line_boxes.size() - 1); if (do_collapse && chunk.is_all_whitespace) continue; } } else { chunk_width = font.width(chunk.view); } line_boxes.last().add_fragment(*this, chunk.start, chunk.length, chunk_width, font.glyph_height()); available_width -= chunk_width; if (do_wrap_lines && available_width < 0) { containing_block.add_line_box(); available_width = context.available_width_at_line(line_boxes.size() - 1); } if (do_wrap_breaks && chunk.has_breaking_newline) { containing_block.add_line_box(); available_width = context.available_width_at_line(line_boxes.size() - 1); } } } void TextNode::split_into_lines(InlineFormattingContext& context, LayoutMode layout_mode) { bool do_collapse = true; bool do_wrap_lines = true; bool do_wrap_breaks = false; if (computed_values().white_space() == CSS::WhiteSpace::Nowrap) { do_collapse = true; do_wrap_lines = false; do_wrap_breaks = false; } else if (computed_values().white_space() == CSS::WhiteSpace::Pre) { do_collapse = false; do_wrap_lines = false; do_wrap_breaks = true; } else if (computed_values().white_space() == CSS::WhiteSpace::PreLine) { do_collapse = true; do_wrap_lines = true; do_wrap_breaks = true; } else if (computed_values().white_space() == CSS::WhiteSpace::PreWrap) { do_collapse = false; do_wrap_lines = true; do_wrap_breaks = true; } split_into_lines_by_rules(context, layout_mode, do_collapse, do_wrap_lines, do_wrap_breaks); } bool TextNode::wants_mouse_events() const { return parent() && is