Bug 1395748 - Fix text selection shadow interaction. r=jrmuizel
authorAlexis Beingessner <a.beingessner@gmail.com>
Tue, 12 Sep 2017 16:50:44 -0400
changeset 430216 065ea3d56a5274db71a9c50f8876bc24f293e0f5
parent 430215 39b0e0aead7b3c19c2492f6344045bf2343bf165
child 430217 1ef5858d537ca65f88278b6d140905fe4fa292fe
push id7761
push userjlund@mozilla.com
push dateFri, 15 Sep 2017 00:19:52 +0000
treeherdermozilla-beta@c38455951db4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjrmuizel
bugs1395748
milestone57.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1395748 - Fix text selection shadow interaction. r=jrmuizel Selections in gecko are used to hack in style changes to subsets of text frames. Mostly this works fine because decorations don't care where they are, and textRunFragments already exist to do style changes midFrame. However we mishandled shadows because we were assuming they applied to the entire run, which isn't the case when shadows are involved. Applying shadows to everything was desirable because the way nsTextFrame is written, it's difficult for us to associate the glyphs and decorations with a "range". However the selections iterator provides a natural grouping, so we use that. The result is that TextDrawTarget effectively becomes an array of what TextDrawTarget used to be (now called SelectedTextRunFragment). Everything else is just fallout of this change. MozReview-Commit-ID: 5GWPruo6daW
layout/generic/TextDrawTarget.h
layout/generic/nsTextFrame.cpp
--- a/layout/generic/TextDrawTarget.h
+++ b/layout/generic/TextDrawTarget.h
@@ -21,16 +21,41 @@ struct TextRunFragment {
 };
 
 // Only webrender handles this, so we use webrender types
 struct SelectionFragment {
   wr::ColorF color;
   wr::LayoutRect rect;
 };
 
+// Selections are used in nsTextFrame to hack in sub-frame style changes.
+// Most notably text-shadows can be changed by selections, and so we need to
+// group all the glyphs and decorations attached to a shadow. We do this by
+// having shadows apply to an entire SelectedTextRunFragment, and creating
+// one for each "piece" of selection.
+//
+// For instance, this text:
+//
+// Hello [there] my name [is Mega]man
+//          ^                ^
+//  normal selection      Ctrl+F highlight selection (yeah it's very overloaded)
+//
+// Would be broken up into 5 SelectedTextRunFragments
+//
+// ["Hello ", "there", " my name ", "is Mega", "man"]
+//
+// For almost all nsTextFrames, there will be only one SelectedTextRunFragment.
+struct SelectedTextRunFragment {
+  Maybe<SelectionFragment> selection;
+  nsTArray<wr::TextShadow> shadows;
+  nsTArray<TextRunFragment> text;
+  nsTArray<wr::Line> beforeDecorations;
+  nsTArray<wr::Line> afterDecorations;
+};
+
 // This class is fake DrawTarget, used to intercept text draw calls, while
 // also collecting up the other aspects of text natively.
 //
 // When using advanced-layers in nsDisplayText's constructor, we construct this
 // and run the full painting algorithm with this as the DrawTarget. This is
 // done to avoid having to massively refactor gecko's text painting code (which
 // has lots of components shared between other rendering algorithms).
 //
@@ -61,25 +86,37 @@ public:
   enum class Phase : uint8_t {
     eSelection, eUnderline, eOverline, eGlyphs, eEmphasisMarks, eLineThrough
   };
 
   explicit TextDrawTarget()
   : mCurrentlyDrawing(Phase::eSelection)
   {
     mCurrentTarget = gfx::Factory::CreateDrawTarget(gfx::BackendType::SKIA, IntSize(1, 1), gfx::SurfaceFormat::B8G8R8A8);
+    SetSelectionIndex(0);
   }
 
   // Prevent this from being copied
   TextDrawTarget(const TextDrawTarget& src) = delete;
   TextDrawTarget& operator=(const TextDrawTarget&) = delete;
 
   // Change the phase of text we're drawing.
   void StartDrawing(Phase aPhase) { mCurrentlyDrawing = aPhase; }
 
