fune/layout/generic/nsTextFrameUtils.cpp
Emilio Cobos Álvarez b651bfe99a Bug 1744009 - Simplify combobox <select> code. r=mconley,dholbert
With this patch on its own we get some a11y tests failures, but those
are fixed on a later patch.

Combobox select no longer creates frames for its <options>, nor an
nsListControlFrame. Instead, it computes its right intrinsic size using
the largest size of the options. This is better, because we render the
option text using the select style so if the select and option styles
are mismatched it'd cause changes in the size of the select when text
changes. See the following in a build without the patch, for example:

  <select>
    <option>ABC</option>
    <option style="font-size: 1px">Something long</option>
  </select>

This seems like a rather obscure case, but it's important to get it
right, see bug 1741888.

With this patch we use the same setup in content and parent processes
(this needs bug 1596852 and bug 1744152). This means we can remove a
bunch of the native view and popup code in nsListControlFrame. A couple
browser_* tests are affected by this change and have been tweaked
appropriately (the changes there are trivial).

Not creating an nsListControlFrame for dropdown select means that we
need to move a bunch of the event handling code from nsListControlFrame
to a common place that nsComboboxControlFrame can also use. That place
is HTMLSelectEventListener, and I think the setup is much nicer than
having the code intertwined with nsListControlFrame. It should be
relatively straight-forward to review, mostly moving code from one part
to another.

Another thing that we need to do in HTMLSelectEventListener that we
didn't use to do is listening for DOM mutations on the dropdown. Before,
we were relying on changes like text mutations triggering a reflow of
the listcontrolframe, which also triggered a reflow of the
comboboxcontrolframe, which in turn updated the text of the anonymous
content. Now we need to trigger that reflow manually.

There are some further simplifications that can be done after this
lands (cleanup naming of openInParentProcess and so on, among others),
but I'd rather land this first (after the merge of course) and work on
them separately.

Differential Revision: https://phabricator.services.mozilla.com/D132719
2022-01-17 11:10:05 +00:00

