1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 
7 #include "TextOverflow.h"
8 #include <algorithm>
9 
10 // Please maintain alphabetical order below
11 #include "gfxContext.h"
12 #include "nsBlockFrame.h"
13 #include "nsCaret.h"
14 #include "nsContentUtils.h"
15 #include "nsCSSAnonBoxes.h"
16 #include "nsFontMetrics.h"
17 #include "nsGfxScrollFrame.h"
18 #include "nsIScrollableFrame.h"
19 #include "nsLayoutUtils.h"
20 #include "nsPresContext.h"
21 #include "nsRect.h"
22 #include "nsTextFrame.h"
23 #include "nsIFrameInlines.h"
24 #include "mozilla/ArrayUtils.h"
25 #include "mozilla/Likely.h"
26 #include "mozilla/PresShell.h"
27 #include "mozilla/dom/Selection.h"
28 #include "TextDrawTarget.h"
29 
30 using mozilla::layout::TextDrawTarget;
31 
32 namespace mozilla {
33 namespace css {
34 
35 class LazyReferenceRenderingDrawTargetGetterFromFrame final
36     : public gfxFontGroup::LazyReferenceDrawTargetGetter {
37  public:
38   typedef mozilla::gfx::DrawTarget DrawTarget;
39 
LazyReferenceRenderingDrawTargetGetterFromFrame(nsIFrame * aFrame)40   explicit LazyReferenceRenderingDrawTargetGetterFromFrame(nsIFrame* aFrame)
41       : mFrame(aFrame) {}
GetRefDrawTarget()42   virtual already_AddRefed<DrawTarget> GetRefDrawTarget() override {
43     RefPtr<gfxContext> ctx =
44         mFrame->PresShell()->CreateReferenceRenderingContext();
45     RefPtr<DrawTarget> dt = ctx->GetDrawTarget();
46     return dt.forget();
47   }
48 
49  private:
50   nsIFrame* mFrame;
51 };
52 
GetEllipsisTextRun(nsIFrame * aFrame)53 static gfxTextRun* GetEllipsisTextRun(nsIFrame* aFrame) {
54   RefPtr<nsFontMetrics> fm =
55       nsLayoutUtils::GetInflatedFontMetricsForFrame(aFrame);
56   LazyReferenceRenderingDrawTargetGetterFromFrame lazyRefDrawTargetGetter(
57       aFrame);
58   return fm->GetThebesFontGroup()->GetEllipsisTextRun(
59       aFrame->PresContext()->AppUnitsPerDevPixel(),
60       nsLayoutUtils::GetTextRunOrientFlagsForStyle(aFrame->Style()),
61       lazyRefDrawTargetGetter);
62 }
63 
GetSelfOrNearestBlock(nsIFrame * aFrame)64 static nsIFrame* GetSelfOrNearestBlock(nsIFrame* aFrame) {
65   MOZ_ASSERT(aFrame);
66   return aFrame->IsBlockFrameOrSubclass()
67              ? aFrame
68              : nsLayoutUtils::FindNearestBlockAncestor(aFrame);
69 }
70 
71 // Return true if the frame is an atomic inline-level element.
72 // It's not supposed to be called for block frames since we never
73 // process block descendants for text-overflow.
IsAtomicElement(nsIFrame * aFrame,LayoutFrameType aFrameType)74 static bool IsAtomicElement(nsIFrame* aFrame, LayoutFrameType aFrameType) {
75   MOZ_ASSERT(!aFrame->IsBlockFrameOrSubclass() || !aFrame->IsBlockOutside(),
76              "unexpected block frame");
77   MOZ_ASSERT(aFrameType != LayoutFrameType::Placeholder,
78              "unexpected placeholder frame");
79   return !aFrame->IsFrameOfType(nsIFrame::eLineParticipant);
80 }
81 
IsFullyClipped(nsTextFrame * aFrame,nscoord aLeft,nscoord aRight,nscoord * aSnappedLeft,nscoord * aSnappedRight)82 static bool IsFullyClipped(nsTextFrame* aFrame, nscoord aLeft, nscoord aRight,
83                            nscoord* aSnappedLeft, nscoord* aSnappedRight) {
84   *aSnappedLeft = aLeft;
85   *aSnappedRight = aRight;
86   if (aLeft <= 0 && aRight <= 0) {
87     return false;
88   }
89   return !aFrame->MeasureCharClippedText(aLeft, aRight, aSnappedLeft,
90                                          aSnappedRight);
91 }
92 
IsInlineAxisOverflowVisible(nsIFrame * aFrame)93 static bool IsInlineAxisOverflowVisible(nsIFrame* aFrame) {
94   MOZ_ASSERT(aFrame && aFrame->IsBlockFrameOrSubclass(),
95              "expected a block frame");
96 
97   nsIFrame* f = aFrame;
98   while (f && f->Style()->IsAnonBox() && !f->IsScrollFrame()) {
99     f = f->GetParent();
100   }
101   if (!f) {
102     return true;
103   }
104   auto overflow = aFrame->GetWritingMode().IsVertical()
105                       ? f->StyleDisplay()->mOverflowY
106                       : f->StyleDisplay()->mOverflowX;
107   return overflow == StyleOverflow::Visible;
108 }
109 
ClipMarker(const nsRect & aContentArea,const nsRect & aMarkerRect,DisplayListClipState::AutoSaveRestore & aClipState)110 static void ClipMarker(const nsRect& aContentArea, const nsRect& aMarkerRect,
111                        DisplayListClipState::AutoSaveRestore& aClipState) {
112   nscoord rightOverflow = aMarkerRect.XMost() - aContentArea.XMost();
113   nsRect markerRect = aMarkerRect;
114   if (rightOverflow > 0) {
115     // Marker overflows on the right side (content width < marker width).
116     markerRect.width -= rightOverflow;
117     aClipState.ClipContentDescendants(markerRect);
118   } else {
119     nscoord leftOverflow = aContentArea.x - aMarkerRect.x;
120     if (leftOverflow > 0) {
121       // Marker overflows on the left side
122       markerRect.width -= leftOverflow;
123       markerRect.x += leftOverflow;
124       aClipState.ClipContentDescendants(markerRect);
125     }
126   }
127 }
128 
InflateIStart(WritingMode aWM,LogicalRect * aRect,nscoord aDelta)129 static void InflateIStart(WritingMode aWM, LogicalRect* aRect, nscoord aDelta) {
130   nscoord iend = aRect->IEnd(aWM);
131   aRect->IStart(aWM) -= aDelta;
132   aRect->ISize(aWM) = std::max(iend - aRect->IStart(aWM), 0);
133 }
134 
InflateIEnd(WritingMode aWM,LogicalRect * aRect,nscoord aDelta)135 static void InflateIEnd(WritingMode aWM, LogicalRect* aRect, nscoord aDelta) {
136   aRect->ISize(aWM) = std::max(aRect->ISize(aWM) + aDelta, 0);
137 }
138 
IsFrameDescendantOfAny(nsIFrame * aChild,const TextOverflow::FrameHashtable & aSetOfFrames,nsIFrame * aCommonAncestor)139 static bool IsFrameDescendantOfAny(
140     nsIFrame* aChild, const TextOverflow::FrameHashtable& aSetOfFrames,
141     nsIFrame* aCommonAncestor) {
142   for (nsIFrame* f = aChild; f && f != aCommonAncestor;
143        f = nsLayoutUtils::GetCrossDocParentFrameInProcess(f)) {
144     if (aSetOfFrames.Contains(f)) {
145       return true;
146     }
147   }
148   return false;
149 }
150 
151 class nsDisplayTextOverflowMarker final : public nsPaintedDisplayItem {
152  public:
nsDisplayTextOverflowMarker(nsDisplayListBuilder * aBuilder,nsIFrame * aFrame,const nsRect & aRect,nscoord aAscent,const StyleTextOverflowSide & aStyle)153   nsDisplayTextOverflowMarker(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame,
154                               const nsRect& aRect, nscoord aAscent,
155                               const StyleTextOverflowSide& aStyle)
156       : nsPaintedDisplayItem(aBuilder, aFrame),
157         mRect(aRect),
158         mStyle(aStyle),
159         mAscent(aAscent) {
160     MOZ_COUNT_CTOR(nsDisplayTextOverflowMarker);
161   }
162 
MOZ_COUNTED_DTOR_OVERRIDE(nsDisplayTextOverflowMarker)163   MOZ_COUNTED_DTOR_OVERRIDE(nsDisplayTextOverflowMarker)
164 
165   virtual nsRect GetBounds(nsDisplayListBuilder* aBuilder,
166                            bool* aSnap) const override {
167     *aSnap = false;
168     nsRect shadowRect = nsLayoutUtils::GetTextShadowRectsUnion(mRect, mFrame);
169     return mRect.Union(shadowRect);
170   }
171 
GetComponentAlphaBounds(nsDisplayListBuilder * aBuilder) const172   virtual nsRect GetComponentAlphaBounds(
173       nsDisplayListBuilder* aBuilder) const override {
174     if (gfxPlatform::GetPlatform()->RespectsFontStyleSmoothing()) {
175       // On OS X, web authors can turn off subpixel text rendering using the
176       // CSS property -moz-osx-font-smoothing. If they do that, we don't need
177       // to use component alpha layers for the affected text.
178       if (mFrame->StyleFont()->mFont.smoothing == NS_FONT_SMOOTHING_GRAYSCALE) {
179         return nsRect();
180       }
181     }
182     bool snap;
183     return GetBounds(aBuilder, &snap);
184   }
185 
186   virtual void Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) override;
187 
188   void PaintTextToContext(gfxContext* aCtx, nsPoint aOffsetFromRect);
189 
190   virtual bool CreateWebRenderCommands(
191       mozilla::wr::DisplayListBuilder& aBuilder,
192       mozilla::wr::IpcResourceUpdateQueue& aResources,
193       const StackingContextHelper& aSc,
194       layers::RenderRootStateManager* aManager,
195       nsDisplayListBuilder* aDisplayListBuilder) override;
196 
197   NS_DISPLAY_DECL_NAME("TextOverflow", TYPE_TEXT_OVERFLOW)
198  private:
199   nsRect mRect;  // in reference frame coordinates
200   const StyleTextOverflowSide mStyle;
201   nscoord mAscent;  // baseline for the marker text in mRect
202 };
203 
PaintTextShadowCallback(gfxContext * aCtx,nsPoint aShadowOffset,const nscolor & aShadowColor,void * aData)204 static void PaintTextShadowCallback(gfxContext* aCtx, nsPoint aShadowOffset,
205                                     const nscolor& aShadowColor, void* aData) {
206   reinterpret_cast<nsDisplayTextOverflowMarker*>(aData)->PaintTextToContext(
207       aCtx, aShadowOffset);
208 }
209 
Paint(nsDisplayListBuilder * aBuilder,gfxContext * aCtx)210 void nsDisplayTextOverflowMarker::Paint(nsDisplayListBuilder* aBuilder,
211                                         gfxContext* aCtx) {
212   nscolor foregroundColor =
213       nsLayoutUtils::GetColor(mFrame, &nsStyleText::mWebkitTextFillColor);
214 
215   // Paint the text-shadows for the overflow marker
216   nsLayoutUtils::PaintTextShadow(mFrame, aCtx, mRect,
217                                  GetPaintRect(aBuilder, aCtx), foregroundColor,
218                                  PaintTextShadowCallback, (void*)this);
219   aCtx->SetColor(gfx::sRGBColor::FromABGR(foregroundColor));
220   PaintTextToContext(aCtx, nsPoint(0, 0));
221 }
222 
PaintTextToContext(gfxContext * aCtx,nsPoint aOffsetFromRect)223 void nsDisplayTextOverflowMarker::PaintTextToContext(gfxContext* aCtx,
224                                                      nsPoint aOffsetFromRect) {
225   WritingMode wm = mFrame->GetWritingMode();
226   nsPoint pt(mRect.x, mRect.y);
227   if (wm.IsVertical()) {
228     if (wm.IsVerticalLR()) {
229       pt.x = NSToCoordFloor(
230           nsLayoutUtils::GetSnappedBaselineX(mFrame, aCtx, pt.x, mAscent));
231     } else {
232       pt.x = NSToCoordFloor(nsLayoutUtils::GetSnappedBaselineX(
233           mFrame, aCtx, pt.x + mRect.width, -mAscent));
234     }
235   } else {
236     pt.y = NSToCoordFloor(
237         nsLayoutUtils::GetSnappedBaselineY(mFrame, aCtx, pt.y, mAscent));
238   }
239   pt += aOffsetFromRect;
240 
241   if (mStyle.IsEllipsis()) {
242     gfxTextRun* textRun = GetEllipsisTextRun(mFrame);
243     if (textRun) {
244       NS_ASSERTION(!textRun->IsRightToLeft(),
245                    "Ellipsis textruns should always be LTR!");
246       gfx::Point gfxPt(pt.x, pt.y);
247       textRun->Draw(gfxTextRun::Range(textRun), gfxPt,
248                     gfxTextRun::DrawParams(aCtx));
249     }
250   } else {
251     RefPtr<nsFontMetrics> fm =
252         nsLayoutUtils::GetInflatedFontMetricsForFrame(mFrame);
253     NS_ConvertUTF8toUTF16 str16{mStyle.AsString().AsString()};
254     nsLayoutUtils::DrawString(mFrame, *fm, aCtx, str16.get(), str16.Length(),
255                               pt);
256   }
257 }
258 
CreateWebRenderCommands(mozilla::wr::DisplayListBuilder & aBuilder,mozilla::wr::IpcResourceUpdateQueue & aResources,const StackingContextHelper & aSc,layers::RenderRootStateManager * aManager,nsDisplayListBuilder * aDisplayListBuilder)259 bool nsDisplayTextOverflowMarker::CreateWebRenderCommands(
260     mozilla::wr::DisplayListBuilder& aBuilder,
261     mozilla::wr::IpcResourceUpdateQueue& aResources,
262     const StackingContextHelper& aSc, layers::RenderRootStateManager* aManager,
263     nsDisplayListBuilder* aDisplayListBuilder) {
264   bool snap;
265   nsRect bounds = GetBounds(aDisplayListBuilder, &snap);
266   if (bounds.IsEmpty()) {
267     return true;
268   }
269 
270   // Run the rendering algorithm to capture the glyphs and shadows
271   RefPtr<TextDrawTarget> textDrawer =
272       new TextDrawTarget(aBuilder, aResources, aSc, aManager, this, bounds);
273   RefPtr<gfxContext> captureCtx = gfxContext::CreateOrNull(textDrawer);
274   Paint(aDisplayListBuilder, captureCtx);
275   textDrawer->TerminateShadows();
276 
277   return textDrawer->Finish();
278 }
279 
TextOverflow(nsDisplayListBuilder * aBuilder,nsIFrame * aBlockFrame)280 TextOverflow::TextOverflow(nsDisplayListBuilder* aBuilder,
281                            nsIFrame* aBlockFrame)
282     : mContentArea(aBlockFrame->GetWritingMode(),
283                    aBlockFrame->GetContentRectRelativeToSelf(),
284                    aBlockFrame->GetSize()),
285       mBuilder(aBuilder),
286       mBlock(aBlockFrame),
287       mScrollableFrame(nsLayoutUtils::GetScrollableFrameFor(aBlockFrame)),
288       mMarkerList(aBuilder),
289       mBlockSize(aBlockFrame->GetSize()),
290       mBlockWM(aBlockFrame->GetWritingMode()),
291       mAdjustForPixelSnapping(false) {
292   if (!mScrollableFrame) {
293     auto pseudoType = aBlockFrame->Style()->GetPseudoType();
294     if (pseudoType == PseudoStyleType::mozXULAnonymousBlock) {
295       mScrollableFrame =
296           nsLayoutUtils::GetScrollableFrameFor(aBlockFrame->GetParent());
297       // nsXULScrollFrame::ClampAndSetBounds rounds to nearest pixels
298       // for RTL blocks (also for overflow:hidden), so we need to move
299       // the edges 1px outward in ExamineLineFrames to avoid triggering
300       // a text-overflow marker in this case.
301       mAdjustForPixelSnapping = mBlockWM.IsBidiRTL();
302     }
303   }
304   mCanHaveInlineAxisScrollbar = false;
305   if (mScrollableFrame) {
306     auto scrollbarStyle = mBlockWM.IsVertical()
307                               ? mScrollableFrame->GetScrollStyles().mVertical
308                               : mScrollableFrame->GetScrollStyles().mHorizontal;
309     mCanHaveInlineAxisScrollbar = scrollbarStyle != StyleOverflow::Hidden;
310     if (!mAdjustForPixelSnapping) {
311       // Scrolling to the end position can leave some text still overflowing due
312       // to pixel snapping behaviour in our scrolling code.
313       mAdjustForPixelSnapping = mCanHaveInlineAxisScrollbar;
314     }
315     // Use a null containerSize to convert a vector from logical to physical.
316     const nsSize nullContainerSize;
317     mContentArea.MoveBy(
318         mBlockWM, LogicalPoint(mBlockWM, mScrollableFrame->GetScrollPosition(),
319                                nullContainerSize));
320   }
321   StyleDirection direction = aBlockFrame->StyleVisibility()->mDirection;
322   const nsStyleTextReset* style = aBlockFrame->StyleTextReset();
323 
324   const auto& textOverflow = style->mTextOverflow;
325   bool shouldToggleDirection =
326       textOverflow.sides_are_logical && (direction == StyleDirection::Rtl);
327   const auto& leftSide =
328       shouldToggleDirection ? textOverflow.second : textOverflow.first;
329   const auto& rightSide =
330       shouldToggleDirection ? textOverflow.first : textOverflow.second;
331 
332   if (mBlockWM.IsBidiLTR()) {
333     mIStart.Init(leftSide);
334     mIEnd.Init(rightSide);
335   } else {
336     mIStart.Init(rightSide);
337     mIEnd.Init(leftSide);
338   }
339   // The left/right marker string is setup in ExamineLineFrames when a line
340   // has overflow on that side.
341 }
342 
343 /* static */
WillProcessLines(nsDisplayListBuilder * aBuilder,nsIFrame * aBlockFrame)344 Maybe<TextOverflow> TextOverflow::WillProcessLines(
345     nsDisplayListBuilder* aBuilder, nsIFrame* aBlockFrame) {
346   // Ignore text-overflow and -webkit-line-clamp for event and frame visibility
347   // processing.
348   if (aBuilder->IsForEventDelivery() || aBuilder->IsForFrameVisibility() ||
349       !CanHaveOverflowMarkers(aBlockFrame)) {
350     return Nothing();
351   }
352   nsIScrollableFrame* scrollableFrame =
353       nsLayoutUtils::GetScrollableFrameFor(aBlockFrame);
354   if (scrollableFrame && scrollableFrame->IsTransformingByAPZ()) {
355     // If the APZ is actively scrolling this, don't bother with markers.
356     return Nothing();
357   }
358   return Some(TextOverflow(aBuilder, aBlockFrame));
359 }
360 
ExamineFrameSubtree(nsIFrame * aFrame,const LogicalRect & aContentArea,const LogicalRect & aInsideMarkersArea,FrameHashtable * aFramesToHide,AlignmentEdges * aAlignmentEdges,bool * aFoundVisibleTextOrAtomic,InnerClipEdges * aClippedMarkerEdges)361 void TextOverflow::ExamineFrameSubtree(nsIFrame* aFrame,
362                                        const LogicalRect& aContentArea,
363                                        const LogicalRect& aInsideMarkersArea,
364                                        FrameHashtable* aFramesToHide,
365                                        AlignmentEdges* aAlignmentEdges,
366                                        bool* aFoundVisibleTextOrAtomic,
367                                        InnerClipEdges* aClippedMarkerEdges) {
368   const LayoutFrameType frameType = aFrame->Type();
369   if (frameType == LayoutFrameType::Br ||
370       frameType == LayoutFrameType::Placeholder) {
371     return;
372   }
373   const bool isAtomic = IsAtomicElement(aFrame, frameType);
374   if (aFrame->StyleVisibility()->IsVisible()) {
375     LogicalRect childRect =
376         GetLogicalScrollableOverflowRectRelativeToBlock(aFrame);
377     bool overflowIStart =
378         childRect.IStart(mBlockWM) < aContentArea.IStart(mBlockWM);
379     bool overflowIEnd = childRect.IEnd(mBlockWM) > aContentArea.IEnd(mBlockWM);
380     if (overflowIStart) {
381       mIStart.mHasOverflow = true;
382     }
383     if (overflowIEnd) {
384       mIEnd.mHasOverflow = true;
385     }
386     if (isAtomic && ((mIStart.mActive && overflowIStart) ||
387                      (mIEnd.mActive && overflowIEnd))) {
388       aFramesToHide->Insert(aFrame);
389     } else if (isAtomic || frameType == LayoutFrameType::Text) {
390       AnalyzeMarkerEdges(aFrame, frameType, aInsideMarkersArea, aFramesToHide,
391                          aAlignmentEdges, aFoundVisibleTextOrAtomic,
392                          aClippedMarkerEdges);
393     }
394   }
395   if (isAtomic) {
396     return;
397   }
398 
399   for (nsIFrame* child : aFrame->PrincipalChildList()) {
400     ExamineFrameSubtree(child, aContentArea, aInsideMarkersArea, aFramesToHide,
401                         aAlignmentEdges, aFoundVisibleTextOrAtomic,
402                         aClippedMarkerEdges);
403   }
404 }
405 
AnalyzeMarkerEdges(nsIFrame * aFrame,LayoutFrameType aFrameType,const LogicalRect & aInsideMarkersArea,FrameHashtable * aFramesToHide,AlignmentEdges * aAlignmentEdges,bool * aFoundVisibleTextOrAtomic,InnerClipEdges * aClippedMarkerEdges)406 void TextOverflow::AnalyzeMarkerEdges(nsIFrame* aFrame,
407                                       LayoutFrameType aFrameType,
408                                       const LogicalRect& aInsideMarkersArea,
409                                       FrameHashtable* aFramesToHide,
410                                       AlignmentEdges* aAlignmentEdges,
411                                       bool* aFoundVisibleTextOrAtomic,
412                                       InnerClipEdges* aClippedMarkerEdges) {
413   MOZ_ASSERT(aFrameType == LayoutFrameType::Text ||
414              IsAtomicElement(aFrame, aFrameType));
415   LogicalRect borderRect(mBlockWM,
416                          nsRect(aFrame->GetOffsetTo(mBlock), aFrame->GetSize()),
417                          mBlockSize);
418   nscoord istartOverlap = std::max(
419       aInsideMarkersArea.IStart(mBlockWM) - borderRect.IStart(mBlockWM), 0);
420   nscoord iendOverlap = std::max(
421       borderRect.IEnd(mBlockWM) - aInsideMarkersArea.IEnd(mBlockWM), 0);
422   bool insideIStartEdge =
423       aInsideMarkersArea.IStart(mBlockWM) <= borderRect.IEnd(mBlockWM);
424   bool insideIEndEdge =
425       borderRect.IStart(mBlockWM) <= aInsideMarkersArea.IEnd(mBlockWM);
426 
427   if (istartOverlap > 0) {
428     aClippedMarkerEdges->AccumulateIStart(mBlockWM, borderRect);
429     if (!mIStart.mActive) {
430       istartOverlap = 0;
431     }
432   }
433   if (iendOverlap > 0) {
434     aClippedMarkerEdges->AccumulateIEnd(mBlockWM, borderRect);
435     if (!mIEnd.mActive) {
436       iendOverlap = 0;
437     }
438   }
439 
440   if ((istartOverlap > 0 && insideIStartEdge) ||
441       (iendOverlap > 0 && insideIEndEdge)) {
442     if (aFrameType == LayoutFrameType::Text) {
443       auto textFrame = static_cast<nsTextFrame*>(aFrame);
444       if ((aInsideMarkersArea.IStart(mBlockWM) <
445            aInsideMarkersArea.IEnd(mBlockWM)) &&
446           textFrame->HasNonSuppressedText()) {
447         // a clipped text frame and there is some room between the markers
448         nscoord snappedIStart, snappedIEnd;
449         bool isFullyClipped =
450             mBlockWM.IsBidiLTR()
451                 ? IsFullyClipped(textFrame, istartOverlap, iendOverlap,
452                                  &snappedIStart, &snappedIEnd)
453                 : IsFullyClipped(textFrame, iendOverlap, istartOverlap,
454                                  &snappedIEnd, &snappedIStart);
455         if (!isFullyClipped) {
456           LogicalRect snappedRect = borderRect;
457           if (istartOverlap > 0) {
458             snappedRect.IStart(mBlockWM) += snappedIStart;
459             snappedRect.ISize(mBlockWM) -= snappedIStart;
460           }
461           if (iendOverlap > 0) {
462             snappedRect.ISize(mBlockWM) -= snappedIEnd;
463           }
464           aAlignmentEdges->AccumulateInner(mBlockWM, snappedRect);
465           *aFoundVisibleTextOrAtomic = true;
466         }
467       }
468     } else {
469       aFramesToHide->Insert(aFrame);
470     }
471   } else if (!insideIStartEdge || !insideIEndEdge) {
472     // frame is outside
473     if (!insideIStartEdge) {
474       aAlignmentEdges->AccumulateOuter(mBlockWM, borderRect);
475     }
476     if (IsAtomicElement(aFrame, aFrameType)) {
477       aFramesToHide->Insert(aFrame);
478     }
479   } else {
480     // frame is inside
481     aAlignmentEdges->AccumulateInner(mBlockWM, borderRect);
482     if (aFrameType == LayoutFrameType::Text) {
483       auto textFrame = static_cast<nsTextFrame*>(aFrame);
484       if (textFrame->HasNonSuppressedText()) {
485         *aFoundVisibleTextOrAtomic = true;
486       }
487     } else {
488       *aFoundVisibleTextOrAtomic = true;
489     }
490   }
491 }
492 
ExamineLineFrames(nsLineBox * aLine,FrameHashtable * aFramesToHide,AlignmentEdges * aAlignmentEdges)493 LogicalRect TextOverflow::ExamineLineFrames(nsLineBox* aLine,
494                                             FrameHashtable* aFramesToHide,
495                                             AlignmentEdges* aAlignmentEdges) {
496   // No ellipsing for 'clip' style.
497   bool suppressIStart = mIStart.IsSuppressed();
498   bool suppressIEnd = mIEnd.IsSuppressed();
499   if (mCanHaveInlineAxisScrollbar) {
500     LogicalPoint pos(mBlockWM, mScrollableFrame->GetScrollPosition(),
501                      mBlockSize);
502     LogicalRect scrollRange(mBlockWM, mScrollableFrame->GetScrollRange(),
503                             mBlockSize);
504     // No ellipsing when nothing to scroll to on that side (this includes
505     // overflow:auto that doesn't trigger a horizontal scrollbar).
506     if (pos.I(mBlockWM) <= scrollRange.IStart(mBlockWM)) {
507       suppressIStart = true;
508     }
509     if (pos.I(mBlockWM) >= scrollRange.IEnd(mBlockWM)) {
510       // Except that we always want to display a -webkit-line-clamp ellipsis.
511       if (!mIEnd.mHasBlockEllipsis) {
512         suppressIEnd = true;
513       }
514     }
515   }
516 
517   LogicalRect contentArea = mContentArea;
518   bool snapStart = true, snapEnd = true;
519   nscoord startEdge, endEdge;
520   if (aLine->GetFloatEdges(&startEdge, &endEdge)) {
521     // Narrow the |contentArea| to account for any floats on this line, and
522     // don't bother with the snapping quirk on whichever side(s) we narrow.
523     nscoord delta = endEdge - contentArea.IEnd(mBlockWM);
524     if (delta < 0) {
525       nscoord newSize = contentArea.ISize(mBlockWM) + delta;
526       contentArea.ISize(mBlockWM) = std::max(nscoord(0), newSize);
527       snapEnd = false;
528     }
529     delta = startEdge - contentArea.IStart(mBlockWM);
530     if (delta > 0) {
531       contentArea.IStart(mBlockWM) = startEdge;
532       nscoord newSize = contentArea.ISize(mBlockWM) - delta;
533       contentArea.ISize(mBlockWM) = std::max(nscoord(0), newSize);
534       snapStart = false;
535     }
536   }
537   // Save the non-snapped area since that's what we want to use when placing
538   // the markers (our return value).  The snapped area is only for analysis.
539   LogicalRect nonSnappedContentArea = contentArea;
540   if (mAdjustForPixelSnapping) {
541     const nscoord scrollAdjust = mBlock->PresContext()->AppUnitsPerDevPixel();
542     if (snapStart) {
543       InflateIStart(mBlockWM, &contentArea, scrollAdjust);
544     }
545     if (snapEnd) {
546       InflateIEnd(mBlockWM, &contentArea, scrollAdjust);
547     }
548   }
549 
550   LogicalRect lineRect(mBlockWM, aLine->ScrollableOverflowRect(), mBlockSize);
551   const bool istartWantsMarker =
552       !suppressIStart &&
553       lineRect.IStart(mBlockWM) < contentArea.IStart(mBlockWM);
554   const bool iendWantsTextOverflowMarker =
555       !suppressIEnd && lineRect.IEnd(mBlockWM) > contentArea.IEnd(mBlockWM);
556   const bool iendWantsBlockEllipsisMarker =
557       !suppressIEnd && mIEnd.mHasBlockEllipsis;
558   const bool iendWantsMarker =
559       iendWantsTextOverflowMarker || iendWantsBlockEllipsisMarker;
560   if (!istartWantsMarker && !iendWantsMarker) {
561     // We don't need any markers on this line.
562     return nonSnappedContentArea;
563   }
564 
565   int pass = 0;
566   bool retryEmptyLine = true;
567   bool guessIStart = istartWantsMarker;
568   bool guessIEnd = iendWantsMarker;
569   mIStart.mActive = istartWantsMarker;
570   mIEnd.mActive = iendWantsMarker;
571   mIStart.mEdgeAligned = mCanHaveInlineAxisScrollbar && istartWantsMarker;
572   mIEnd.mEdgeAligned =
573       mCanHaveInlineAxisScrollbar && iendWantsTextOverflowMarker;
574   bool clippedIStartMarker = false;
575   bool clippedIEndMarker = false;
576   do {
577     // Setup marker strings as needed.
578     if (guessIStart) {
579       mIStart.SetupString(mBlock);
580     }
581     if (guessIEnd) {
582       mIEnd.SetupString(mBlock);
583     }
584 
585     // If there is insufficient space for both markers then keep the one on the
586     // end side per the block's 'direction'.
587     nscoord istartMarkerISize = mIStart.mActive ? mIStart.mISize : 0;
588     nscoord iendMarkerISize = mIEnd.mActive ? mIEnd.mISize : 0;
589     if (istartMarkerISize && iendMarkerISize &&
590         istartMarkerISize + iendMarkerISize > contentArea.ISize(mBlockWM)) {
591       istartMarkerISize = 0;
592     }
593 
594     // Calculate the area between the potential markers aligned at the
595     // block's edge.
596     LogicalRect insideMarkersArea = nonSnappedContentArea;
597     if (guessIStart) {
598       InflateIStart(mBlockWM, &insideMarkersArea, -istartMarkerISize);
599     }
600     if (guessIEnd) {
601       InflateIEnd(mBlockWM, &insideMarkersArea, -iendMarkerISize);
602     }
603 
604     // Analyze the frames on aLine for the overflow situation at the content
605     // edges and at the edges of the area between the markers.
606     bool foundVisibleTextOrAtomic = false;
607     int32_t n = aLine->GetChildCount();
608     nsIFrame* child = aLine->mFirstChild;
609     InnerClipEdges clippedMarkerEdges;
610     for (; n-- > 0; child = child->GetNextSibling()) {
611       ExamineFrameSubtree(child, contentArea, insideMarkersArea, aFramesToHide,
612                           aAlignmentEdges, &foundVisibleTextOrAtomic,
613                           &clippedMarkerEdges);
614     }
615     if (!foundVisibleTextOrAtomic && retryEmptyLine) {
616       aAlignmentEdges->mAssignedInner = false;
617       aAlignmentEdges->mIEndOuter = 0;
618       aFramesToHide->Clear();
619       pass = -1;
620       if (mIStart.IsNeeded() && mIStart.mActive && !clippedIStartMarker) {
621         if (clippedMarkerEdges.mAssignedIStart &&
622             clippedMarkerEdges.mIStart >
623                 nonSnappedContentArea.IStart(mBlockWM)) {
624           mIStart.mISize = clippedMarkerEdges.mIStart -
625                            nonSnappedContentArea.IStart(mBlockWM);
626           NS_ASSERTION(mIStart.mISize < mIStart.mIntrinsicISize,
627                        "clipping a marker should make it strictly smaller");
628           clippedIStartMarker = true;
629         } else {
630           mIStart.mActive = guessIStart = false;
631         }
632         continue;
633       }
634       if (mIEnd.IsNeeded() && mIEnd.mActive && !clippedIEndMarker) {
635         if (clippedMarkerEdges.mAssignedIEnd &&
636             nonSnappedContentArea.IEnd(mBlockWM) > clippedMarkerEdges.mIEnd) {
637           mIEnd.mISize =
638               nonSnappedContentArea.IEnd(mBlockWM) - clippedMarkerEdges.mIEnd;
639           NS_ASSERTION(mIEnd.mISize < mIEnd.mIntrinsicISize,
640                        "clipping a marker should make it strictly smaller");
641           clippedIEndMarker = true;
642         } else {
643           mIEnd.mActive = guessIEnd = false;
644         }
645         continue;
646       }
647       // The line simply has no visible content even without markers,
648       // so examine the line again without suppressing markers.
649       retryEmptyLine = false;
650       mIStart.mISize = mIStart.mIntrinsicISize;
651       mIStart.mActive = guessIStart = istartWantsMarker;
652       mIEnd.mISize = mIEnd.mIntrinsicISize;
653       mIEnd.mActive = guessIEnd = iendWantsMarker;
654       // If we wanted to place a block ellipsis but didn't, due to not having
655       // any visible content to align to or the line's content being scrolled
656       // out of view, then clip the ellipsis so that it looks like it is aligned
657       // with the out of view content.
658       if (mIEnd.IsNeeded() && mIEnd.mActive && mIEnd.mHasBlockEllipsis) {
659         NS_ASSERTION(nonSnappedContentArea.IStart(mBlockWM) >
660                          aAlignmentEdges->mIEndOuter,
661                      "Expected the alignment edge for the out of view content "
662                      "to be before the start of the content area");
663         mIEnd.mISize = std::max(
664             mIEnd.mIntrinsicISize - (nonSnappedContentArea.IStart(mBlockWM) -
665                                      aAlignmentEdges->mIEndOuter),
666             0);
667       }
668       continue;
669     }
670     if (guessIStart == (mIStart.mActive && mIStart.IsNeeded()) &&
671         guessIEnd == (mIEnd.mActive && mIEnd.IsNeeded())) {
672       break;
673     } else {
674       guessIStart = mIStart.mActive && mIStart.IsNeeded();
675       guessIEnd = mIEnd.mActive && mIEnd.IsNeeded();
676       mIStart.Reset();
677       mIEnd.Reset();
678       aFramesToHide->Clear();
679     }
680     NS_ASSERTION(pass == 0, "2nd pass should never guess wrong");
681   } while (++pass != 2);
682   if (!istartWantsMarker || !mIStart.mActive) {
683     mIStart.Reset();
684   }
685   if (!iendWantsMarker || !mIEnd.mActive) {
686     mIEnd.Reset();
687   }
688   return nonSnappedContentArea;
689 }
690 
ProcessLine(const nsDisplayListSet & aLists,nsLineBox * aLine,uint32_t aLineNumber)691 void TextOverflow::ProcessLine(const nsDisplayListSet& aLists, nsLineBox* aLine,
692                                uint32_t aLineNumber) {
693   if (mIStart.mStyle->IsClip() && mIEnd.mStyle->IsClip() &&
694       !aLine->HasLineClampEllipsis()) {
695     return;
696   }
697 
698   mIStart.Reset();
699   mIStart.mActive = !mIStart.mStyle->IsClip();
700   mIEnd.Reset();
701   mIEnd.mHasBlockEllipsis = aLine->HasLineClampEllipsis();
702   mIEnd.mActive = !mIEnd.mStyle->IsClip() || aLine->HasLineClampEllipsis();
703 
704   FrameHashtable framesToHide(64);
705   AlignmentEdges alignmentEdges;
706   const LogicalRect contentArea =
707       ExamineLineFrames(aLine, &framesToHide, &alignmentEdges);
708   bool needIStart = mIStart.IsNeeded();
709   bool needIEnd = mIEnd.IsNeeded();
710   if (!needIStart && !needIEnd) {
711     return;
712   }
713   NS_ASSERTION(!mIStart.IsSuppressed() || !needIStart,
714                "left marker when not needed");
715   NS_ASSERTION(!mIEnd.IsSuppressed() || !needIEnd,
716                "right marker when not needed");
717 
718   // If there is insufficient space for both markers then keep the one on the
719   // end side per the block's 'direction'.
720   if (needIStart && needIEnd &&
721       mIStart.mISize + mIEnd.mISize > contentArea.ISize(mBlockWM)) {
722     needIStart = false;
723   }
724   LogicalRect insideMarkersArea = contentArea;
725   if (needIStart) {
726     InflateIStart(mBlockWM, &insideMarkersArea, -mIStart.mISize);
727   }
728   if (needIEnd) {
729     InflateIEnd(mBlockWM, &insideMarkersArea, -mIEnd.mISize);
730   }
731 
732   if (alignmentEdges.mAssignedInner) {
733     if (mIStart.mEdgeAligned) {
734       alignmentEdges.mIStart = insideMarkersArea.IStart(mBlockWM);
735     }
736     if (mIEnd.mEdgeAligned) {
737       alignmentEdges.mIEnd = insideMarkersArea.IEnd(mBlockWM);
738     }
739     LogicalRect alignmentRect(mBlockWM, alignmentEdges.mIStart,
740                               insideMarkersArea.BStart(mBlockWM),
741                               alignmentEdges.ISize(), 1);
742     insideMarkersArea.IntersectRect(insideMarkersArea, alignmentRect);
743   } else {
744     // There was no content on the line that was visible at the current scolled
745     // position.  If we wanted to place a block ellipsis but failed due to
746     // having no visible content to align it to, we still need to ensure it
747     // is displayed.  It goes at the start of the line, even though it's an
748     // IEnd marker, since that is the side of the line that the content has
749     // been scrolled past.  We set the insideMarkersArea to a zero-sized
750     // rectangle placed next to the scrolled-out-of-view content.
751     if (mIEnd.mHasBlockEllipsis) {
752       insideMarkersArea = LogicalRect(mBlockWM, alignmentEdges.mIEndOuter,
753                                       insideMarkersArea.BStart(mBlockWM), 0, 1);
754     }
755   }
756 
757   // Clip and remove display items as needed at the final marker edges.
758   nsDisplayList* lists[] = {aLists.Content(), aLists.PositionedDescendants()};
759   for (uint32_t i = 0; i < ArrayLength(lists); ++i) {
760     PruneDisplayListContents(lists[i], framesToHide, insideMarkersArea);
761   }
762   CreateMarkers(aLine, needIStart, needIEnd, insideMarkersArea, contentArea,
763                 aLineNumber);
764 }
765 
PruneDisplayListContents(nsDisplayList * aList,const FrameHashtable & aFramesToHide,const LogicalRect & aInsideMarkersArea)766 void TextOverflow::PruneDisplayListContents(
767     nsDisplayList* aList, const FrameHashtable& aFramesToHide,
768     const LogicalRect& aInsideMarkersArea) {
769   for (nsDisplayItem* item : aList->TakeItems()) {
770     nsIFrame* itemFrame = item->Frame();
771     if (IsFrameDescendantOfAny(itemFrame, aFramesToHide, mBlock)) {
772       item->Destroy(mBuilder);
773       continue;
774     }
775 
776     nsDisplayList* wrapper = item->GetSameCoordinateSystemChildren();
777     if (wrapper) {
778       if (!itemFrame || GetSelfOrNearestBlock(itemFrame) == mBlock) {
779         PruneDisplayListContents(wrapper, aFramesToHide, aInsideMarkersArea);
780       }
781     }
782 
783     nsDisplayText* textItem =
784         itemFrame ? nsDisplayText::CheckCast(item) : nullptr;
785     if (textItem && GetSelfOrNearestBlock(itemFrame) == mBlock) {
786       LogicalRect rect =
787           GetLogicalScrollableOverflowRectRelativeToBlock(itemFrame);
788       if (mIStart.IsNeeded()) {
789         nscoord istart =
790             aInsideMarkersArea.IStart(mBlockWM) - rect.IStart(mBlockWM);
791         if (istart > 0) {
792           (mBlockWM.IsBidiLTR() ? textItem->VisIStartEdge()
793                                 : textItem->VisIEndEdge()) = istart;
794         }
795       }
796       if (mIEnd.IsNeeded()) {
797         nscoord iend = rect.IEnd(mBlockWM) - aInsideMarkersArea.IEnd(mBlockWM);
798         if (iend > 0) {
799           (mBlockWM.IsBidiLTR() ? textItem->VisIEndEdge()
800                                 : textItem->VisIStartEdge()) = iend;
801         }
802       }
803     }
804 
805     aList->AppendToTop(item);
806   }
807 }
808 
809 /* static */
HasClippedTextOverflow(nsIFrame * aBlockFrame)810 bool TextOverflow::HasClippedTextOverflow(nsIFrame* aBlockFrame) {
811   const nsStyleTextReset* style = aBlockFrame->StyleTextReset();
812   return style->mTextOverflow.first.IsClip() &&
813          style->mTextOverflow.second.IsClip();
814 }
815 
816 /* static */
HasBlockEllipsis(nsIFrame * aBlockFrame)817 bool TextOverflow::HasBlockEllipsis(nsIFrame* aBlockFrame) {
818   nsBlockFrame* f = do_QueryFrame(aBlockFrame);
819   return f && f->HasAnyStateBits(NS_BLOCK_HAS_LINE_CLAMP_ELLIPSIS);
820 }
821 
822 /* static */
CanHaveOverflowMarkers(nsIFrame * aBlockFrame)823 bool TextOverflow::CanHaveOverflowMarkers(nsIFrame* aBlockFrame) {
824   // Treat a line with a -webkit-line-clamp ellipsis as a kind of text
825   // overflow.
826   if (aBlockFrame->HasAnyStateBits(NS_BLOCK_HAS_LINE_CLAMP_ELLIPSIS)) {
827     return true;
828   }
829 
830   // Nothing to do for text-overflow:clip or if 'overflow-x/y:visible'.
831   if (HasClippedTextOverflow(aBlockFrame) ||
832       IsInlineAxisOverflowVisible(aBlockFrame)) {
833     return false;
834   }
835 
836   // Skip ComboboxControlFrame because it would clip the drop-down arrow.
837   // Its anon block inherits 'text-overflow' and does what is expected.
838   if (aBlockFrame->IsComboboxControlFrame()) {
839     return false;
840   }
841 
842   // Inhibit the markers if a descendant content owns the caret.
843   RefPtr<nsCaret> caret = aBlockFrame->PresShell()->GetCaret();
844   if (caret && caret->IsVisible()) {
845     RefPtr<dom::Selection> domSelection = caret->GetSelection();
846     if (domSelection) {
847       nsCOMPtr<nsIContent> content =
848           nsIContent::FromNodeOrNull(domSelection->GetFocusNode());
849       if (content &&
850           content->IsInclusiveDescendantOf(aBlockFrame->GetContent())) {
851         return false;
852       }
853     }
854   }
855   return true;
856 }
857 
CreateMarkers(const nsLineBox * aLine,bool aCreateIStart,bool aCreateIEnd,const LogicalRect & aInsideMarkersArea,const LogicalRect & aContentArea,uint32_t aLineNumber)858 void TextOverflow::CreateMarkers(const nsLineBox* aLine, bool aCreateIStart,
859                                  bool aCreateIEnd,
860                                  const LogicalRect& aInsideMarkersArea,
861                                  const LogicalRect& aContentArea,
862                                  uint32_t aLineNumber) {
863   if (!mBlock->IsVisibleForPainting()) {
864     return;
865   }
866 
867   if (aCreateIStart) {
868     DisplayListClipState::AutoSaveRestore clipState(mBuilder);
869 
870     LogicalRect markerLogicalRect(
871         mBlockWM, aInsideMarkersArea.IStart(mBlockWM) - mIStart.mIntrinsicISize,
872         aLine->BStart(), mIStart.mIntrinsicISize, aLine->BSize());
873     nsPoint offset = mBuilder->ToReferenceFrame(mBlock);
874     nsRect markerRect =
875         markerLogicalRect.GetPhysicalRect(mBlockWM, mBlockSize) + offset;
876     ClipMarker(aContentArea.GetPhysicalRect(mBlockWM, mBlockSize) + offset,
877                markerRect, clipState);
878 
879     mMarkerList.AppendNewToTopWithIndex<nsDisplayTextOverflowMarker>(
880         mBuilder, mBlock, /* aIndex = */ (aLineNumber << 1) + 0, markerRect,
881         aLine->GetLogicalAscent(), *mIStart.mStyle);
882   }
883 
884   if (aCreateIEnd) {
885     DisplayListClipState::AutoSaveRestore clipState(mBuilder);
886 
887     LogicalRect markerLogicalRect(mBlockWM, aInsideMarkersArea.IEnd(mBlockWM),
888                                   aLine->BStart(), mIEnd.mIntrinsicISize,
889                                   aLine->BSize());
890     nsPoint offset = mBuilder->ToReferenceFrame(mBlock);
891     nsRect markerRect =
892         markerLogicalRect.GetPhysicalRect(mBlockWM, mBlockSize) + offset;
893     ClipMarker(aContentArea.GetPhysicalRect(mBlockWM, mBlockSize) + offset,
894                markerRect, clipState);
895 
896     mMarkerList.AppendNewToTopWithIndex<nsDisplayTextOverflowMarker>(
897         mBuilder, mBlock, /* aIndex = */ (aLineNumber << 1) + 1, markerRect,
898         aLine->GetLogicalAscent(),
899         mIEnd.mHasBlockEllipsis ? StyleTextOverflowSide::Ellipsis()
900                                 : *mIEnd.mStyle);
901   }
902 }
903 
SetupString(nsIFrame * aFrame)904 void TextOverflow::Marker::SetupString(nsIFrame* aFrame) {
905   if (mInitialized) {
906     return;
907   }
908 
909   // A limitation here is that at the IEnd of a line, we only ever render one of
910   // a text-overflow marker and a -webkit-line-clamp block ellipsis.  Since we
911   // don't track the block ellipsis string and the text-overflow marker string
912   // separately, if both apply to the element, we will always use "…" as the
913   // string for text-overflow.
914   if (HasBlockEllipsis(aFrame) || mStyle->IsEllipsis()) {
915     gfxTextRun* textRun = GetEllipsisTextRun(aFrame);
916     if (textRun) {
917       mISize = textRun->GetAdvanceWidth();
918     } else {
919       mISize = 0;
920     }
921   } else {
922     RefPtr<gfxContext> rc =
923         aFrame->PresShell()->CreateReferenceRenderingContext();
924     RefPtr<nsFontMetrics> fm =
925         nsLayoutUtils::GetInflatedFontMetricsForFrame(aFrame);
926     mISize = nsLayoutUtils::AppUnitWidthOfStringBidi(
927         NS_ConvertUTF8toUTF16(mStyle->AsString().AsString()), aFrame, *fm, *rc);
928   }
929   mIntrinsicISize = mISize;
930   mInitialized = true;
931 }
932 
933 }  // namespace css
934 }  // namespace mozilla
935