+  void SetSelectionIndex(size_t i) {
+    // i should only be accessed if i-1 has already been
+    MOZ_ASSERT(mParts.Length() <= i);
+
+    if (mParts.Length() == i){
+      mParts.AppendElement();
+    }
+
+    mCurrentPart = &mParts[i];
+  }
+
   // This overload just stores the glyphs/font/color.
   void
   FillGlyphs(ScaledFont* aFont,
              const GlyphBuffer& aBuffer,
              const Pattern& aPattern,
              const DrawOptions& aOptions,
              const GlyphRenderingOptions* aRenderingOptions) override
   {
@@ -99,24 +136,24 @@ public:
     if (mCurrentlyDrawing != Phase::eGlyphs &&
         mCurrentlyDrawing != Phase::eEmphasisMarks) {
       MOZ_CRASH("TextDrawTarget received glyphs in wrong phase");
     }
 
     // We need to push a new TextRunFragment whenever the font/color changes
     // (usually this implies some font fallback from mixing languages/emoji)
     TextRunFragment* fragment;
-    if (mText.IsEmpty() ||
-        mText.LastElement().font != aFont ||
-        mText.LastElement().color != colorPat->mColor) {
-      fragment = mText.AppendElement();
+    if (mCurrentPart->text.IsEmpty() ||
+        mCurrentPart->text.LastElement().font != aFont ||
+        mCurrentPart->text.LastElement().color != colorPat->mColor) {
+      fragment = mCurrentPart->text.AppendElement();
       fragment->font = aFont;
       fragment->color = colorPat->mColor;
     } else {
-      fragment = &mText.LastElement();
+      fragment = &mCurrentPart->text.LastElement();
     }
 
     nsTArray<Glyph>& glyphs = fragment->glyphs;
 
     size_t oldLength = glyphs.Length();
     glyphs.SetLength(oldLength + aBuffer.mNumGlyphs);
     PodCopy(glyphs.Elements() + oldLength, aBuffer.mGlyphs, aBuffer.mNumGlyphs);
 
@@ -129,44 +166,47 @@ public:
                            0, 0);
       for (size_t i = oldLength; i < oldLength + aBuffer.mNumGlyphs; ++i) {
         auto position = &glyphs[i].mPosition;
         *position = skew.TransformPoint(*position);
       }
     }
   }
 
-  void AppendShadow(const wr::TextShadow& aShadow) { mShadows.AppendElement(aShadow); }
+  void
+  AppendShadow(const wr::TextShadow& aShadow) {
+    mCurrentPart->shadows.AppendElement(aShadow);
+  }
 
   void
-  AppendSelection(const LayoutDeviceRect& aRect, const Color& aColor)
+  SetSelectionRect(const LayoutDeviceRect& aRect, const Color& aColor)
   {
     SelectionFragment frag;
     frag.rect = wr::ToLayoutRect(aRect);
     frag.color = wr::ToColorF(aColor);
-    mSelections.AppendElement(frag);
+    mCurrentPart->selection = Some(frag);
   }
 
   void
   AppendDecoration(const Point& aStart,
                    const Point& aEnd,
                    const float aThickness,
                    const bool aVertical,
                    const Color& aColor,
                    const uint8_t aStyle)
   {
     wr::Line* decoration;
 
     switch (mCurrentlyDrawing) {
       case Phase::eUnderline:
       case Phase::eOverline:
-        decoration = mBeforeDecorations.AppendElement();
+        decoration = mCurrentPart->beforeDecorations.AppendElement();
         break;
       case Phase::eLineThrough:
-        decoration = mAfterDecorations.AppendElement();
+        decoration = mCurrentPart->afterDecorations.AppendElement();
         break;
       default:
         MOZ_CRASH("TextDrawTarget received Decoration in wrong phase");
     }
 
     // This function is basically designed to slide into the decoration drawing
     // code of nsCSSRendering with minimum disruption, to minimize the
     // chances of implementation drift. As such, it mostly looks like a call
@@ -203,44 +243,77 @@ public:
       case NS_STYLE_TEXT_DECORATION_STYLE_DOUBLE:
       default:
         MOZ_CRASH("TextDrawTarget received unsupported line style");
     }
 
 
   }
 
