diff --git a/layout/base/tests/marionette/test_accessiblecaret_cursor_mode.py b/layout/base/tests/marionette/test_accessiblecaret_cursor_mode.py index a134b78f73b9..bf45caf0b676 100644 --- a/layout/base/tests/marionette/test_accessiblecaret_cursor_mode.py +++ b/layout/base/tests/marionette/test_accessiblecaret_cursor_mode.py @@ -224,8 +224,8 @@ class AccessibleCaretCursorModeTestCase(MarionetteTestCase): self.open_test_html("layout/test_carets_columns.html") el = self.marionette.find_element(By.ID, "columns-inner") sel = SelectionManager(el) + original_content = sel.content content_to_add = "!" - target_content = sel.content + content_to_add # Goal: the cursor position can be changed by dragging the caret from # the front to the end of the content. @@ -244,7 +244,16 @@ class AccessibleCaretCursorModeTestCase(MarionetteTestCase): self.actions.flick(el, src_x, src_y, dest_x, dest_y).perform() self.actions.send_keys(content_to_add).perform() - self.assertEqual(target_content, sel.content) + # Depending on where the column break ends up we might see an empty + # block, which adds a newline to the selection serialization. + # That's not related to the point of the test-case. + self.assertIn( + sel.content, + [ + original_content + content_to_add, + original_content + "\n" + content_to_add, + ], + ) def test_move_cursor_to_front_by_dragging_caret_to_front_br_element(self): self.open_test_html(self._cursor_html) diff --git a/layout/generic/nsBlockFrame.cpp b/layout/generic/nsBlockFrame.cpp index 15d43ee9ce5c..ef0f03821061 100644 --- a/layout/generic/nsBlockFrame.cpp +++ b/layout/generic/nsBlockFrame.cpp @@ -661,28 +661,18 @@ Maybe nsBlockFrame::GetNaturalBaselineBOffset( } nscoord nsBlockFrame::GetCaretBaseline() const { - nsRect contentRect = GetContentRect(); - nsMargin bp = GetUsedBorderAndPadding(); - + const auto wm = GetWritingMode(); if (!mLines.empty()) { ConstLineIterator line = LinesBegin(); if (!line->IsEmpty()) { if (line->IsBlock()) { - return bp.top + line->mFirstChild->GetCaretBaseline(); + return GetLogicalUsedBorderAndPadding(wm).BStart(wm) + + line->mFirstChild->GetCaretBaseline(); } return line->BStart() + line->GetLogicalAscent(); } } - - float inflation = nsLayoutUtils::FontSizeInflationFor(this); - RefPtr fm = - nsLayoutUtils::GetFontMetricsForFrame(this, inflation); - nscoord lineHeight = ReflowInput::CalcLineHeight( - *Style(), PresContext(), GetContent(), contentRect.height, inflation); - const WritingMode wm = GetWritingMode(); - return nsLayoutUtils::GetCenteredFontBaseline(fm, lineHeight, - wm.IsLineInverted()) + - bp.top; + return GetFontMetricsDerivedCaretBaseline(ContentBSize(wm)); } ///////////////////////////////////////////////////////////////////////////// @@ -2997,8 +2987,8 @@ static void DumpLine(const BlockReflowState& aState, nsLineBox* aLine, #endif } -static bool LinesAreEmpty(const nsLineList& aList) { - for (const auto& line : aList) { +bool nsBlockFrame::LinesAreEmpty() const { + for (const auto& line : mLines) { if (!line.IsEmpty()) { return false; } @@ -3718,7 +3708,7 @@ bool nsBlockFrame::ReflowDirtyLines(BlockReflowState& aState) { } } - if (LinesAreEmpty(mLines) && ShouldHaveLineIfEmpty()) { + if (LinesAreEmpty() && ShouldHaveLineIfEmpty()) { aState.mBCoord += aState.mMinLineHeight; } @@ -4103,7 +4093,7 @@ bool nsBlockFrame::IsEmpty() { return false; } - return LinesAreEmpty(mLines); + return LinesAreEmpty(); } bool nsBlockFrame::ShouldApplyBStartMargin(BlockReflowState& aState, diff --git a/layout/generic/nsBlockFrame.h b/layout/generic/nsBlockFrame.h index e7d530e95b76..b459dd1aea66 100644 --- a/layout/generic/nsBlockFrame.h +++ b/layout/generic/nsBlockFrame.h @@ -230,6 +230,7 @@ class nsBlockFrame : public nsContainerFrame { bool IsEmpty() override; bool CachedIsEmpty() override; bool IsSelfEmpty() override; + bool LinesAreEmpty() const; // Given that we have a ::marker frame, does it actually draw something, i.e., // do we have either a 'list-style-type' or 'list-style-image' that is diff --git a/layout/generic/nsIFrame.cpp b/layout/generic/nsIFrame.cpp index 96c81dc19a8d..7327c04b6c0e 100644 --- a/layout/generic/nsIFrame.cpp +++ b/layout/generic/nsIFrame.cpp @@ -325,6 +325,16 @@ bool nsIFrame::CheckAndClearPaintedState() { return result; } +nsIFrame* nsIFrame::FindLineContainer() const { + MOZ_ASSERT(IsLineParticipant()); + nsIFrame* parent = GetParent(); + while (parent && + (parent->IsLineParticipant() || parent->CanContinueTextRun())) { + parent = parent->GetParent(); + } + return parent; +} + bool nsIFrame::CheckAndClearDisplayListState() { bool result = BuiltDisplayList(); SetBuiltDisplayList(false); @@ -2134,6 +2144,18 @@ nsIFrame::CaretBlockAxisMetrics nsIFrame::GetCaretBlockAxisMetrics( return CaretBlockAxisMetrics{.mOffset = baseline - ascent, .mExtent = height}; } +nscoord nsIFrame::GetFontMetricsDerivedCaretBaseline(nscoord aBSize) const { + float inflation = nsLayoutUtils::FontSizeInflationFor(this); + RefPtr fm = + nsLayoutUtils::GetFontMetricsForFrame(this, inflation); + const WritingMode wm = GetWritingMode(); + nscoord lineHeight = ReflowInput::CalcLineHeight( + *Style(), PresContext(), GetContent(), aBSize, inflation); + return nsLayoutUtils::GetCenteredFontBaseline(fm, lineHeight, + wm.IsLineInverted()) + + GetLogicalUsedBorderAndPadding(wm).BStart(wm); +} + const nsAtom* nsIFrame::ComputePageValue(const nsAtom* aAutoValue) const { const nsAtom* value = aAutoValue ? aAutoValue : nsGkAtoms::_empty; const nsIFrame* frame = this; diff --git a/layout/generic/nsIFrame.h b/layout/generic/nsIFrame.h index 0c087141f3e9..cd2235cd202a 100644 --- a/layout/generic/nsIFrame.h +++ b/layout/generic/nsIFrame.h @@ -1798,6 +1798,17 @@ class nsIFrame : public nsQueryFrame { return GetLogicalBaseline(GetWritingMode()); } + // Gets a caret baseline suitable for the frame if the frame doesn't have one. + // + // @param aBSize the content box block size of the line container. Needed to + // resolve line-height: -moz-block-height. NS_UNCONSTRAINEDSIZE is fine + // otherwise. + // + // TODO(emilio): Now we support align-content on blocks it seems we could + // get rid of line-height: -moz-block-height. + nscoord GetFontMetricsDerivedCaretBaseline( + nscoord aBSize = NS_UNCONSTRAINEDSIZE) const; + /** * Subclasses can call this method to enable visibility tracking for this * frame. @@ -2612,6 +2623,11 @@ class nsIFrame : public nsQueryFrame { */ virtual nsIFrame* LastInFlow() const { return const_cast(this); } + /** + * Useful for line participants. Find the line container frame for this line. + */ + nsIFrame* FindLineContainer() const; + /** * Mark any stored intrinsic inline size information as dirty (requiring * re-calculation). Note that this should generally not be called diff --git a/layout/generic/nsInlineFrame.cpp b/layout/generic/nsInlineFrame.cpp index ede4199d658d..856df6e0acc0 100644 --- a/layout/generic/nsInlineFrame.cpp +++ b/layout/generic/nsInlineFrame.cpp @@ -154,6 +154,21 @@ bool nsInlineFrame::IsEmpty() { return true; } +nscoord nsInlineFrame::GetCaretBaseline() const { + if (mBaseline == 0 && mRect.IsEmpty()) { + nsBlockFrame* container = do_QueryFrame(FindLineContainer()); + // TODO(emilio): Ideally we'd want to find out if only our line is empty, + // but that's non-trivial to do, and realistically empty inlines and text + // will get placed into a non-empty line unless all lines are empty, I + // believe... + if (container && container->LinesAreEmpty()) { + nscoord blockSize = container->ContentBSize(GetWritingMode()); + return GetFontMetricsDerivedCaretBaseline(blockSize); + } + } + return nsIFrame::GetCaretBaseline(); +} + nsIFrame::FrameSearchResult nsInlineFrame::PeekOffsetCharacter( bool aForward, int32_t* aOffset, PeekOffsetCharacterOptions aOptions) { // Override the implementation in nsFrame, to skip empty inline frames diff --git a/layout/generic/nsInlineFrame.h b/layout/generic/nsInlineFrame.h index e86dcf371d63..832eb11744aa 100644 --- a/layout/generic/nsInlineFrame.h +++ b/layout/generic/nsInlineFrame.h @@ -52,6 +52,8 @@ class nsInlineFrame : public nsContainerFrame { bool IsEmpty() override; bool IsSelfEmpty() override; + nscoord GetCaretBaseline() const override; + FrameSearchResult PeekOffsetCharacter( bool aForward, int32_t* aOffset, PeekOffsetCharacterOptions aOptions = diff --git a/layout/generic/nsTextFrame.cpp b/layout/generic/nsTextFrame.cpp index 37da630dc6d9..d477df7443b1 100644 --- a/layout/generic/nsTextFrame.cpp +++ b/layout/generic/nsTextFrame.cpp @@ -1140,7 +1140,7 @@ class BuildTextRunsScanner { uint8_t mCurrentRunContextInfo; }; -static nsIFrame* FindLineContainer(nsIFrame* aFrame) { +static const nsIFrame* FindLineContainer(const nsIFrame* aFrame) { while (aFrame && (aFrame->IsLineParticipant() || aFrame->CanContinueTextRun())) { aFrame = aFrame->GetParent(); @@ -1148,6 +1148,11 @@ static nsIFrame* FindLineContainer(nsIFrame* aFrame) { return aFrame; } +static nsIFrame* FindLineContainer(nsIFrame* aFrame) { + return const_cast( + FindLineContainer(const_cast(aFrame))); +} + static bool IsLineBreakingWhiteSpace(char16_t aChar) { // 0x0A (\n) is not handled as white-space by the line breaker, since // we break before it, if it isn't transformed to a normal space. @@ -8915,7 +8920,7 @@ void nsTextFrame::AddInlineMinISize(const IntrinsicSizeInput& aInput, if (f == this || f->GetTextRun(trtype) != lastTextRun) { nsIFrame* lc; if (aData->LineContainer() && - aData->LineContainer() != (lc = FindLineContainer(f))) { + aData->LineContainer() != (lc = f->FindLineContainer())) { NS_ASSERTION(f != this, "wrong InlineMinISizeData container" " for first continuation"); @@ -9075,7 +9080,7 @@ void nsTextFrame::AddInlinePrefISize(const IntrinsicSizeInput& aInput, if (f == this || f->GetTextRun(trtype) != lastTextRun) { nsIFrame* lc; if (aData->LineContainer() && - aData->LineContainer() != (lc = FindLineContainer(f))) { + aData->LineContainer() != (lc = f->FindLineContainer())) { NS_ASSERTION(f != this, "wrong InlinePrefISizeData container" " for first continuation"); @@ -10354,7 +10359,7 @@ bool nsTextFrame::AppendRenderedText(AppendRenderedTextState& aState, bool startsAtHardBreak, endsAtHardBreak; if (!HasAnyStateBits(TEXT_START_OF_LINE | TEXT_END_OF_LINE)) { startsAtHardBreak = endsAtHardBreak = false; - } else if (nsBlockFrame* thisLc = do_QueryFrame(FindLineContainer(this))) { + } else if (nsBlockFrame* thisLc = do_QueryFrame(FindLineContainer())) { if (thisLc != aState.mLineContainer) { // Setup line cursor when needed. aState.mLineContainer = thisLc; @@ -10536,11 +10541,8 @@ bool nsTextFrame::IsEmpty() { if (textStyle->WhiteSpaceIsSignificant()) { // When WhiteSpaceIsSignificant styles are in effect, we only treat the // frame as empty if its content really is entirely *empty* (not just - // whitespace), AND it is NOT editable or within an element. - // In these cases we consider that the whitespace-preserving style makes - // the frame behave as non-empty so that its height doesn't become zero. - return GetContentLength() == 0 && !GetContent()->IsEditable() && - !GetContent()->GetParent()->IsHTMLElement(nsGkAtoms::input); + // whitespace). + return !GetContentLength(); } if (HasAnyStateBits(TEXT_ISNOT_ONLY_WHITESPACE)) { @@ -10696,6 +10698,21 @@ Maybe nsTextFrame::GetNaturalBaselineBOffset( return Some(parentAscent - (aWM.IsVertical() ? position.x : position.y)); } +nscoord nsTextFrame::GetCaretBaseline() const { + if (mAscent == 0 && HasAnyStateBits(TEXT_NO_RENDERED_GLYPHS)) { + nsBlockFrame* container = do_QueryFrame(FindLineContainer()); + // TODO(emilio): Ideally we'd want to find out if only our line is empty, + // but that's non-trivial to do, and realistically empty inlines and text + // will get placed into a non-empty line unless all lines are empty, I + // believe... + if (container && container->LinesAreEmpty()) { + nscoord blockSize = container->ContentBSize(GetWritingMode()); + return GetFontMetricsDerivedCaretBaseline(blockSize); + } + } + return nsIFrame::GetCaretBaseline(); +} + bool nsTextFrame::HasAnyNoncollapsedCharacters() { gfxSkipCharsIterator iter = EnsureTextRun(nsTextFrame::eInflated); int32_t offset = GetContentOffset(), offsetEnd = GetContentEnd(); diff --git a/layout/generic/nsTextFrame.h b/layout/generic/nsTextFrame.h index 6f513e75f3a6..f760862b9443 100644 --- a/layout/generic/nsTextFrame.h +++ b/layout/generic/nsTextFrame.h @@ -365,6 +365,7 @@ class nsTextFrame : public nsIFrame { Maybe GetNaturalBaselineBOffset( mozilla::WritingMode aWM, BaselineSharingGroup aBaselineGroup, BaselineExportContext) const override; + nscoord GetCaretBaseline() const override; bool HasSignificantTerminalNewline() const final; diff --git a/layout/reftests/css-ui/caret-empty-contenteditable-01.html b/layout/reftests/css-ui/caret-empty-contenteditable-01.html new file mode 100644 index 000000000000..86c54fab02a0 --- /dev/null +++ b/layout/reftests/css-ui/caret-empty-contenteditable-01.html @@ -0,0 +1,13 @@ + + +
diff --git a/layout/reftests/css-ui/caret-empty-contenteditable-02.html b/layout/reftests/css-ui/caret-empty-contenteditable-02.html new file mode 100644 index 000000000000..00f83a154077 --- /dev/null +++ b/layout/reftests/css-ui/caret-empty-contenteditable-02.html @@ -0,0 +1,13 @@ + + +
diff --git a/layout/reftests/css-ui/caret-empty-contenteditable-03.html b/layout/reftests/css-ui/caret-empty-contenteditable-03.html new file mode 100644 index 000000000000..86ade59dd3b0 --- /dev/null +++ b/layout/reftests/css-ui/caret-empty-contenteditable-03.html @@ -0,0 +1,13 @@ + + +