408 lines
14 KiB
C++

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "nsTextFrameUtils.h"
#include "mozilla/dom/Text.h"
#include "nsBidiUtils.h"
#include "nsCharTraits.h"
#include "nsIContent.h"
#include "nsStyleStruct.h"
#include "nsTextFragment.h"
#include "nsUnicharUtils.h"
#include "nsUnicodeProperties.h"
#include <algorithm>
using namespace mozilla;
using namespace mozilla::dom;
// static
bool nsTextFrameUtils::IsSpaceCombiningSequenceTail(const char16_t* aChars,
int32_t aLength) {
return aLength > 0 &&
(mozilla::unicode::IsClusterExtender(aChars[0]) ||
(IsBidiControl(aChars[0]) &&
IsSpaceCombiningSequenceTail(aChars + 1, aLength - 1)));
}
static bool IsDiscardable(char16_t ch, nsTextFrameUtils::Flags* aFlags) {
// Unlike IS_DISCARDABLE, we don't discard \r. \r will be ignored by
// gfxTextRun and discarding it would force us to copy text in many cases of
// preformatted text containing \r\n.
if (ch == CH_SHY) {
*aFlags |= nsTextFrameUtils::Flags::HasShy;
return true;
}
return IsBidiControl(ch);
}
static bool IsDiscardable(uint8_t ch, nsTextFrameUtils::Flags* aFlags) {
if (ch == CH_SHY) {
*aFlags |= nsTextFrameUtils::Flags::HasShy;
return true;
}
return false;
}
static bool IsSegmentBreak(char16_t aCh) { return aCh == '\n'; }
static bool IsSpaceOrTab(char16_t aCh) { return aCh == ' ' || aCh == '\t'; }
static bool IsSpaceOrTabOrSegmentBreak(char16_t aCh) {
return IsSpaceOrTab(aCh) || IsSegmentBreak(aCh);
}
template <typename CharT>
/* static */
bool nsTextFrameUtils::IsSkippableCharacterForTransformText(CharT aChar) {
return aChar == ' ' || aChar == '\t' || aChar == '\n' || aChar == CH_SHY ||
(aChar > 0xFF && IsBidiControl(aChar));
}
#ifdef DEBUG
template <typename CharT>
static void AssertSkippedExpectedChars(const CharT* aText,
const gfxSkipChars& aSkipChars,
int32_t aSkipCharsOffset) {
gfxSkipCharsIterator it(aSkipChars);
it.AdvanceOriginal(aSkipCharsOffset);
while (it.GetOriginalOffset() < it.GetOriginalEnd()) {
CharT ch = aText[it.GetOriginalOffset() - aSkipCharsOffset];
MOZ_ASSERT(!it.IsOriginalCharSkipped() ||
nsTextFrameUtils::IsSkippableCharacterForTransformText(ch),
"skipped unexpected character; need to update "
"IsSkippableCharacterForTransformText?");
it.AdvanceOriginal(1);
}
}
#endif
template <class CharT>
static CharT* TransformWhiteSpaces(
const CharT* aText, uint32_t aLength, uint32_t aBegin, uint32_t aEnd,
bool aHasSegmentBreak, bool& aInWhitespace, CharT* aOutput,
nsTextFrameUtils::Flags& aFlags,
nsTextFrameUtils::CompressionMode aCompression, gfxSkipChars* aSkipChars) {
MOZ_ASSERT(aCompression == nsTextFrameUtils::COMPRESS_WHITESPACE ||
aCompression == nsTextFrameUtils::COMPRESS_WHITESPACE_NEWLINE,
"whitespaces should be skippable!!");
// Get the context preceding/following this white space range.
// For 8-bit text (sizeof CharT == 1), the checks here should get optimized
// out, and isSegmentBreakSkippable should be initialized to be 'false'.
bool isSegmentBreakSkippable =
sizeof(CharT) > 1 &&
((aBegin > 0 && IS_ZERO_WIDTH_SPACE(aText[aBegin - 1])) ||
(aEnd < aLength && IS_ZERO_WIDTH_SPACE(aText[aEnd])));
if (sizeof(CharT) > 1 && !isSegmentBreakSkippable && aBegin > 0 &&
aEnd < aLength) {
uint32_t ucs4before;
uint32_t ucs4after;
if (aBegin > 1 &&
NS_IS_SURROGATE_PAIR(aText[aBegin - 2], aText[aBegin - 1])) {
ucs4before = SURROGATE_TO_UCS4(aText[aBegin - 2], aText[aBegin - 1]);
} else {
ucs4before = aText[aBegin - 1];
}
if (aEnd + 1 < aLength &&
NS_IS_SURROGATE_PAIR(aText[aEnd], aText[aEnd + 1])) {
ucs4after = SURROGATE_TO_UCS4(aText[aEnd], aText[aEnd + 1]);
} else {
ucs4after = aText[aEnd];
}
// Discard newlines between characters that have F, W, or H
// EastAsianWidth property and neither side is Hangul.
isSegmentBreakSkippable =
IsSegmentBreakSkipChar(ucs4before) && IsSegmentBreakSkipChar(ucs4after);
}
for (uint32_t i = aBegin; i < aEnd; ++i) {
CharT ch = aText[i];
bool keepChar = false;
bool keepTransformedWhiteSpace = false;
if (IsDiscardable(ch, &aFlags)) {
aSkipChars->SkipChar();
continue;
}
if (IsSpaceOrTab(ch)) {
if (aHasSegmentBreak) {
// If white-space is set to normal, nowrap, or pre-line, white space
// characters are considered collapsible and all spaces and tabs
// immediately preceding or following a segment break are removed.
aSkipChars->SkipChar();
continue;
}
if (aInWhitespace) {
aSkipChars->SkipChar();
continue;
} else {
keepTransformedWhiteSpace = true;
}
} else {
// Apply Segment Break Transformation Rules (CSS Text 3 - 4.1.2) for
// segment break characters.
if (aCompression == nsTextFrameUtils::COMPRESS_WHITESPACE ||
// XXX: According to CSS Text 3, a lone CR should not always be
// kept, but still go through the Segment Break Transformation
// Rules. However, this is what current modern browser engines
// (webkit/blink/edge) do. So, once we can get some clarity
// from the specification issue, we should either remove the
// lone CR condition here, or leave it here with this comment
// being rephrased.
// Please see https://github.com/w3c/csswg-drafts/issues/855.
ch == '\r') {
keepChar = true;
} else {
// aCompression == COMPRESS_WHITESPACE_NEWLINE
// Any collapsible segment break immediately following another
// collapsible segment break is removed. Then the remaining segment
// break is either transformed into a space (U+0020) or removed
// depending on the context before and after the break.
if (isSegmentBreakSkippable || aInWhitespace) {
aSkipChars->SkipChar();
continue;
}
isSegmentBreakSkippable = true;
keepTransformedWhiteSpace = true;
}
}
if (keepChar) {
*aOutput++ = ch;
aSkipChars->KeepChar();
aInWhitespace = IsSpaceOrTab(ch);
} else if (keepTransformedWhiteSpace) {
*aOutput++ = ' ';
aSkipChars->KeepChar();
aInWhitespace = true;
} else {
MOZ_ASSERT_UNREACHABLE("Should've skipped the character!!");
}
}
return aOutput;
}
template <class CharT>
CharT* nsTextFrameUtils::TransformText(const CharT* aText, uint32_t aLength,
CharT* aOutput,
CompressionMode aCompression,
uint8_t* aIncomingFlags,
gfxSkipChars* aSkipChars,
Flags* aAnalysisFlags) {
Flags flags = Flags();
#ifdef DEBUG
int32_t skipCharsOffset = aSkipChars->GetOriginalCharCount();
#endif
bool lastCharArabic = false;
if (aCompression == COMPRESS_NONE ||
aCompression == COMPRESS_NONE_TRANSFORM_TO_SPACE) {
// Skip discardables.
uint32_t i;
for (i = 0; i < aLength; ++i) {
CharT ch = aText[i];
if (IsDiscardable(ch, &flags)) {
aSkipChars->SkipChar();
} else {
aSkipChars->KeepChar();
if (ch > ' ') {
lastCharArabic = IS_ARABIC_CHAR(ch);
} else if (aCompression == COMPRESS_NONE_TRANSFORM_TO_SPACE) {
if (ch == '\t' || ch == '\n') {
ch = ' ';
}
} else {
// aCompression == COMPRESS_NONE
if (ch == '\t') {
flags |= Flags::HasTab;
}
}
*aOutput++ = ch;
}
}
if (lastCharArabic) {
*aIncomingFlags |= INCOMING_ARABICCHAR;
} else {
*aIncomingFlags &= ~INCOMING_ARABICCHAR;
}
*aIncomingFlags &= ~INCOMING_WHITESPACE;
} else {
bool inWhitespace = (*aIncomingFlags & INCOMING_WHITESPACE) != 0;
uint32_t i;
for (i = 0; i < aLength; ++i) {
CharT ch = aText[i];
// CSS Text 3 - 4.1. The White Space Processing Rules
// White space processing in CSS affects only the document white space
// characters: spaces (U+0020), tabs (U+0009), and segment breaks.
// Since we need the context of segment breaks and their surrounding
// white spaces to proceed the white space processing, a consecutive run
// of spaces/tabs/segment breaks is collected in a first pass loop, then
// we apply the collapsing and transformation rules to this run in a
// second pass loop.
if (IsSpaceOrTabOrSegmentBreak(ch)) {
bool keepLastSpace = false;
bool hasSegmentBreak = IsSegmentBreak(ch);
uint32_t countTrailingDiscardables = 0;
uint32_t j;
for (j = i + 1; j < aLength && (IsSpaceOrTabOrSegmentBreak(aText[j]) ||
IsDiscardable(aText[j], &flags));
j++) {
if (IsSegmentBreak(aText[j])) {
hasSegmentBreak = true;
}
}
// Exclude trailing discardables before checking space combining
// sequence tail.
for (; IsDiscardable(aText[j - 1], &flags); j--) {
countTrailingDiscardables++;
}
// If the last white space is followed by a combining sequence tail,
// exclude it from the range of TransformWhiteSpaces.
if (sizeof(CharT) > 1 && aText[j - 1] == ' ' && j < aLength &&
IsSpaceCombiningSequenceTail(&aText[j], aLength - j)) {
keepLastSpace = true;
j--;
}
if (j > i) {
aOutput = TransformWhiteSpaces(aText, aLength, i, j, hasSegmentBreak,
inWhitespace, aOutput, flags,
aCompression, aSkipChars);
}
// We need to keep KeepChar()/SkipChar() in order, so process the
// last white space first, then process the trailing discardables.
if (keepLastSpace) {
keepLastSpace = false;
*aOutput++ = ' ';
aSkipChars->KeepChar();
lastCharArabic = false;
j++;
}
for (; countTrailingDiscardables > 0; countTrailingDiscardables--) {
aSkipChars->SkipChar();
j++;
}
i = j - 1;
continue;
}
// Process characters other than the document white space characters.
if (IsDiscardable(ch, &flags)) {
aSkipChars->SkipChar();
} else {
*aOutput++ = ch;
aSkipChars->KeepChar();
}
lastCharArabic = IS_ARABIC_CHAR(ch);
inWhitespace = false;
}
if (lastCharArabic) {
*aIncomingFlags |= INCOMING_ARABICCHAR;
} else {
*aIncomingFlags &= ~INCOMING_ARABICCHAR;
}
if (inWhitespace) {
*aIncomingFlags |= INCOMING_WHITESPACE;
} else {
*aIncomingFlags &= ~INCOMING_WHITESPACE;
}
}
*aAnalysisFlags = flags;
#ifdef DEBUG
AssertSkippedExpectedChars(aText, *aSkipChars, skipCharsOffset);
#endif
return aOutput;
}
/*
* NOTE: The TransformText and IsSkippableCharacterForTransformText template
* functions are part of the public API of nsTextFrameUtils, while
* their function bodies are not available in the header. They may stop working
* (fail to resolve symbol in link time) once their callsites are moved to a
* different translation unit (e.g. a different unified source file).
* Explicit instantiating this function template with `uint8_t` and `char16_t`
* could prevent us from the potential risk.
*/
template uint8_t* nsTextFrameUtils::TransformText(
const uint8_t* aText, uint32_t aLength, uint8_t* aOutput,
CompressionMode aCompression, uint8_t* aIncomingFlags,
gfxSkipChars* aSkipChars, Flags* aAnalysisFlags);
template char16_t* nsTextFrameUtils::TransformText(
const char16_t* aText, uint32_t aLength, char16_t* aOutput,
CompressionMode aCompression, uint8_t* aIncomingFlags,
gfxSkipChars* aSkipChars, Flags* aAnalysisFlags);
template bool nsTextFrameUtils::IsSkippableCharacterForTransformText(
uint8_t aChar);
template bool nsTextFrameUtils::IsSkippableCharacterForTransformText(
char16_t aChar);
template <typename CharT>
static uint32_t DoComputeApproximateLengthWithWhitespaceCompression(
const CharT* aChars, uint32_t aLength, const nsStyleText* aStyleText) {
// This is an approximation so we don't really need anything
// too fancy here.
uint32_t len;
if (aStyleText->WhiteSpaceIsSignificant()) {
return aLength;
}
bool prevWS = true; // more important to ignore blocks with
// only whitespace than get inline boundaries
// exactly right
len = 0;
for (uint32_t i = 0; i < aLength; ++i) {
CharT c = aChars[i];
if (c == ' ' || c == '\n' || c == '\t' || c == '\r') {
if (!prevWS) {
++len;
}
prevWS = true;
} else {
++len;
prevWS = false;
}
}
return len;
}
uint32_t nsTextFrameUtils::ComputeApproximateLengthWithWhitespaceCompression(
Text* aText, const nsStyleText* aStyleText) {
const nsTextFragment* frag = &aText->TextFragment();
if (frag->Is2b()) {
return DoComputeApproximateLengthWithWhitespaceCompression(
frag->Get2b(), frag->GetLength(), aStyleText);
}
return DoComputeApproximateLengthWithWhitespaceCompression(
frag->Get1b(), frag->GetLength(), aStyleText);
}
uint32_t nsTextFrameUtils::ComputeApproximateLengthWithWhitespaceCompression(
const nsAString& aString, const nsStyleText* aStyleText) {
return DoComputeApproximateLengthWithWhitespaceCompression(
aString.BeginReading(), aString.Length(), aStyleText);
}
bool nsSkipCharsRunIterator::NextRun() {
do {
if (mRunLength) {
mIterator.AdvanceOriginal(mRunLength);
NS_ASSERTION(mRunLength > 0,
"No characters in run (initial length too large?)");
if (!mSkipped || mLengthIncludesSkipped) {
mRemainingLength -= mRunLength;
}
}
if (!mRemainingLength) {
return false;
}
int32_t length;
mSkipped = mIterator.IsOriginalCharSkipped(&length);
mRunLength = std::min(length, mRemainingLength);
} while (!mVisitSkipped && mSkipped);
return true;
}