| // Copyright 2014 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "third_party/blink/renderer/core/paint/inline_text_box_painter.h" |
| |
| #include "base/optional.h" |
| #include "third_party/blink/renderer/core/editing/editor.h" |
| #include "third_party/blink/renderer/core/editing/markers/composition_marker.h" |
| #include "third_party/blink/renderer/core/editing/markers/document_marker_controller.h" |
| #include "third_party/blink/renderer/core/editing/markers/text_match_marker.h" |
| #include "third_party/blink/renderer/core/frame/local_frame.h" |
| #include "third_party/blink/renderer/core/layout/api/line_layout_api_shim.h" |
| #include "third_party/blink/renderer/core/layout/api/line_layout_box.h" |
| #include "third_party/blink/renderer/core/layout/layout_text_combine.h" |
| #include "third_party/blink/renderer/core/layout/layout_theme.h" |
| #include "third_party/blink/renderer/core/layout/line/inline_text_box.h" |
| #include "third_party/blink/renderer/core/layout/text_decoration_offset.h" |
| #include "third_party/blink/renderer/core/paint/applied_decoration_painter.h" |
| #include "third_party/blink/renderer/core/paint/decoration_info.h" |
| #include "third_party/blink/renderer/core/paint/document_marker_painter.h" |
| #include "third_party/blink/renderer/core/paint/paint_info.h" |
| #include "third_party/blink/renderer/core/paint/selection_painting_utils.h" |
| #include "third_party/blink/renderer/core/paint/text_painter.h" |
| #include "third_party/blink/renderer/platform/graphics/graphics_context_state_saver.h" |
| #include "third_party/blink/renderer/platform/graphics/paint/drawing_recorder.h" |
| #include "third_party/blink/renderer/platform/graphics/paint/paint_record.h" |
| #include "third_party/blink/renderer/platform/graphics/paint/paint_recorder.h" |
| #include "third_party/blink/renderer/platform/graphics/paint/paint_shader.h" |
| #include "third_party/skia/include/effects/SkGradientShader.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| // If an inline text box is truncated by an ellipsis, text box markers paint |
| // over the ellipsis and other marker types don't. Other marker types that want |
| // the normal behavior should use MarkerPaintStartAndEnd(). |
| std::pair<unsigned, unsigned> GetTextMatchMarkerPaintOffsets( |
| const DocumentMarker& marker, |
| const InlineTextBox& text_box) { |
| // text_box.Start() returns an offset relative to the start of the layout |
| // object. We add the LineLayoutItem's TextStartOffset() to get a DOM offset |
| // (which is what DocumentMarker uses). This is necessary to get proper |
| // behavior with the :first-letter psuedo element. |
| const unsigned text_box_start = |
| text_box.Start() + text_box.GetLineLayoutItem().TextStartOffset(); |
| |
| DCHECK_EQ(DocumentMarker::kTextMatch, marker.GetType()); |
| const unsigned start_offset = marker.StartOffset() > text_box_start |
| ? marker.StartOffset() - text_box_start |
| : 0U; |
| const unsigned end_offset = |
| std::min(marker.EndOffset() - text_box_start, text_box.Len()); |
| return std::make_pair(start_offset, end_offset); |
| } |
| |
| } // anonymous namespace |
| |
| static LineLayoutItem EnclosingUnderlineObject( |
| const InlineTextBox* inline_text_box) { |
| bool first_line = inline_text_box->IsFirstLineStyle(); |
| for (LineLayoutItem current = inline_text_box->Parent()->GetLineLayoutItem(); |
| ;) { |
| if (current.IsLayoutBlock()) |
| return current; |
| if (!current.IsLayoutInline() || current.IsRubyText()) |
| return nullptr; |
| |
| const ComputedStyle& style_to_use = current.StyleRef(first_line); |
| if (EnumHasFlags(style_to_use.GetTextDecoration(), |
| TextDecoration::kUnderline)) |
| return current; |
| |
| current = current.Parent(); |
| if (!current) |
| return current; |
| |
| if (Node* node = current.GetNode()) { |
| if (IsHTMLAnchorElement(node) || node->HasTagName(HTMLNames::fontTag)) |
| return current; |
| } |
| } |
| } |
| |
| LayoutObject& InlineTextBoxPainter::InlineLayoutObject() const { |
| return *LineLayoutAPIShim::LayoutObjectFrom( |
| inline_text_box_.GetLineLayoutItem()); |
| } |
| |
| bool InlineTextBoxPainter::PaintsMarkerHighlights( |
| const LayoutObject& layout_object) { |
| return layout_object.GetNode() && |
| layout_object.GetDocument().Markers().HasMarkers( |
| layout_object.GetNode()); |
| } |
| |
| static void ComputeOriginAndWidthForBox(const InlineTextBox& box, |
| LayoutPoint& local_origin, |
| LayoutUnit& width) { |
| if (box.Truncation() != kCNoTruncation) { |
| bool ltr = box.IsLeftToRightDirection(); |
| bool flow_is_ltr = |
| box.GetLineLayoutItem().StyleRef().IsLeftToRightDirection(); |
| width = LayoutUnit(box.GetLineLayoutItem().Width( |
| ltr == flow_is_ltr ? box.Start() : box.Start() + box.Truncation(), |
| ltr == flow_is_ltr ? box.Truncation() : box.Len() - box.Truncation(), |
| box.TextPos(), flow_is_ltr ? TextDirection::kLtr : TextDirection::kRtl, |
| box.IsFirstLineStyle())); |
| if (!flow_is_ltr) { |
| local_origin.Move(box.LogicalWidth() - width, LayoutUnit()); |
| } |
| } |
| } |
| |
| void InlineTextBoxPainter::Paint(const PaintInfo& paint_info, |
| const LayoutPoint& paint_offset) { |
| if (!ShouldPaintTextBox(paint_info)) |
| return; |
| |
| DCHECK(!ShouldPaintSelfOutline(paint_info.phase) && |
| !ShouldPaintDescendantOutlines(paint_info.phase)); |
| |
| LayoutRect logical_visual_overflow = inline_text_box_.LogicalOverflowRect(); |
| LayoutUnit logical_start = |
| logical_visual_overflow.X() + |
| (inline_text_box_.IsHorizontal() ? paint_offset.X() : paint_offset.Y()); |
| LayoutUnit logical_extent = logical_visual_overflow.Width(); |
| |
| if (inline_text_box_.IsHorizontal()) { |
| if (!paint_info.GetCullRect().IntersectsHorizontalRange( |
| logical_start, logical_start + logical_extent)) |
| return; |
| } else { |
| if (!paint_info.GetCullRect().IntersectsVerticalRange( |
| logical_start, logical_start + logical_extent)) |
| return; |
| } |
| |
| bool is_printing = paint_info.IsPrinting(); |
| |
| // Determine whether or not we're selected. |
| bool have_selection = |
| !is_printing && paint_info.phase != PaintPhase::kTextClip && |
| inline_text_box_.GetSelectionState() != SelectionState::kNone; |
| if (!have_selection && paint_info.phase == PaintPhase::kSelection) { |
| // When only painting the selection, don't bother to paint if there is none. |
| return; |
| } |
| |
| // The text clip phase already has a DrawingRecorder. Text clips are initiated |
| // only in BoxPainter::PaintFillLayer, which is already within a |
| // DrawingRecorder. |
| base::Optional<DrawingRecorder> recorder; |
| if (paint_info.phase != PaintPhase::kTextClip) { |
| if (DrawingRecorder::UseCachedDrawingIfPossible( |
| paint_info.context, inline_text_box_, |
| DisplayItem::PaintPhaseToDrawingType(paint_info.phase))) |
| return; |
| recorder.emplace(paint_info.context, inline_text_box_, |
| DisplayItem::PaintPhaseToDrawingType(paint_info.phase)); |
| } |
| |
| GraphicsContext& context = paint_info.context; |
| const ComputedStyle& style_to_use = |
| inline_text_box_.GetLineLayoutItem().StyleRef( |
| inline_text_box_.IsFirstLineStyle()); |
| |
| LayoutPoint box_origin(inline_text_box_.PhysicalLocation() + paint_offset); |
| |
| // We round the y-axis to ensure consistent line heights. |
| if (inline_text_box_.IsHorizontal()) { |
| box_origin.SetY(LayoutUnit(box_origin.Y().Round())); |
| } else { |
| box_origin.SetX(LayoutUnit(box_origin.X().Round())); |
| } |
| |
| LayoutRect box_rect(box_origin, LayoutSize(inline_text_box_.LogicalWidth(), |
| inline_text_box_.LogicalHeight())); |
| |
| unsigned length = inline_text_box_.Len(); |
| const String& layout_item_string = |
| inline_text_box_.GetLineLayoutItem().GetText(); |
| |
| String first_line_string; |
| if (inline_text_box_.IsFirstLineStyle()) { |
| first_line_string = layout_item_string; |
| const ComputedStyle& style = inline_text_box_.GetLineLayoutItem().StyleRef( |
| inline_text_box_.IsFirstLineStyle()); |
| style.ApplyTextTransform( |
| &first_line_string, |
| inline_text_box_.GetLineLayoutItem().PreviousCharacter()); |
| // TODO(crbug.com/795498): this is a hack. The root issue is that |
| // capitalizing letters can change the length of the backing string. |
| // That needs to be taken into account when computing the size of the box |
| // or its painting. |
| length = std::min(length, first_line_string.length()); |
| |
| // TODO(szager): Figure out why this CHECK sometimes fails, it shouldn't. |
| CHECK(inline_text_box_.Start() + length <= first_line_string.length()); |
| } else { |
| // TODO(szager): Figure out why this CHECK sometimes fails, it shouldn't. |
| CHECK(inline_text_box_.Start() + length <= layout_item_string.length()); |
| } |
| StringView string = |
| StringView(inline_text_box_.IsFirstLineStyle() ? first_line_string |
| : layout_item_string, |
| inline_text_box_.Start(), length); |
| int maximum_length = inline_text_box_.GetLineLayoutItem().TextLength() - |
| inline_text_box_.Start(); |
| |
| StringBuilder characters_with_hyphen; |
| TextRun text_run = inline_text_box_.ConstructTextRun( |
| style_to_use, string, maximum_length, |
| inline_text_box_.HasHyphen() ? &characters_with_hyphen : nullptr); |
| if (inline_text_box_.HasHyphen()) |
| length = text_run.length(); |
| |
| bool should_rotate = false; |
| LayoutTextCombine* combined_text = nullptr; |
| if (!inline_text_box_.IsHorizontal()) { |
| if (style_to_use.HasTextCombine() && |
| inline_text_box_.GetLineLayoutItem().IsCombineText()) { |
| combined_text = &ToLayoutTextCombine(InlineLayoutObject()); |
| if (!combined_text->IsCombined()) |
| combined_text = nullptr; |
| } |
| if (combined_text) { |
| box_rect.SetWidth(combined_text->InlineWidthForLayout()); |
| // Justfication applies to before and after the combined text as if |
| // it is an ideographic character, and is prohibited inside the |
| // combined text. |
| if (float expansion = text_run.Expansion()) { |
| text_run.SetExpansion(0); |
| if (text_run.AllowsLeadingExpansion()) { |
| if (text_run.AllowsTrailingExpansion()) |
| expansion /= 2; |
| LayoutSize offset = |
| LayoutSize(LayoutUnit(), LayoutUnit::FromFloatRound(expansion)); |
| box_origin.Move(offset); |
| box_rect.Move(offset); |
| } |
| } |
| } else { |
| should_rotate = true; |
| context.ConcatCTM( |
| TextPainterBase::Rotation(box_rect, TextPainterBase::kClockwise)); |
| } |
| } |
| |
| // Determine text colors. |
| TextPaintStyle text_style = TextPainterBase::TextPaintingStyle( |
| inline_text_box_.GetLineLayoutItem().GetDocument(), style_to_use, |
| paint_info); |
| TextPaintStyle selection_style = TextPainterBase::SelectionPaintingStyle( |
| inline_text_box_.GetLineLayoutItem().GetDocument(), style_to_use, |
| inline_text_box_.GetLineLayoutItem().GetNode(), have_selection, |
| paint_info, text_style); |
| bool paint_selected_text_only = (paint_info.phase == PaintPhase::kSelection); |
| bool paint_selected_text_separately = |
| !paint_selected_text_only && text_style != selection_style; |
| |
| // Set our font. |
| const Font& font = style_to_use.GetFont(); |
| const SimpleFontData* font_data = font.PrimaryFont(); |
| DCHECK(font_data); |
| |
| int ascent = font_data ? font_data->GetFontMetrics().Ascent() : 0; |
| LayoutPoint text_origin(box_origin.X(), box_origin.Y() + ascent); |
| |
| const DocumentMarkerVector& markers_to_paint = ComputeMarkersToPaint(); |
| |
| // 1. Paint backgrounds behind text if needed. Examples of such backgrounds |
| // include selection and composition highlights. |
| if (paint_info.phase != PaintPhase::kSelection && |
| paint_info.phase != PaintPhase::kTextClip && !is_printing) { |
| PaintDocumentMarkers(markers_to_paint, paint_info, box_origin, style_to_use, |
| font, DocumentMarkerPaintPhase::kBackground); |
| if (have_selection) { |
| if (combined_text) |
| PaintSelection<InlineTextBoxPainter::PaintOptions::kCombinedText>( |
| context, box_rect, style_to_use, font, selection_style.fill_color, |
| combined_text); |
| else |
| PaintSelection<InlineTextBoxPainter::PaintOptions::kNormal>( |
| context, box_rect, style_to_use, font, selection_style.fill_color); |
| } |
| } |
| |
| // 2. Now paint the foreground, including text and decorations. |
| int selection_start = 0; |
| int selection_end = 0; |
| if (paint_selected_text_only || paint_selected_text_separately) |
| inline_text_box_.SelectionStartEnd(selection_start, selection_end); |
| |
| bool respect_hyphen = |
| selection_end == static_cast<int>(inline_text_box_.Len()) && |
| inline_text_box_.HasHyphen(); |
| if (respect_hyphen) |
| selection_end = text_run.length(); |
| |
| bool ltr = inline_text_box_.IsLeftToRightDirection(); |
| bool flow_is_ltr = inline_text_box_.GetLineLayoutItem() |
| .ContainingBlock() |
| .Style() |
| ->IsLeftToRightDirection(); |
| |
| const PaintOffsets& selection_offsets = |
| ApplyTruncationToPaintOffsets({static_cast<unsigned>(selection_start), |
| static_cast<unsigned>(selection_end)}); |
| selection_start = selection_offsets.start; |
| selection_end = selection_offsets.end; |
| if (have_selection) { |
| font.ExpandRangeToIncludePartialGlyphs(text_run, &selection_start, |
| &selection_end); |
| } |
| |
| if (inline_text_box_.Truncation() != kCNoTruncation) { |
| // In a mixed-direction flow the ellipsis is at the start of the text |
| // rather than at the end of it. |
| length = |
| ltr == flow_is_ltr ? inline_text_box_.Truncation() : text_run.length(); |
| } |
| |
| TextPainter text_painter(context, font, text_run, text_origin, box_rect, |
| inline_text_box_.IsHorizontal()); |
| TextEmphasisPosition emphasis_mark_position; |
| bool has_text_emphasis = inline_text_box_.GetEmphasisMarkPosition( |
| style_to_use, emphasis_mark_position); |
| if (has_text_emphasis) |
| text_painter.SetEmphasisMark(style_to_use.TextEmphasisMarkString(), |
| emphasis_mark_position); |
| if (combined_text) |
| text_painter.SetCombinedText(combined_text); |
| if (inline_text_box_.Truncation() != kCNoTruncation && ltr != flow_is_ltr) |
| text_painter.SetEllipsisOffset(inline_text_box_.Truncation()); |
| |
| if (!paint_selected_text_only) { |
| // Paint text decorations except line-through. |
| DecorationInfo decoration_info; |
| bool has_line_through_decoration = false; |
| if (style_to_use.TextDecorationsInEffect() != TextDecoration::kNone && |
| inline_text_box_.Truncation() != kCFullTruncation) { |
| LayoutPoint local_origin = LayoutPoint(box_origin); |
| LayoutUnit width = inline_text_box_.LogicalWidth(); |
| ComputeOriginAndWidthForBox(inline_text_box_, local_origin, width); |
| const LineLayoutItem& decorating_box = |
| EnclosingUnderlineObject(&inline_text_box_); |
| const ComputedStyle* decorating_box_style = |
| decorating_box ? decorating_box.Style() : nullptr; |
| text_painter.ComputeDecorationInfo(decoration_info, box_origin, |
| local_origin, width, |
| inline_text_box_.Root().BaselineType(), |
| style_to_use, decorating_box_style); |
| TextDecorationOffset decoration_offset(*decoration_info.style, |
| &inline_text_box_, decorating_box); |
| text_painter.PaintDecorationsExceptLineThrough( |
| decoration_offset, decoration_info, paint_info, |
| style_to_use.AppliedTextDecorations(), text_style, |
| &has_line_through_decoration); |
| } |
| |
| int start_offset = 0; |
| int end_offset = length; |
| // Where the text and its flow have opposite directions then our offset into |
| // the text given by |truncation| is at the start of the part that will be |
| // visible. |
| if (inline_text_box_.Truncation() != kCNoTruncation && ltr != flow_is_ltr) { |
| start_offset = inline_text_box_.Truncation(); |
| end_offset = text_run.length(); |
| } |
| |
| if (paint_selected_text_separately && selection_start < selection_end) { |
| start_offset = selection_end; |
| end_offset = selection_start; |
| } |
| text_painter.Paint(start_offset, end_offset, length, text_style); |
| |
| // Paint line-through decoration if needed. |
| if (has_line_through_decoration) { |
| text_painter.PaintDecorationsOnlyLineThrough( |
| decoration_info, paint_info, style_to_use.AppliedTextDecorations(), |
| text_style); |
| } |
| } |
| |
| if ((paint_selected_text_only || paint_selected_text_separately) && |
| selection_start < selection_end) { |
| // paint only the text that is selected. |
| // Because only a part of the text glyph can be selected, we need to draw |
| // the selection twice: |
| LayoutRect selection_rect = |
| GetSelectionRect<InlineTextBoxPainter::PaintOptions::kNormal>( |
| context, box_rect, style_to_use, font); |
| |
| // the first time, we draw the glyphs outside the selection area, with |
| // the original style. |
| { |
| GraphicsContextStateSaver state_saver(context); |
| context.ClipOut(FloatRect(selection_rect)); |
| text_painter.Paint(selection_start, selection_end, length, text_style); |
| } |
| // the second time, we draw the glyphs inside the selection area, with |
| // the selection style. |
| { |
| GraphicsContextStateSaver state_saver(context); |
| context.Clip(FloatRect(selection_rect)); |
| text_painter.Paint(selection_start, selection_end, length, |
| selection_style); |
| } |
| } |
| |
| if (paint_info.phase == PaintPhase::kForeground) { |
| PaintDocumentMarkers(markers_to_paint, paint_info, box_origin, style_to_use, |
| font, DocumentMarkerPaintPhase::kForeground); |
| } |
| |
| if (should_rotate) { |
| context.ConcatCTM(TextPainterBase::Rotation( |
| box_rect, TextPainterBase::kCounterclockwise)); |
| } |
| } |
| |
| bool InlineTextBoxPainter::ShouldPaintTextBox(const PaintInfo& paint_info) { |
| // When painting selection, we want to include a highlight when the |
| // selection spans line breaks. In other cases such as invisible elements |
| // or those with no text that are not line breaks, we can skip painting |
| // wholesale. |
| // TODO(wkorman): Constrain line break painting to appropriate paint phase. |
| // This code path is only called in PaintPhaseForeground whereas we would |
| // expect PaintPhaseSelection. The existing haveSelection logic in paint() |
| // tests for != PaintPhaseTextClip. |
| if (inline_text_box_.GetLineLayoutItem().StyleRef().Visibility() != |
| EVisibility::kVisible || |
| inline_text_box_.Truncation() == kCFullTruncation || |
| !inline_text_box_.Len()) |
| return false; |
| return true; |
| } |
| |
| InlineTextBoxPainter::PaintOffsets |
| InlineTextBoxPainter::ApplyTruncationToPaintOffsets( |
| const InlineTextBoxPainter::PaintOffsets& offsets) { |
| const unsigned short truncation = inline_text_box_.Truncation(); |
| if (truncation == kCNoTruncation) |
| return offsets; |
| |
| // If we're in mixed-direction mode (LTR text in an RTL box or vice-versa), |
| // the truncation ellipsis is at the *start* of the text box rather than the |
| // end. |
| bool ltr = inline_text_box_.IsLeftToRightDirection(); |
| bool flow_is_ltr = inline_text_box_.GetLineLayoutItem() |
| .ContainingBlock() |
| .Style() |
| ->IsLeftToRightDirection(); |
| |
| // truncation is relative to the start of the InlineTextBox, not the text |
| // node. |
| if (ltr == flow_is_ltr) { |
| return {std::min<unsigned>(offsets.start, truncation), |
| std::min<unsigned>(offsets.end, truncation)}; |
| } |
| |
| return {std::max<unsigned>(offsets.start, truncation), |
| std::max<unsigned>(offsets.end, truncation)}; |
| } |
| |
| InlineTextBoxPainter::PaintOffsets InlineTextBoxPainter::MarkerPaintStartAndEnd( |
| const DocumentMarker& marker) { |
| // Text match markers are painted differently (in an inline text box truncated |
| // by an ellipsis, they paint over the ellipsis) and so should not use this |
| // function. |
| DCHECK_NE(DocumentMarker::kTextMatch, marker.GetType()); |
| DCHECK(inline_text_box_.Truncation() != kCFullTruncation); |
| DCHECK(inline_text_box_.Len()); |
| |
| // inline_text_box_.Start() returns an offset relative to the start of the |
| // layout object. We add the LineLayoutItem's TextStartOffset() to get a DOM |
| // offset (which is what DocumentMarker uses). This is necessary to get proper |
| // behavior with the :first-letter psuedo element. |
| const unsigned inline_text_box_start = |
| inline_text_box_.Start() + |
| inline_text_box_.GetLineLayoutItem().TextStartOffset(); |
| |
| // Start painting at the beginning of the text or the specified underline |
| // start offset, whichever is greater. |
| unsigned paint_start = std::max(inline_text_box_start, marker.StartOffset()); |
| // Cap the maximum paint start to the last character in the text box. |
| paint_start = std::min(paint_start, inline_text_box_.end()); |
| |
| // End painting just past the end of the text or the specified underline end |
| // offset, whichever is less. |
| unsigned paint_end = std::min( |
| inline_text_box_.end() + 1, |
| marker.EndOffset()); // end() points at the last char, not past it. |
| |
| // paint_start and paint_end are currently relative to the start of the text |
| // node. Subtract to make them relative to the start of the InlineTextBox. |
| paint_start -= inline_text_box_start; |
| paint_end -= inline_text_box_start; |
| |
| return ApplyTruncationToPaintOffsets({paint_start, paint_end}); |
| } |
| |
| void InlineTextBoxPainter::PaintSingleMarkerBackgroundRun( |
| GraphicsContext& context, |
| const LayoutPoint& box_origin, |
| const ComputedStyle& style, |
| const Font& font, |
| Color background_color, |
| int start_pos, |
| int end_pos) { |
| if (background_color == Color::kTransparent) |
| return; |
| |
| int delta_y = (inline_text_box_.GetLineLayoutItem() |
| .StyleRef() |
| .IsFlippedLinesWritingMode() |
| ? inline_text_box_.Root().SelectionBottom() - |
| inline_text_box_.LogicalBottom() |
| : inline_text_box_.LogicalTop() - |
| inline_text_box_.Root().SelectionTop()) |
| .ToInt(); |
| int sel_height = inline_text_box_.Root().SelectionHeight().ToInt(); |
| FloatPoint local_origin(box_origin.X().ToFloat(), |
| box_origin.Y().ToFloat() - delta_y); |
| context.DrawHighlightForText(font, inline_text_box_.ConstructTextRun(style), |
| local_origin, sel_height, background_color, |
| start_pos, end_pos); |
| } |
| |
| DocumentMarkerVector InlineTextBoxPainter::ComputeMarkersToPaint() const { |
| Node* const node = inline_text_box_.GetLineLayoutItem().GetNode(); |
| if (!node || !node->IsTextNode()) |
| return DocumentMarkerVector(); |
| |
| DocumentMarkerController& document_marker_controller = |
| inline_text_box_.GetLineLayoutItem().GetDocument().Markers(); |
| return document_marker_controller.ComputeMarkersToPaint(ToText(*node)); |
| } |
| |
| void InlineTextBoxPainter::PaintDocumentMarkers( |
| const DocumentMarkerVector& markers_to_paint, |
| const PaintInfo& paint_info, |
| const LayoutPoint& box_origin, |
| const ComputedStyle& style, |
| const Font& font, |
| DocumentMarkerPaintPhase marker_paint_phase) { |
| if (!inline_text_box_.GetLineLayoutItem().GetNode()) |
| return; |
| |
| DCHECK(inline_text_box_.Truncation() != kCFullTruncation); |
| DCHECK(inline_text_box_.Len()); |
| |
| DocumentMarkerVector::const_iterator marker_it = markers_to_paint.begin(); |
| // Give any document markers that touch this run a chance to draw before the |
| // text has been drawn. Note end() points at the last char, not one past it |
| // like endOffset and ranges do. |
| for (; marker_it != markers_to_paint.end(); ++marker_it) { |
| DCHECK(*marker_it); |
| const DocumentMarker& marker = **marker_it; |
| |
| if (marker.EndOffset() <= inline_text_box_.Start()) { |
| // marker is completely before this run. This might be a marker that sits |
| // before the first run we draw, or markers that were within runs we |
| // skipped due to truncation. |
| continue; |
| } |
| if (marker.StartOffset() > inline_text_box_.end()) { |
| // marker is completely after this run, bail. A later run will paint it. |
| continue; |
| } |
| |
| // marker intersects this run. Paint it. |
| switch (marker.GetType()) { |
| case DocumentMarker::kSpelling: |
| if (marker_paint_phase == DocumentMarkerPaintPhase::kBackground) |
| continue; |
| inline_text_box_.PaintDocumentMarker(paint_info.context, box_origin, |
| marker, style, font, false); |
| break; |
| case DocumentMarker::kGrammar: |
| if (marker_paint_phase == DocumentMarkerPaintPhase::kBackground) |
| continue; |
| inline_text_box_.PaintDocumentMarker(paint_info.context, box_origin, |
| marker, style, font, true); |
| break; |
| case DocumentMarker::kTextMatch: |
| if (marker_paint_phase == DocumentMarkerPaintPhase::kBackground) { |
| inline_text_box_.PaintTextMatchMarkerBackground( |
| paint_info, box_origin, ToTextMatchMarker(marker), style, font); |
| } else { |
| inline_text_box_.PaintTextMatchMarkerForeground( |
| paint_info, box_origin, ToTextMatchMarker(marker), style, font); |
| } |
| break; |
| case DocumentMarker::kComposition: |
| case DocumentMarker::kActiveSuggestion: |
| case DocumentMarker::kSuggestion: { |
| const StyleableMarker& styleable_marker = ToStyleableMarker(marker); |
| if (marker_paint_phase == DocumentMarkerPaintPhase::kBackground) { |
| const PaintOffsets marker_offsets = |
| MarkerPaintStartAndEnd(styleable_marker); |
| PaintSingleMarkerBackgroundRun( |
| paint_info.context, box_origin, style, font, |
| styleable_marker.BackgroundColor(), marker_offsets.start, |
| marker_offsets.end); |
| } else { |
| PaintStyleableMarkerUnderline(paint_info.context, box_origin, |
| styleable_marker, style, font); |
| } |
| } break; |
| default: |
| // Marker is not painted, or painting code has not been added yet |
| break; |
| } |
| } |
| } |
| |
| void InlineTextBoxPainter::PaintDocumentMarker(GraphicsContext& context, |
| const LayoutPoint& box_origin, |
| const DocumentMarker& marker, |
| const ComputedStyle& style, |
| const Font& font, |
| bool grammar) { |
| if (inline_text_box_.GetLineLayoutItem().GetDocument().Printing()) |
| return; |
| |
| if (inline_text_box_.Truncation() == kCFullTruncation) |
| return; |
| |
| LayoutUnit start; // start of line to draw, relative to tx |
| LayoutUnit width = inline_text_box_.LogicalWidth(); // how much line to draw |
| |
| // Determine whether we need to measure text |
| bool marker_spans_whole_box = true; |
| if (inline_text_box_.Start() <= marker.StartOffset()) |
| marker_spans_whole_box = false; |
| if ((inline_text_box_.end() + 1) != |
| marker.EndOffset()) // end points at the last char, not past it |
| marker_spans_whole_box = false; |
| if (inline_text_box_.Truncation() != kCNoTruncation) |
| marker_spans_whole_box = false; |
| |
| if (!marker_spans_whole_box || grammar) { |
| const PaintOffsets& marker_offsets = MarkerPaintStartAndEnd(marker); |
| |
| // Calculate start & width |
| int delta_y = (inline_text_box_.GetLineLayoutItem() |
| .Style() |
| ->IsFlippedLinesWritingMode() |
| ? inline_text_box_.Root().SelectionBottom() - |
| inline_text_box_.LogicalBottom() |
| : inline_text_box_.LogicalTop() - |
| inline_text_box_.Root().SelectionTop()) |
| .ToInt(); |
| int sel_height = inline_text_box_.Root().SelectionHeight().ToInt(); |
| LayoutPoint start_point(box_origin.X(), box_origin.Y() - delta_y); |
| TextRun run = inline_text_box_.ConstructTextRun(style); |
| |
| // FIXME: Convert the document markers to float rects. |
| IntRect marker_rect = EnclosingIntRect( |
| font.SelectionRectForText(run, FloatPoint(start_point), sel_height, |
| marker_offsets.start, marker_offsets.end)); |
| start = marker_rect.X() - start_point.X(); |
| width = LayoutUnit(marker_rect.Width()); |
| } |
| DocumentMarkerPainter::PaintDocumentMarker( |
| context, box_origin, style, marker.GetType(), |
| LayoutRect(start, LayoutUnit(), width, inline_text_box_.LogicalHeight())); |
| } |
| |
| template <InlineTextBoxPainter::PaintOptions options> |
| LayoutRect InlineTextBoxPainter::GetSelectionRect( |
| GraphicsContext& context, |
| const LayoutRect& box_rect, |
| const ComputedStyle& style, |
| const Font& font, |
| LayoutTextCombine* combined_text) { |
| // See if we have a selection to paint at all. |
| int start_pos, end_pos; |
| inline_text_box_.SelectionStartEnd(start_pos, end_pos); |
| if (start_pos >= end_pos) |
| return LayoutRect(); |
| |
| // If the text is truncated, let the thing being painted in the truncation |
| // draw its own highlight. |
| unsigned start = inline_text_box_.Start(); |
| int length = inline_text_box_.Len(); |
| bool ltr = inline_text_box_.IsLeftToRightDirection(); |
| bool flow_is_ltr = inline_text_box_.GetLineLayoutItem() |
| .ContainingBlock() |
| .Style() |
| ->IsLeftToRightDirection(); |
| if (inline_text_box_.Truncation() != kCNoTruncation) { |
| // In a mixed-direction flow the ellipsis is at the start of the text |
| // so we need to start after it. Otherwise we just need to make sure |
| // the end of the text is where the ellipsis starts. |
| if (ltr != flow_is_ltr) |
| start_pos = std::max<int>(start_pos, inline_text_box_.Truncation()); |
| else |
| length = inline_text_box_.Truncation(); |
| } |
| StringView string(inline_text_box_.GetLineLayoutItem().GetText(), start, |
| static_cast<unsigned>(length)); |
| |
| StringBuilder characters_with_hyphen; |
| bool respect_hyphen = end_pos == length && inline_text_box_.HasHyphen(); |
| TextRun text_run = inline_text_box_.ConstructTextRun( |
| style, string, |
| inline_text_box_.GetLineLayoutItem().TextLength() - |
| inline_text_box_.Start(), |
| respect_hyphen ? &characters_with_hyphen : nullptr); |
| if (respect_hyphen) |
| end_pos = text_run.length(); |
| |
| if (options == InlineTextBoxPainter::PaintOptions::kCombinedText) { |
| DCHECK(combined_text); |
| // We can't use the height of m_inlineTextBox because LayoutTextCombine's |
| // inlineTextBox is horizontal within vertical flow |
| combined_text->TransformToInlineCoordinates(context, box_rect, true); |
| } |
| |
| LayoutUnit selection_bottom = inline_text_box_.Root().SelectionBottom(); |
| LayoutUnit selection_top = inline_text_box_.Root().SelectionTop(); |
| |
| int delta_y = |
| RoundToInt(inline_text_box_.GetLineLayoutItem() |
| .StyleRef() |
| .IsFlippedLinesWritingMode() |
| ? selection_bottom - inline_text_box_.LogicalBottom() |
| : inline_text_box_.LogicalTop() - selection_top); |
| int sel_height = std::max(0, RoundToInt(selection_bottom - selection_top)); |
| |
| FloatPoint local_origin(box_rect.X().ToFloat(), |
| (box_rect.Y() - delta_y).ToFloat()); |
| LayoutRect selection_rect = LayoutRect(font.SelectionRectForText( |
| text_run, local_origin, sel_height, start_pos, end_pos)); |
| // For line breaks, just painting a selection where the line break itself |
| // is rendered is sufficient. Don't select it if there's an ellipsis |
| // there. |
| if (inline_text_box_.HasWrappedSelectionNewline() && |
| inline_text_box_.Truncation() == kCNoTruncation && |
| !inline_text_box_.IsLineBreak()) |
| ExpandToIncludeNewlineForSelection(selection_rect); |
| |
| // Line breaks report themselves as having zero width for layout purposes, |
| // and so will end up positioned at (0, 0), even though we paint their |
| // selection highlight with character width. For RTL then, we have to |
| // explicitly shift the selection rect over to paint in the right location. |
| if (!inline_text_box_.IsLeftToRightDirection() && |
| inline_text_box_.IsLineBreak()) |
| selection_rect.Move(-selection_rect.Width(), LayoutUnit()); |
| if (!flow_is_ltr && !ltr && inline_text_box_.Truncation() != kCNoTruncation) { |
| selection_rect.Move( |
| inline_text_box_.LogicalWidth() - selection_rect.Width(), LayoutUnit()); |
| } |
| |
| return selection_rect; |
| } |
| |
| template <InlineTextBoxPainter::PaintOptions options> |
| void InlineTextBoxPainter::PaintSelection(GraphicsContext& context, |
| const LayoutRect& box_rect, |
| const ComputedStyle& style, |
| const Font& font, |
| Color text_color, |
| LayoutTextCombine* combined_text) { |
| auto layout_item = inline_text_box_.GetLineLayoutItem(); |
| Color c = SelectionPaintingUtils::SelectionBackgroundColor( |
| layout_item.GetDocument(), layout_item.StyleRef(), layout_item.GetNode()); |
| if (!c.Alpha()) |
| return; |
| |
| LayoutRect selection_rect = |
| GetSelectionRect<options>(context, box_rect, style, font, combined_text); |
| |
| // If the text color ends up being the same as the selection background, |
| // invert the selection background. |
| if (text_color == c) |
| c = Color(0xff - c.Red(), 0xff - c.Green(), 0xff - c.Blue()); |
| |
| GraphicsContextStateSaver state_saver(context); |
| |
| context.FillRect(FloatRect(selection_rect), c); |
| } |
| |
| void InlineTextBoxPainter::ExpandToIncludeNewlineForSelection( |
| LayoutRect& rect) { |
| FloatRectOutsets outsets = FloatRectOutsets(); |
| float space_width = inline_text_box_.NewlineSpaceWidth(); |
| if (inline_text_box_.IsLeftToRightDirection()) |
| outsets.SetRight(space_width); |
| else |
| outsets.SetLeft(space_width); |
| rect.Expand(outsets); |
| } |
| |
| void InlineTextBoxPainter::PaintStyleableMarkerUnderline( |
| GraphicsContext& context, |
| const LayoutPoint& box_origin, |
| const StyleableMarker& marker, |
| const ComputedStyle& style, |
| const Font& font) { |
| if (inline_text_box_.Truncation() == kCFullTruncation) |
| return; |
| |
| const PaintOffsets marker_offsets = MarkerPaintStartAndEnd(marker); |
| const TextRun& run = inline_text_box_.ConstructTextRun(style); |
| // Pass 0 for height since we only care about the width |
| const FloatRect& marker_rect = font.SelectionRectForText( |
| run, FloatPoint(), 0, marker_offsets.start, marker_offsets.end); |
| DocumentMarkerPainter::PaintStyleableMarkerUnderline( |
| context, box_origin, marker, style, marker_rect, |
| inline_text_box_.LogicalHeight()); |
| } |
| |
| void InlineTextBoxPainter::PaintTextMatchMarkerForeground( |
| const PaintInfo& paint_info, |
| const LayoutPoint& box_origin, |
| const TextMatchMarker& marker, |
| const ComputedStyle& style, |
| const Font& font) { |
| if (!InlineLayoutObject() |
| .GetFrame() |
| ->GetEditor() |
| .MarkedTextMatchesAreHighlighted()) |
| return; |
| |
| const auto paint_offsets = |
| GetTextMatchMarkerPaintOffsets(marker, inline_text_box_); |
| TextRun run = inline_text_box_.ConstructTextRun(style); |
| |
| const SimpleFontData* font_data = font.PrimaryFont(); |
| DCHECK(font_data); |
| if (!font_data) |
| return; |
| |
| const TextPaintStyle text_style = |
| DocumentMarkerPainter::ComputeTextPaintStyleFrom(style, marker); |
| if (text_style.current_color == Color::kTransparent) |
| return; |
| |
| LayoutRect box_rect(box_origin, LayoutSize(inline_text_box_.LogicalWidth(), |
| inline_text_box_.LogicalHeight())); |
| LayoutPoint text_origin( |
| box_origin.X(), box_origin.Y() + font_data->GetFontMetrics().Ascent()); |
| TextPainter text_painter(paint_info.context, font, run, text_origin, box_rect, |
| inline_text_box_.IsHorizontal()); |
| |
| text_painter.Paint(paint_offsets.first, paint_offsets.second, |
| inline_text_box_.Len(), text_style); |
| } |
| |
| void InlineTextBoxPainter::PaintTextMatchMarkerBackground( |
| const PaintInfo& paint_info, |
| const LayoutPoint& box_origin, |
| const TextMatchMarker& marker, |
| const ComputedStyle& style, |
| const Font& font) { |
| if (!LineLayoutAPIShim::LayoutObjectFrom(inline_text_box_.GetLineLayoutItem()) |
| ->GetFrame() |
| ->GetEditor() |
| .MarkedTextMatchesAreHighlighted()) |
| return; |
| |
| const auto paint_offsets = |
| GetTextMatchMarkerPaintOffsets(marker, inline_text_box_); |
| TextRun run = inline_text_box_.ConstructTextRun(style); |
| |
| Color color = LayoutTheme::GetTheme().PlatformTextSearchHighlightColor( |
| marker.IsActiveMatch()); |
| GraphicsContext& context = paint_info.context; |
| GraphicsContextStateSaver state_saver(context); |
| |
| LayoutRect box_rect(box_origin, LayoutSize(inline_text_box_.LogicalWidth(), |
| inline_text_box_.LogicalHeight())); |
| context.Clip(FloatRect(box_rect)); |
| context.DrawHighlightForText(font, run, FloatPoint(box_origin), |
| box_rect.Height().ToInt(), color, |
| paint_offsets.first, paint_offsets.second); |
| } |
| |
| } // namespace blink |