-  const nsTArray<wr::TextShadow>& GetShadows() { return mShadows; }
-  const nsTArray<TextRunFragment>& GetText() { return mText; }
-  const nsTArray<SelectionFragment>& GetSelections() { return mSelections; }
-  const nsTArray<wr::Line>& GetBeforeDecorations() { return mBeforeDecorations; }
-  const nsTArray<wr::Line>& GetAfterDecorations() { return mAfterDecorations; }
+  const nsTArray<SelectedTextRunFragment>& GetParts() { return mParts; }
 
   bool
   CanSerializeFonts()
   {
-    for (const TextRunFragment& frag : GetText()) {
-      if (!frag.font->CanSerialize()) {
+    for (const SelectedTextRunFragment& part : GetParts()) {
+      for (const TextRunFragment& frag : part.text) {
+        if (!frag.font->CanSerialize()) {
+          return false;
+        }
+      }
+    }
+    return true;
+  }
+
+  // TextLayers don't support very complicated text right now. This checks
+  // if any of the problem cases exist.
+  bool
+  ContentsAreSimple()
+  {
+
+    ScaledFont* font = nullptr;
+
+    for (const SelectedTextRunFragment& part : GetParts()) {
+      // Can't handle shadows, selections, or decorations
+      if (part.shadows.Length() > 0 ||
+          part.beforeDecorations.Length() > 0 ||
+          part.afterDecorations.Length() > 0 ||
+          part.selection.isSome()) {
         return false;
       }
+
+      // Must only have one font (multiple colors is fine)
+      for (const mozilla::layout::TextRunFragment& text : part.text) {
+        if (!font) {
+          font = text.font;
+        }
+        if (font != text.font) {
+          return false;
+        }
+      }
     }
+
+    // Must have an actual font (i.e. actual text)
+    if (!font) {
+      return false;
+    }
+
     return true;
   }
 
 
 private:
   // The part of the text we're currently drawing (glyphs, underlines, etc.)
   Phase mCurrentlyDrawing;
 
-  // Properties of the whole text
-  nsTArray<wr::TextShadow> mShadows;
-  nsTArray<TextRunFragment> mText;
-  nsTArray<SelectionFragment> mSelections;
-  nsTArray<wr::Line> mBeforeDecorations;
-  nsTArray<wr::Line> mAfterDecorations;
+  // Which chunk of mParts is actively being populated
+  SelectedTextRunFragment* mCurrentPart;
+
+  // Chunks of the text, grouped by selection
+  nsTArray<SelectedTextRunFragment> mParts;
 
   // A dummy to handle parts of the DrawTarget impl we don't care for
   RefPtr<DrawTarget> mCurrentTarget;
 
   // The rest of this is dummy implementations of DrawTarget's API
 public:
   DrawTargetType GetType() const override {
     return mCurrentTarget->GetType();
--- a/layout/generic/nsTextFrame.cpp
+++ b/layout/generic/nsTextFrame.cpp
@@ -5156,39 +5156,17 @@ nsDisplayText::GetLayerState(nsDisplayLi
 
   // If we're using the webrender backend, then we're good to go!
   if (aManager->GetBackendType() == layers::LayersBackend::LAYERS_WR) {
     return mozilla::LAYER_ACTIVE;
   }
 
   // If we're using the TextLayer backend, then we need to make sure
   // the input is plain enough for it to handle
-
-  // Can't handle shadows, selections, or decorations
-  if (mTextDrawer->GetShadows().Length() > 0 ||
-      mTextDrawer->GetSelections().Length() > 0 ||
-      mTextDrawer->GetBeforeDecorations().Length() > 0 ||
-      mTextDrawer->GetAfterDecorations().Length() > 0) {
-    return mozilla::LAYER_NONE;
-  }
-
-  // Must only have one font (multiple colors is fine)
-  ScaledFont* font = nullptr;
-
-  for (const mozilla::layout::TextRunFragment& text : mTextDrawer->GetText()) {
-    if (!font) {
-      font = text.font;
-    }
-    if (font != text.font) {
-      return mozilla::LAYER_NONE;
-    }
-  }
-
-  // Must have an actual font (i.e. actual text)
-  if (!font) {
+  if (!mTextDrawer->ContentsAreSimple()) {
     return mozilla::LAYER_NONE;
   }
 
   return mozilla::LAYER_ACTIVE;
 }
 
 void
 nsDisplayText::Paint(nsDisplayListBuilder* aBuilder,
@@ -5232,48 +5210,52 @@ nsDisplayText::CreateWebRenderCommands(m
   wr::LayoutRect wrClipRect = aSc.ToRelativeLayoutRect(clipRect); // wr::ToLayoutRect(clipRect);
   wr::LayoutRect wrBoundsRect = aSc.ToRelativeLayoutRect(boundsRect); //wr::ToLayoutRect(boundsRect);
 
   // Drawing order: selections, shadows,
   //                underline, overline, [grouped in one array]
   //                text, emphasisText,  [grouped in one array]
   //                lineThrough
 
-  for (const mozilla::layout::SelectionFragment& selection:
-       mTextDrawer->GetSelections()) {
-    aBuilder.PushRect(selection.rect, wrClipRect, selection.color);
-  }
-
-  // WR takes the shadows in CSS-order (reverse of rendering order),
-  // because the drawing of a shadow actually occurs when it's popped.
-  for (const wr::TextShadow& shadow : mTextDrawer->GetShadows()) {
-    aBuilder.PushTextShadow(wrBoundsRect, wrClipRect, shadow);
-  }
-
-  for (const wr::Line& decoration: mTextDrawer->GetBeforeDecorations()) {
-    aBuilder.PushLine(wrClipRect, decoration);
-  }
-
-  for (const mozilla::layout::TextRunFragment& text: mTextDrawer->GetText()) {
-    // mOpacity is set after we do our analysis, so we need to apply it here.
-    // mOpacity is only non-trivial when we have "pure" text, so we don't
-    // ever need to apply it to shadows or decorations.
-    auto color = text.color;
-    color.a *= mOpacity;
-
-    aManager->WrBridge()->PushGlyphs(aBuilder, text.glyphs, text.font,
-                                     color, aSc, boundsRect, clipRect);
-  }
-
-  for (const wr::Line& decoration: mTextDrawer->GetAfterDecorations()) {
-    aBuilder.PushLine(wrClipRect, decoration);
-  }
-
-  for (size_t i = 0; i < mTextDrawer->GetShadows().Length(); ++i) {
-    aBuilder.PopTextShadow();
+  for (auto& part : mTextDrawer->GetParts()) {
+    if (part.selection) {
+      auto selection = part.selection.value();
+      aBuilder.PushRect(selection.rect, wrClipRect, selection.color);
+    }
+  }
+
+  for (auto& part : mTextDrawer->GetParts()) {
+    // WR takes the shadows in CSS-order (reverse of rendering order),
+    // because the drawing of a shadow actually occurs when it's popped.
+    for (const wr::TextShadow& shadow : part.shadows) {
+      aBuilder.PushTextShadow(wrBoundsRect, wrClipRect, shadow);
+    }
+
+    for (const wr::Line& decoration : part.beforeDecorations) {
+      aBuilder.PushLine(wrClipRect, decoration);
+    }
+
+    for (const mozilla::layout::TextRunFragment& text : part.text) {
+      // mOpacity is set after we do our analysis, so we need to apply it here.
+      // mOpacity is only non-trivial when we have "pure" text, so we don't
+      // ever need to apply it to shadows or decorations.
+      auto color = text.color;
+      color.a *= mOpacity;
+
+      aManager->WrBridge()->PushGlyphs(aBuilder, text.glyphs, text.font,
+                                       color, aSc, boundsRect, clipRect);
+    }
+
+    for (const wr::Line& decoration : part.afterDecorations) {
+      aBuilder.PushLine(wrClipRect, decoration);
+    }
+
+    for (size_t i = 0; i < part.shadows.Length(); ++i) {
+      aBuilder.PopTextShadow();
+    }
   }
 
   return true;
 }
 
 already_AddRefed<layers::Layer>
 nsDisplayText::BuildLayer(nsDisplayListBuilder* aBuilder,
                           LayerManager* aManager,
@@ -5293,29 +5275,36 @@ nsDisplayText::BuildLayer(nsDisplayListB
     layer = aManager->CreateTextLayer();
   }
 
   // GetLayerState has guaranteed to us that we have exactly one font
   // so this will be overwritten by the time we use it.
   ScaledFont* font = nullptr;
 
   nsTArray<GlyphArray> allGlyphs;
-  allGlyphs.SetCapacity(mTextDrawer->GetText().Length());
-  for (const mozilla::layout::TextRunFragment& text : mTextDrawer->GetText()) {
-    if (!font) {
-      font = text.font;
-    }
-
-    GlyphArray* glyphs = allGlyphs.AppendElement();
-    glyphs->glyphs() = text.glyphs;
-
-    // Apply folded alpha (only applies to glyphs)
-    auto color = text.color;
-    color.a *= mOpacity;
-    glyphs->color() = color;
+  size_t totalLength = 0;
+  for (auto& part : mTextDrawer->GetParts()) {
+    totalLength += part.text.Length();
+  }
+  allGlyphs.SetCapacity(totalLength);
+
+  for (auto& part : mTextDrawer->GetParts()) {
+    for (const mozilla::layout::TextRunFragment& text : part.text) {
+      if (!font) {
+        font = text.font;
+      }
+
+      GlyphArray* glyphs = allGlyphs.AppendElement();
+      glyphs->glyphs() = text.glyphs;
+
+      // Apply folded alpha (only applies to glyphs)
+      auto color = text.color;
+      color.a *= mOpacity;
+      glyphs->color() = color;
+    }
   }
 
   MOZ_ASSERT(font);
 
   layer->SetGlyphs(Move(allGlyphs));
   layer->SetScaledFont(font);
 
   auto A2D = mFrame->PresContext()->AppUnitsPerDevPixel();
@@ -6608,18 +6597,23 @@ nsTextFrame::PaintTextWithSelectionColor
   TextRangeStyle rangeStyle;
   // Draw background colors
   if (anyBackgrounds && !aParams.IsGenerateTextMask()) {
     int32_t appUnitsPerDevPixel =
       aParams.textPaintStyle->PresContext()->AppUnitsPerDevPixel();
     SelectionIterator iterator(prevailingSelections, contentRange,
                                *aParams.provider, mTextRun, startIOffset);
     SelectionType selectionType;
+    size_t selectionIndex = 0;
     while (iterator.GetNextSegment(&iOffset, &range, &hyphenWidth,
                                    &selectionType, &rangeStyle)) {
+      if (aParams.textDrawer) {
+        aParams.textDrawer->SetSelectionIndex(selectionIndex);
+      }
+
       nscolor foreground, background;
       GetSelectionTextColors(selectionType, *aParams.textPaintStyle,
                              rangeStyle, &foreground, &background);
       // Draw background color
       gfxFloat advance = hyphenWidth +
         mTextRun->GetAdvanceWidth(range, aParams.provider);
       if (NS_GET_A(background) > 0) {
         nsRect bgRect;
@@ -6631,25 +6625,26 @@ nsTextFrame::PaintTextWithSelectionColor
           bgRect = nsRect(aParams.framePt.x + offs, aParams.framePt.y,
                           advance, GetSize().height);
         }
 
         LayoutDeviceRect selectionRect =
           LayoutDeviceRect::FromAppUnits(bgRect, appUnitsPerDevPixel);
 
         if (aParams.textDrawer) {
-          aParams.textDrawer->AppendSelection(selectionRect,
-                                              ToDeviceColor(background));
+          aParams.textDrawer->SetSelectionRect(selectionRect,
+                                               ToDeviceColor(background));
         } else {
           PaintSelectionBackground(
             *aParams.context->GetDrawTarget(), background, aParams.dirtyRect,
             selectionRect, aParams.callbacks);
         }
       }
       iterator.UpdateWithAdvance(advance);
+      ++selectionIndex;
     }
   }
 
   if (aParams.IsPaintBGColor()) {
     return true;
   }
 
   gfxFloat advance;
@@ -6667,18 +6662,24 @@ nsTextFrame::PaintTextWithSelectionColor
   shadowParams.provider = aParams.provider;
   shadowParams.clipEdges = &aClipEdges;
 
   // Draw text
   const nsStyleText* textStyle = StyleText();
   SelectionIterator iterator(prevailingSelections, contentRange,
                              *aParams.provider, mTextRun, startIOffset);
   SelectionType selectionType;
+
+  size_t selectionIndex = 0;
   while (iterator.GetNextSegment(&iOffset, &range, &hyphenWidth,
                                  &selectionType, &rangeStyle)) {
+    if (aParams.textDrawer) {
+      aParams.textDrawer->SetSelectionIndex(selectionIndex);
+    }
+
     nscolor foreground, background;
     if (aParams.IsGenerateTextMask()) {
       foreground = NS_RGBA(0, 0, 0, 255);
     } else {
       GetSelectionTextColors(selectionType, *aParams.textPaintStyle,
                              rangeStyle, &foreground, &background);
     }
 
@@ -6707,16 +6708,17 @@ nsTextFrame::PaintTextWithSelectionColor
     // Draw text segment
     params.textColor = foreground;
     params.textStrokeColor = aParams.textPaintStyle->GetWebkitTextStrokeColor();
     params.textStrokeWidth = aParams.textPaintStyle->GetWebkitTextStrokeWidth();
     params.drawSoftHyphen = hyphenWidth > 0;
     DrawText(range, textBaselinePt, params);
     advance += hyphenWidth;
     iterator.UpdateWithAdvance(advance);
+    ++selectionIndex;
   }
   return true;
 }
 
 void
 nsTextFrame::PaintTextSelectionDecorations(
     const PaintTextSelectionParams& aParams,
     const UniquePtr<SelectionDetails>& aDetails,
@@ -6782,18 +6784,22 @@ nsTextFrame::PaintTextSelectionDecoratio
   if (verticalRun) {
     pt.x = (aParams.textBaselinePt.x - mAscent) / app;
   } else {
     pt.y = (aParams.textBaselinePt.y - mAscent) / app;
   }
   gfxFloat decorationOffsetDir = mTextRun->IsSidewaysLeft() ? -1.0 : 1.0;
   SelectionType nextSelectionType;
   TextRangeStyle selectedStyle;
+  size_t selectionIndex = 0;
   while (iterator.GetNextSegment(&iOffset, &range, &hyphenWidth,
                                  &nextSelectionType, &selectedStyle)) {
+    if (aParams.textDrawer) {
+      aParams.textDrawer->SetSelectionIndex(selectionIndex);
+    }
     gfxFloat advance = hyphenWidth +
       mTextRun->GetAdvanceWidth(range, aParams.provider);
     if (nextSelectionType == aSelectionType) {
       if (verticalRun) {
         pt.y = (aParams.framePt.y + iOffset -
                (mTextRun->IsInlineReversed() ? advance : 0)) / app;
       } else {
         pt.x = (aParams.framePt.x + iOffset -
@@ -6803,16 +6809,17 @@ nsTextFrame::PaintTextSelectionDecoratio
       gfxFloat xInFrame = pt.x - (aParams.framePt.x / app);
       DrawSelectionDecorations(
         aParams.context, aParams.dirtyRect, aSelectionType,
         *aParams.textPaintStyle, selectedStyle, pt, xInFrame,
         width, mAscent / app, decorationMetrics, aParams.callbacks,
         verticalRun, decorationOffsetDir, kDecoration);
     }
     iterator.UpdateWithAdvance(advance);
+    ++selectionIndex;
   }
 }
 
 bool
 nsTextFrame::PaintTextWithSelection(
     const PaintTextSelectionParams& aParams,
     const nsCharClipDisplayItem::ClipEdges& aClipEdges)
 {