diff --git a/layout/reftests/css-ui/caret-empty-contenteditable-ref.html b/layout/reftests/css-ui/caret-empty-contenteditable-ref.html new file mode 100644 index 000000000000..7c6a81947fbf --- /dev/null +++ b/layout/reftests/css-ui/caret-empty-contenteditable-ref.html @@ -0,0 +1,13 @@ + + +
x
diff --git a/layout/reftests/css-ui/caret-empty-input-01.html b/layout/reftests/css-ui/caret-empty-input-01.html new file mode 100644 index 000000000000..9ce0ad2985c5 --- /dev/null +++ b/layout/reftests/css-ui/caret-empty-input-01.html @@ -0,0 +1,13 @@ + + + diff --git a/layout/reftests/css-ui/caret-empty-input-ref.html b/layout/reftests/css-ui/caret-empty-input-ref.html new file mode 100644 index 000000000000..75fc6a2c472e --- /dev/null +++ b/layout/reftests/css-ui/caret-empty-input-ref.html @@ -0,0 +1,13 @@ + + + diff --git a/layout/reftests/css-ui/reftest.list b/layout/reftests/css-ui/reftest.list index ac3ef57920c2..6fd3a5d2df14 100644 --- a/layout/reftests/css-ui/reftest.list +++ b/layout/reftests/css-ui/reftest.list @@ -1 +1,13 @@ -fails-if(useDrawSnapshot) pref(ui.caretWidth,16) needs-focus == caret-color-01.html caret-color-01-ref.html +defaults pref(ui.caretWidth,16) + +# drawSnapshot doesn't draw carets +fails-if(useDrawSnapshot) needs-focus == caret-color-01.html caret-color-01-ref.html + +fails-if(useDrawSnapshot) needs-focus != caret-empty-contenteditable-ref.html about:blank + +needs-focus == caret-empty-contenteditable-01.html caret-empty-contenteditable-ref.html +random needs-focus == caret-empty-contenteditable-02.html caret-empty-contenteditable-ref.html # Dependency on font metrics that needs to be sorted out. +needs-focus == caret-empty-contenteditable-03.html caret-empty-contenteditable-ref.html + +fails-if(useDrawSnapshot) needs-focus != caret-empty-input-ref.html about:blank +needs-focus == caret-empty-input-01.html caret-empty-input-ref.html