Bug 1869043 allow a device to be specified with MediaTrackGraph::NotifyWhenDeviceStar...
[gecko.git] / layout / generic / TextOverflow.cpp
blobf529a2268d7673eaaca9f478f747c7387ee47eb8
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/. */
7 #include "TextOverflow.h"
8 #include <algorithm>
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"
30 using mozilla::layout::TextDrawTarget;
32 namespace mozilla {
33 namespace css {
35 class LazyReferenceRenderingDrawTargetGetterFromFrame final
36 : public gfxFontGroup::LazyReferenceDrawTargetGetter {
37 public:
38 typedef mozilla::gfx::DrawTarget DrawTarget;
40 explicit LazyReferenceRenderingDrawTargetGetterFromFrame(nsIFrame* aFrame)
41 : mFrame(aFrame) {}
42 virtual already_AddRefed<DrawTarget> GetRefDrawTarget() override {
43 UniquePtr<gfxContext> ctx =
44 mFrame->PresShell()->CreateReferenceRenderingContext();
45 RefPtr<DrawTarget> dt = ctx->GetDrawTarget();
46 return dt.forget();
49 private:
50 nsIFrame* mFrame;
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);
64 static nsIFrame* GetSelfOrNearestBlock(nsIFrame* aFrame) {
65 MOZ_ASSERT(aFrame);
66 return aFrame->IsBlockFrameOrSubclass()
67 ? aFrame
68 : nsLayoutUtils::FindNearestBlockAncestor(aFrame);
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.
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->IsLineParticipant();
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;
89 return !aFrame->MeasureCharClippedText(aLeft, aRight, aSnappedLeft,
90 aSnappedRight);
93 static bool IsInlineAxisOverflowVisible(nsIFrame* aFrame) {
94 MOZ_ASSERT(aFrame && aFrame->IsBlockFrameOrSubclass(),
95 "expected a block frame");
97 nsIFrame* f = aFrame;
98 while (f && f->Style()->IsAnonBox() && !f->IsScrollFrame()) {
99 f = f->GetParent();
101 if (!f) {
102 return true;
104 auto overflow = aFrame->GetWritingMode().IsVertical()
105 ? f->StyleDisplay()->mOverflowY
106 : f->StyleDisplay()->mOverflowX;
107 return overflow == StyleOverflow::Visible;
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);
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);
135 static void InflateIEnd(WritingMode aWM, LogicalRect* aRect, nscoord aDelta) {
136 aRect->ISize(aWM) = std::max(aRect->ISize(aWM) + aDelta, 0);
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;
148 return false;
151 class nsDisplayTextOverflowMarker final : public nsPaintedDisplayItem {
152 public:
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);
163 MOZ_COUNTED_DTOR_OVERRIDE(nsDisplayTextOverflowMarker)
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);
172 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();
182 bool snap;
183 return GetBounds(aBuilder, &snap);
186 virtual void Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) override;
188 void PaintTextToContext(gfxContext* aCtx, nsPoint aOffsetFromRect);
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;
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
204 static void PaintTextShadowCallback(gfxContext* aCtx, nsPoint aShadowOffset,
205 const nscolor& aShadowColor, void* aData) {
206 reinterpret_cast<nsDisplayTextOverflowMarker*>(aData)->PaintTextToContext(
207 aCtx, aShadowOffset);
210 void nsDisplayTextOverflowMarker::Paint(nsDisplayListBuilder* aBuilder,
211 gfxContext* aCtx) {
212 nscolor foregroundColor =
213 nsLayoutUtils::GetColor(mFrame, &nsStyleText::mWebkitTextFillColor);
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));
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));
235 } else {
236 pt.y = NSToCoordFloor(
237 nsLayoutUtils::GetSnappedBaselineY(mFrame, aCtx, pt.y, mAscent));
239 pt += aOffsetFromRect;
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 auto& paletteCache = mFrame->PresContext()->FontPaletteCache();
248 textRun->Draw(gfxTextRun::Range(textRun), gfxPt,
249 gfxTextRun::DrawParams(aCtx, paletteCache));
251 } else {
252 RefPtr<nsFontMetrics> fm =
253 nsLayoutUtils::GetInflatedFontMetricsForFrame(mFrame);
254 NS_ConvertUTF8toUTF16 str16{mStyle.AsString().AsString()};
255 nsLayoutUtils::DrawString(mFrame, *fm, aCtx, str16.get(), str16.Length(),
256 pt);
260 bool nsDisplayTextOverflowMarker::CreateWebRenderCommands(
261 mozilla::wr::DisplayListBuilder& aBuilder,
262 mozilla::wr::IpcResourceUpdateQueue& aResources,
263 const StackingContextHelper& aSc, layers::RenderRootStateManager* aManager,
264 nsDisplayListBuilder* aDisplayListBuilder) {
265 bool snap;
266 nsRect bounds = GetBounds(aDisplayListBuilder, &snap);
267 if (bounds.IsEmpty()) {
268 return true;
271 // Run the rendering algorithm to capture the glyphs and shadows
272 RefPtr<TextDrawTarget> textDrawer =
273 new TextDrawTarget(aBuilder, aResources, aSc, aManager, this, bounds);
274 MOZ_ASSERT(textDrawer->IsValid());
275 if (!textDrawer->IsValid()) {
276 return false;
278 gfxContext captureCtx(textDrawer);
279 Paint(aDisplayListBuilder, &captureCtx);
280 textDrawer->TerminateShadows();
282 return textDrawer->Finish();
285 TextOverflow::TextOverflow(nsDisplayListBuilder* aBuilder,
286 nsBlockFrame* aBlockFrame)
287 : mContentArea(aBlockFrame->GetWritingMode(),
288 aBlockFrame->GetContentRectRelativeToSelf(),
289 aBlockFrame->GetSize()),
290 mBuilder(aBuilder),
291 mBlock(aBlockFrame),
292 mScrollableFrame(nsLayoutUtils::GetScrollableFrameFor(aBlockFrame)),
293 mMarkerList(aBuilder),
294 mBlockSize(aBlockFrame->GetSize()),
295 mBlockWM(aBlockFrame->GetWritingMode()),
296 mCanHaveInlineAxisScrollbar(false),
297 mInLineClampContext(aBlockFrame->IsInLineClampContext()),
298 mAdjustForPixelSnapping(false) {
299 if (mScrollableFrame) {
300 auto scrollbarStyle = mBlockWM.IsVertical()
301 ? mScrollableFrame->GetScrollStyles().mVertical
302 : mScrollableFrame->GetScrollStyles().mHorizontal;
303 mCanHaveInlineAxisScrollbar = scrollbarStyle != StyleOverflow::Hidden;
304 if (!mAdjustForPixelSnapping) {
305 // Scrolling to the end position can leave some text still overflowing due
306 // to pixel snapping behaviour in our scrolling code.
307 mAdjustForPixelSnapping = mCanHaveInlineAxisScrollbar;
309 // Use a null containerSize to convert a vector from logical to physical.
310 const nsSize nullContainerSize;
311 mContentArea.MoveBy(
312 mBlockWM, LogicalPoint(mBlockWM, mScrollableFrame->GetScrollPosition(),
313 nullContainerSize));
315 StyleDirection direction = aBlockFrame->StyleVisibility()->mDirection;
316 const nsStyleTextReset* style = aBlockFrame->StyleTextReset();
318 const auto& textOverflow = style->mTextOverflow;
319 bool shouldToggleDirection =
320 textOverflow.sides_are_logical && (direction == StyleDirection::Rtl);
321 const auto& leftSide =
322 shouldToggleDirection ? textOverflow.second : textOverflow.first;
323 const auto& rightSide =
324 shouldToggleDirection ? textOverflow.first : textOverflow.second;
326 if (mBlockWM.IsBidiLTR()) {
327 mIStart.Init(leftSide);
328 mIEnd.Init(rightSide);
329 } else {
330 mIStart.Init(rightSide);
331 mIEnd.Init(leftSide);
333 // The left/right marker string is setup in ExamineLineFrames when a line
334 // has overflow on that side.
337 /* static */
338 Maybe<TextOverflow> TextOverflow::WillProcessLines(
339 nsDisplayListBuilder* aBuilder, nsBlockFrame* aBlockFrame) {
340 // Ignore text-overflow and -webkit-line-clamp for event and frame visibility
341 // processing.
342 if (aBuilder->IsForEventDelivery() || aBuilder->IsForFrameVisibility() ||
343 !CanHaveOverflowMarkers(aBlockFrame)) {
344 return Nothing();
346 nsIScrollableFrame* scrollableFrame =
347 nsLayoutUtils::GetScrollableFrameFor(aBlockFrame);
348 if (scrollableFrame && scrollableFrame->IsTransformingByAPZ()) {
349 // If the APZ is actively scrolling this, don't bother with markers.
350 return Nothing();
352 return Some(TextOverflow(aBuilder, aBlockFrame));
355 void TextOverflow::ExamineFrameSubtree(nsIFrame* aFrame,
356 const LogicalRect& aContentArea,
357 const LogicalRect& aInsideMarkersArea,
358 FrameHashtable* aFramesToHide,
359 AlignmentEdges* aAlignmentEdges,
360 bool* aFoundVisibleTextOrAtomic,
361 InnerClipEdges* aClippedMarkerEdges) {
362 const LayoutFrameType frameType = aFrame->Type();
363 if (frameType == LayoutFrameType::Br ||
364 frameType == LayoutFrameType::Placeholder) {
365 return;
367 const bool isAtomic = IsAtomicElement(aFrame, frameType);
368 if (aFrame->StyleVisibility()->IsVisible()) {
369 LogicalRect childRect =
370 GetLogicalScrollableOverflowRectRelativeToBlock(aFrame);
371 bool overflowIStart =
372 childRect.IStart(mBlockWM) < aContentArea.IStart(mBlockWM);
373 bool overflowIEnd = childRect.IEnd(mBlockWM) > aContentArea.IEnd(mBlockWM);
374 if (overflowIStart) {
375 mIStart.mHasOverflow = true;
377 if (overflowIEnd) {
378 mIEnd.mHasOverflow = true;
380 if (isAtomic && ((mIStart.mActive && overflowIStart) ||
381 (mIEnd.mActive && overflowIEnd))) {
382 aFramesToHide->Insert(aFrame);
383 } else if (isAtomic || frameType == LayoutFrameType::Text) {
384 AnalyzeMarkerEdges(aFrame, frameType, aInsideMarkersArea, aFramesToHide,
385 aAlignmentEdges, aFoundVisibleTextOrAtomic,
386 aClippedMarkerEdges);
389 if (isAtomic) {
390 return;
393 for (nsIFrame* child : aFrame->PrincipalChildList()) {
394 ExamineFrameSubtree(child, aContentArea, aInsideMarkersArea, aFramesToHide,
395 aAlignmentEdges, aFoundVisibleTextOrAtomic,
396 aClippedMarkerEdges);
400 void TextOverflow::AnalyzeMarkerEdges(nsIFrame* aFrame,
401 LayoutFrameType aFrameType,
402 const LogicalRect& aInsideMarkersArea,
403 FrameHashtable* aFramesToHide,
404 AlignmentEdges* aAlignmentEdges,
405 bool* aFoundVisibleTextOrAtomic,
406 InnerClipEdges* aClippedMarkerEdges) {
407 MOZ_ASSERT(aFrameType == LayoutFrameType::Text ||
408 IsAtomicElement(aFrame, aFrameType));
409 LogicalRect borderRect(mBlockWM,
410 nsRect(aFrame->GetOffsetTo(mBlock), aFrame->GetSize()),
411 mBlockSize);
412 nscoord istartOverlap = std::max(
413 aInsideMarkersArea.IStart(mBlockWM) - borderRect.IStart(mBlockWM), 0);
414 nscoord iendOverlap = std::max(
415 borderRect.IEnd(mBlockWM) - aInsideMarkersArea.IEnd(mBlockWM), 0);
416 bool insideIStartEdge =
417 aInsideMarkersArea.IStart(mBlockWM) <= borderRect.IEnd(mBlockWM);
418 bool insideIEndEdge =
419 borderRect.IStart(mBlockWM) <= aInsideMarkersArea.IEnd(mBlockWM);
421 if (istartOverlap > 0) {
422 aClippedMarkerEdges->AccumulateIStart(mBlockWM, borderRect);
423 if (!mIStart.mActive) {
424 istartOverlap = 0;
427 if (iendOverlap > 0) {
428 aClippedMarkerEdges->AccumulateIEnd(mBlockWM, borderRect);
429 if (!mIEnd.mActive) {
430 iendOverlap = 0;
434 if ((istartOverlap > 0 && insideIStartEdge) ||
435 (iendOverlap > 0 && insideIEndEdge)) {
436 if (aFrameType == LayoutFrameType::Text) {
437 auto textFrame = static_cast<nsTextFrame*>(aFrame);
438 if ((aInsideMarkersArea.IStart(mBlockWM) <
439 aInsideMarkersArea.IEnd(mBlockWM)) &&
440 textFrame->HasNonSuppressedText()) {
441 // a clipped text frame and there is some room between the markers
442 nscoord snappedIStart, snappedIEnd;
443 bool isFullyClipped =
444 mBlockWM.IsBidiLTR()
445 ? IsFullyClipped(textFrame, istartOverlap, iendOverlap,
446 &snappedIStart, &snappedIEnd)
447 : IsFullyClipped(textFrame, iendOverlap, istartOverlap,
448 &snappedIEnd, &snappedIStart);
449 if (!isFullyClipped) {
450 LogicalRect snappedRect = borderRect;
451 if (istartOverlap > 0) {
452 snappedRect.IStart(mBlockWM) += snappedIStart;
453 snappedRect.ISize(mBlockWM) -= snappedIStart;
455 if (iendOverlap > 0) {
456 snappedRect.ISize(mBlockWM) -= snappedIEnd;
458 aAlignmentEdges->AccumulateInner(mBlockWM, snappedRect);
459 *aFoundVisibleTextOrAtomic = true;
462 } else {
463 aFramesToHide->Insert(aFrame);
465 } else if (!insideIStartEdge || !insideIEndEdge) {
466 // frame is outside
467 if (!insideIStartEdge) {
468 aAlignmentEdges->AccumulateOuter(mBlockWM, borderRect);
470 if (IsAtomicElement(aFrame, aFrameType)) {
471 aFramesToHide->Insert(aFrame);
473 } else {
474 // frame is inside
475 aAlignmentEdges->AccumulateInner(mBlockWM, borderRect);
476 if (aFrameType == LayoutFrameType::Text) {
477 auto textFrame = static_cast<nsTextFrame*>(aFrame);
478 if (textFrame->HasNonSuppressedText()) {
479 *aFoundVisibleTextOrAtomic = true;
481 } else {
482 *aFoundVisibleTextOrAtomic = true;
487 LogicalRect TextOverflow::ExamineLineFrames(nsLineBox* aLine,
488 FrameHashtable* aFramesToHide,
489 AlignmentEdges* aAlignmentEdges) {
490 // No ellipsing for 'clip' style.
491 bool suppressIStart = mIStart.IsSuppressed(mInLineClampContext);
492 bool suppressIEnd = mIEnd.IsSuppressed(mInLineClampContext);
493 if (mCanHaveInlineAxisScrollbar) {
494 LogicalPoint pos(mBlockWM, mScrollableFrame->GetScrollPosition(),
495 mBlockSize);
496 LogicalRect scrollRange(mBlockWM, mScrollableFrame->GetScrollRange(),
497 mBlockSize);
498 // No ellipsing when nothing to scroll to on that side (this includes
499 // overflow:auto that doesn't trigger a horizontal scrollbar).
500 if (pos.I(mBlockWM) <= scrollRange.IStart(mBlockWM)) {
501 suppressIStart = true;
503 if (pos.I(mBlockWM) >= scrollRange.IEnd(mBlockWM)) {
504 // Except that we always want to display a -webkit-line-clamp ellipsis.
505 if (!mIEnd.mHasBlockEllipsis) {
506 suppressIEnd = true;
511 LogicalRect contentArea = mContentArea;
512 bool snapStart = true, snapEnd = true;
513 nscoord startEdge, endEdge;
514 if (aLine->GetFloatEdges(&startEdge, &endEdge)) {
515 // Narrow the |contentArea| to account for any floats on this line, and
516 // don't bother with the snapping quirk on whichever side(s) we narrow.
517 nscoord delta = endEdge - contentArea.IEnd(mBlockWM);
518 if (delta < 0) {
519 nscoord newSize = contentArea.ISize(mBlockWM) + delta;
520 contentArea.ISize(mBlockWM) = std::max(nscoord(0), newSize);
521 snapEnd = false;
523 delta = startEdge - contentArea.IStart(mBlockWM);
524 if (delta > 0) {
525 contentArea.IStart(mBlockWM) = startEdge;
526 nscoord newSize = contentArea.ISize(mBlockWM) - delta;
527 contentArea.ISize(mBlockWM) = std::max(nscoord(0), newSize);
528 snapStart = false;
531 // Save the non-snapped area since that's what we want to use when placing
532 // the markers (our return value). The snapped area is only for analysis.
533 LogicalRect nonSnappedContentArea = contentArea;
534 if (mAdjustForPixelSnapping) {
535 const nscoord scrollAdjust = mBlock->PresContext()->AppUnitsPerDevPixel();
536 if (snapStart) {
537 InflateIStart(mBlockWM, &contentArea, scrollAdjust);
539 if (snapEnd) {
540 InflateIEnd(mBlockWM, &contentArea, scrollAdjust);
544 LogicalRect lineRect(mBlockWM, aLine->ScrollableOverflowRect(), mBlockSize);
545 const bool istartWantsMarker =
546 !suppressIStart &&
547 lineRect.IStart(mBlockWM) < contentArea.IStart(mBlockWM);
548 const bool iendWantsTextOverflowMarker =
549 !suppressIEnd && lineRect.IEnd(mBlockWM) > contentArea.IEnd(mBlockWM);
550 const bool iendWantsBlockEllipsisMarker =
551 !suppressIEnd && mIEnd.mHasBlockEllipsis;
552 const bool iendWantsMarker =
553 iendWantsTextOverflowMarker || iendWantsBlockEllipsisMarker;
554 if (!istartWantsMarker && !iendWantsMarker) {
555 // We don't need any markers on this line.
556 return nonSnappedContentArea;
559 int pass = 0;
560 bool retryEmptyLine = true;
561 bool guessIStart = istartWantsMarker;
562 bool guessIEnd = iendWantsMarker;
563 mIStart.mActive = istartWantsMarker;
564 mIEnd.mActive = iendWantsMarker;
565 mIStart.mEdgeAligned = mCanHaveInlineAxisScrollbar && istartWantsMarker;
566 mIEnd.mEdgeAligned =
567 mCanHaveInlineAxisScrollbar && iendWantsTextOverflowMarker;
568 bool clippedIStartMarker = false;
569 bool clippedIEndMarker = false;
570 do {
571 // Setup marker strings as needed.
572 if (guessIStart) {
573 mIStart.SetupString(mBlock);
575 if (guessIEnd) {
576 mIEnd.SetupString(mBlock);
579 // If there is insufficient space for both markers then keep the one on the
580 // end side per the block's 'direction'.
581 nscoord istartMarkerISize = mIStart.mActive ? mIStart.mISize : 0;
582 nscoord iendMarkerISize = mIEnd.mActive ? mIEnd.mISize : 0;
583 if (istartMarkerISize && iendMarkerISize &&
584 istartMarkerISize + iendMarkerISize > contentArea.ISize(mBlockWM)) {
585 istartMarkerISize = 0;
588 // Calculate the area between the potential markers aligned at the
589 // block's edge.
590 LogicalRect insideMarkersArea = nonSnappedContentArea;
591 if (guessIStart) {
592 InflateIStart(mBlockWM, &insideMarkersArea, -istartMarkerISize);
594 if (guessIEnd) {
595 InflateIEnd(mBlockWM, &insideMarkersArea, -iendMarkerISize);
598 // Analyze the frames on aLine for the overflow situation at the content
599 // edges and at the edges of the area between the markers.
600 bool foundVisibleTextOrAtomic = false;
601 int32_t n = aLine->GetChildCount();
602 nsIFrame* child = aLine->mFirstChild;
603 InnerClipEdges clippedMarkerEdges;
604 for (; n-- > 0; child = child->GetNextSibling()) {
605 ExamineFrameSubtree(child, contentArea, insideMarkersArea, aFramesToHide,
606 aAlignmentEdges, &foundVisibleTextOrAtomic,
607 &clippedMarkerEdges);
609 if (!foundVisibleTextOrAtomic && retryEmptyLine) {
610 aAlignmentEdges->mAssignedInner = false;
611 aAlignmentEdges->mIEndOuter = 0;
612 aFramesToHide->Clear();
613 pass = -1;
614 if (mIStart.IsNeeded() && mIStart.mActive && !clippedIStartMarker) {
615 if (clippedMarkerEdges.mAssignedIStart &&
616 clippedMarkerEdges.mIStart >
617 nonSnappedContentArea.IStart(mBlockWM)) {
618 mIStart.mISize = clippedMarkerEdges.mIStart -
619 nonSnappedContentArea.IStart(mBlockWM);
620 NS_ASSERTION(mIStart.mISize < mIStart.mIntrinsicISize,
621 "clipping a marker should make it strictly smaller");
622 clippedIStartMarker = true;
623 } else {
624 mIStart.mActive = guessIStart = false;
626 continue;
628 if (mIEnd.IsNeeded() && mIEnd.mActive && !clippedIEndMarker) {
629 if (clippedMarkerEdges.mAssignedIEnd &&
630 nonSnappedContentArea.IEnd(mBlockWM) > clippedMarkerEdges.mIEnd) {
631 mIEnd.mISize =
632 nonSnappedContentArea.IEnd(mBlockWM) - clippedMarkerEdges.mIEnd;
633 NS_ASSERTION(mIEnd.mISize < mIEnd.mIntrinsicISize,
634 "clipping a marker should make it strictly smaller");
635 clippedIEndMarker = true;
636 } else {
637 mIEnd.mActive = guessIEnd = false;
639 continue;
641 // The line simply has no visible content even without markers,
642 // so examine the line again without suppressing markers.
643 retryEmptyLine = false;
644 mIStart.mISize = mIStart.mIntrinsicISize;
645 mIStart.mActive = guessIStart = istartWantsMarker;
646 mIEnd.mISize = mIEnd.mIntrinsicISize;
647 mIEnd.mActive = guessIEnd = iendWantsMarker;
648 // If we wanted to place a block ellipsis but didn't, due to not having
649 // any visible content to align to or the line's content being scrolled
650 // out of view, then clip the ellipsis so that it looks like it is aligned
651 // with the out of view content.
652 if (mIEnd.IsNeeded() && mIEnd.mActive && mIEnd.mHasBlockEllipsis) {
653 NS_ASSERTION(nonSnappedContentArea.IStart(mBlockWM) >
654 aAlignmentEdges->mIEndOuter,
655 "Expected the alignment edge for the out of view content "
656 "to be before the start of the content area");
657 mIEnd.mISize = std::max(
658 mIEnd.mIntrinsicISize - (nonSnappedContentArea.IStart(mBlockWM) -
659 aAlignmentEdges->mIEndOuter),
662 continue;
664 if (guessIStart == (mIStart.mActive && mIStart.IsNeeded()) &&
665 guessIEnd == (mIEnd.mActive && mIEnd.IsNeeded())) {
666 break;
667 } else {
668 guessIStart = mIStart.mActive && mIStart.IsNeeded();
669 guessIEnd = mIEnd.mActive && mIEnd.IsNeeded();
670 mIStart.Reset();
671 mIEnd.Reset();
672 aFramesToHide->Clear();
674 NS_ASSERTION(pass == 0, "2nd pass should never guess wrong");
675 } while (++pass != 2);
676 if (!istartWantsMarker || !mIStart.mActive) {
677 mIStart.Reset();
679 if (!iendWantsMarker || !mIEnd.mActive) {
680 mIEnd.Reset();
682 return nonSnappedContentArea;
685 void TextOverflow::ProcessLine(const nsDisplayListSet& aLists, nsLineBox* aLine,
686 uint32_t aLineNumber) {
687 if (mIStart.mStyle->IsClip() && mIEnd.mStyle->IsClip() &&
688 !aLine->HasLineClampEllipsis()) {
689 return;
692 mIStart.Reset();
693 mIStart.mActive = !mIStart.mStyle->IsClip();
694 mIEnd.Reset();
695 mIEnd.mHasBlockEllipsis = aLine->HasLineClampEllipsis();
696 mIEnd.mActive = !mIEnd.IsSuppressed(mInLineClampContext);
698 FrameHashtable framesToHide(64);
699 AlignmentEdges alignmentEdges;
700 const LogicalRect contentArea =
701 ExamineLineFrames(aLine, &framesToHide, &alignmentEdges);
702 bool needIStart = mIStart.IsNeeded();
703 bool needIEnd = mIEnd.IsNeeded();
704 if (!needIStart && !needIEnd) {
705 return;
707 NS_ASSERTION(!mIStart.IsSuppressed(mInLineClampContext) || !needIStart,
708 "left marker when not needed");
709 NS_ASSERTION(!mIEnd.IsSuppressed(mInLineClampContext) || !needIEnd,
710 "right marker when not needed");
712 // If there is insufficient space for both markers then keep the one on the
713 // end side per the block's 'direction'.
714 if (needIStart && needIEnd &&
715 mIStart.mISize + mIEnd.mISize > contentArea.ISize(mBlockWM)) {
716 needIStart = false;
718 LogicalRect insideMarkersArea = contentArea;
719 if (needIStart) {
720 InflateIStart(mBlockWM, &insideMarkersArea, -mIStart.mISize);
722 if (needIEnd) {
723 InflateIEnd(mBlockWM, &insideMarkersArea, -mIEnd.mISize);
726 if (alignmentEdges.mAssignedInner) {
727 if (mIStart.mEdgeAligned) {
728 alignmentEdges.mIStart = insideMarkersArea.IStart(mBlockWM);
730 if (mIEnd.mEdgeAligned) {
731 alignmentEdges.mIEnd = insideMarkersArea.IEnd(mBlockWM);
733 LogicalRect alignmentRect(mBlockWM, alignmentEdges.mIStart,
734 insideMarkersArea.BStart(mBlockWM),
735 alignmentEdges.ISize(), 1);
736 insideMarkersArea.IntersectRect(insideMarkersArea, alignmentRect);
737 } else {
738 // There was no content on the line that was visible at the current scolled
739 // position. If we wanted to place a block ellipsis but failed due to
740 // having no visible content to align it to, we still need to ensure it
741 // is displayed. It goes at the start of the line, even though it's an
742 // IEnd marker, since that is the side of the line that the content has
743 // been scrolled past. We set the insideMarkersArea to a zero-sized
744 // rectangle placed next to the scrolled-out-of-view content.
745 if (mIEnd.mHasBlockEllipsis) {
746 insideMarkersArea = LogicalRect(mBlockWM, alignmentEdges.mIEndOuter,
747 insideMarkersArea.BStart(mBlockWM), 0, 1);
751 // Clip and remove display items as needed at the final marker edges.
752 nsDisplayList* lists[] = {aLists.Content(), aLists.PositionedDescendants()};
753 for (uint32_t i = 0; i < ArrayLength(lists); ++i) {
754 PruneDisplayListContents(lists[i], framesToHide, insideMarkersArea);
756 CreateMarkers(aLine, needIStart, needIEnd, insideMarkersArea, contentArea,
757 aLineNumber);
760 void TextOverflow::PruneDisplayListContents(
761 nsDisplayList* aList, const FrameHashtable& aFramesToHide,
762 const LogicalRect& aInsideMarkersArea) {
763 for (nsDisplayItem* item : aList->TakeItems()) {
764 nsIFrame* itemFrame = item->Frame();
765 if (IsFrameDescendantOfAny(itemFrame, aFramesToHide, mBlock)) {
766 item->Destroy(mBuilder);
767 continue;
770 nsDisplayList* wrapper = item->GetSameCoordinateSystemChildren();
771 if (wrapper) {
772 if (!itemFrame || GetSelfOrNearestBlock(itemFrame) == mBlock) {
773 PruneDisplayListContents(wrapper, aFramesToHide, aInsideMarkersArea);
777 nsDisplayText* textItem =
778 itemFrame ? nsDisplayText::CheckCast(item) : nullptr;
779 if (textItem && GetSelfOrNearestBlock(itemFrame) == mBlock) {
780 LogicalRect rect =
781 GetLogicalScrollableOverflowRectRelativeToBlock(itemFrame);
782 if (mIStart.IsNeeded()) {
783 nscoord istart =
784 aInsideMarkersArea.IStart(mBlockWM) - rect.IStart(mBlockWM);
785 if (istart > 0) {
786 (mBlockWM.IsBidiLTR() ? textItem->VisIStartEdge()
787 : textItem->VisIEndEdge()) = istart;
790 if (mIEnd.IsNeeded()) {
791 nscoord iend = rect.IEnd(mBlockWM) - aInsideMarkersArea.IEnd(mBlockWM);
792 if (iend > 0) {
793 (mBlockWM.IsBidiLTR() ? textItem->VisIEndEdge()
794 : textItem->VisIStartEdge()) = iend;
799 aList->AppendToTop(item);
803 /* static */
804 bool TextOverflow::HasClippedTextOverflow(nsIFrame* aBlockFrame) {
805 const nsStyleTextReset* style = aBlockFrame->StyleTextReset();
806 return style->mTextOverflow.first.IsClip() &&
807 style->mTextOverflow.second.IsClip();
810 /* static */
811 bool TextOverflow::HasBlockEllipsis(nsIFrame* aBlockFrame) {
812 nsBlockFrame* f = do_QueryFrame(aBlockFrame);
813 return f && f->HasAnyStateBits(NS_BLOCK_HAS_LINE_CLAMP_ELLIPSIS);
816 static bool BlockCanHaveLineClampEllipsis(nsBlockFrame* aBlockFrame,
817 bool aBeforeReflow) {
818 if (aBeforeReflow) {
819 return aBlockFrame->IsInLineClampContext();
821 return aBlockFrame->HasAnyStateBits(NS_BLOCK_HAS_LINE_CLAMP_ELLIPSIS);
824 /* static */
825 bool TextOverflow::CanHaveOverflowMarkers(nsBlockFrame* aBlockFrame,
826 BeforeReflow aBeforeReflow) {
827 if (BlockCanHaveLineClampEllipsis(aBlockFrame,
828 aBeforeReflow == BeforeReflow::Yes)) {
829 return true;
832 // Nothing to do for text-overflow:clip or if 'overflow-x/y:visible'.
833 if (HasClippedTextOverflow(aBlockFrame) ||
834 IsInlineAxisOverflowVisible(aBlockFrame)) {
835 return false;
838 // Skip ComboboxControlFrame because it would clip the drop-down arrow.
839 // Its anon block inherits 'text-overflow' and does what is expected.
840 if (aBlockFrame->IsComboboxControlFrame()) {
841 return false;
844 // Inhibit the markers if a descendant content owns the caret.
845 RefPtr<nsCaret> caret = aBlockFrame->PresShell()->GetCaret();
846 if (caret && caret->IsVisible()) {
847 RefPtr<dom::Selection> domSelection = caret->GetSelection();
848 if (domSelection) {
849 nsCOMPtr<nsIContent> content =
850 nsIContent::FromNodeOrNull(domSelection->GetFocusNode());
851 if (content &&
852 content->IsInclusiveDescendantOf(aBlockFrame->GetContent())) {
853 return false;
857 return true;
860 void TextOverflow::CreateMarkers(const nsLineBox* aLine, bool aCreateIStart,
861 bool aCreateIEnd,
862 const LogicalRect& aInsideMarkersArea,
863 const LogicalRect& aContentArea,
864 uint32_t aLineNumber) {
865 if (!mBlock->IsVisibleForPainting()) {
866 return;
869 if (aCreateIStart) {
870 DisplayListClipState::AutoSaveRestore clipState(mBuilder);
872 LogicalRect markerLogicalRect(
873 mBlockWM, aInsideMarkersArea.IStart(mBlockWM) - mIStart.mIntrinsicISize,
874 aLine->BStart(), mIStart.mIntrinsicISize, aLine->BSize());
875 nsPoint offset = mBuilder->ToReferenceFrame(mBlock);
876 nsRect markerRect =
877 markerLogicalRect.GetPhysicalRect(mBlockWM, mBlockSize) + offset;
878 ClipMarker(aContentArea.GetPhysicalRect(mBlockWM, mBlockSize) + offset,
879 markerRect, clipState);
881 mMarkerList.AppendNewToTopWithIndex<nsDisplayTextOverflowMarker>(
882 mBuilder, mBlock, /* aIndex = */ (aLineNumber << 1) + 0, markerRect,
883 aLine->GetLogicalAscent(), *mIStart.mStyle);
886 if (aCreateIEnd) {
887 DisplayListClipState::AutoSaveRestore clipState(mBuilder);
889 LogicalRect markerLogicalRect(mBlockWM, aInsideMarkersArea.IEnd(mBlockWM),
890 aLine->BStart(), mIEnd.mIntrinsicISize,
891 aLine->BSize());
892 nsPoint offset = mBuilder->ToReferenceFrame(mBlock);
893 nsRect markerRect =
894 markerLogicalRect.GetPhysicalRect(mBlockWM, mBlockSize) + offset;
895 ClipMarker(aContentArea.GetPhysicalRect(mBlockWM, mBlockSize) + offset,
896 markerRect, clipState);
898 mMarkerList.AppendNewToTopWithIndex<nsDisplayTextOverflowMarker>(
899 mBuilder, mBlock, /* aIndex = */ (aLineNumber << 1) + 1, markerRect,
900 aLine->GetLogicalAscent(),
901 mIEnd.mHasBlockEllipsis ? StyleTextOverflowSide::Ellipsis()
902 : *mIEnd.mStyle);
906 void TextOverflow::Marker::SetupString(nsIFrame* aFrame) {
907 if (mInitialized) {
908 return;
911 // A limitation here is that at the IEnd of a line, we only ever render one of
912 // a text-overflow marker and a -webkit-line-clamp block ellipsis. Since we
913 // don't track the block ellipsis string and the text-overflow marker string
914 // separately, if both apply to the element, we will always use "…" as the
915 // string for text-overflow.
916 if (HasBlockEllipsis(aFrame) || mStyle->IsEllipsis()) {
917 gfxTextRun* textRun = GetEllipsisTextRun(aFrame);
918 if (textRun) {
919 mISize = textRun->GetAdvanceWidth();
920 } else {
921 mISize = 0;
923 } else {
924 UniquePtr<gfxContext> rc =
925 aFrame->PresShell()->CreateReferenceRenderingContext();
926 RefPtr<nsFontMetrics> fm =
927 nsLayoutUtils::GetInflatedFontMetricsForFrame(aFrame);
928 mISize = nsLayoutUtils::AppUnitWidthOfStringBidi(
929 NS_ConvertUTF8toUTF16(mStyle->AsString().AsString()), aFrame, *fm, *rc);
931 mIntrinsicISize = mISize;
932 mInitialized = true;
935 } // namespace css
936 } // namespace mozilla