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