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