blob: a4cab5760fca6744a5633be135f880d645b482a6 [file] [log] [blame]
/*
* Copyright (C) 2023 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "AXTextRun.h"
#include "Logging.h"
#if ENABLE(AX_THREAD_TEXT_APIS)
#include <wtf/text/MakeString.h>
namespace WebCore {
String AXTextRuns::debugDescription() const
{
StringBuilder builder;
builder.append('[');
for (size_t i = 0; i < runs.size(); i++) {
AXTextRunLineID lineID = { containingBlock, runs[i].lineIndex };
builder.append(makeString(
lineID.debugDescription(),
": |"_s, makeStringByReplacingAll(runString(i), '\n', "{newline}"_s),
"|(len "_s, runs[i].length(), ")"_s
));
if (i != runs.size() - 1)
builder.append(", "_s);
}
builder.append(']');
return builder.toString();
}
size_t AXTextRuns::indexForOffset(unsigned textOffset, Affinity affinity) const
{
size_t cumulativeLength = 0;
for (size_t i = 0; i < runs.size(); i++) {
cumulativeLength += runLength(i);
if (cumulativeLength > textOffset) {
// The offset points into the middle of a run, which is never amibiguous.
return i;
}
if (cumulativeLength == textOffset) {
// The offset points to the end of a run, which could make this an ambiguous position
// when considering soft linebreaks.
if (affinity == Affinity::Downstream && i < lastRunIndex())
return i + 1;
return i;
}
}
return notFound;
}
unsigned AXTextRuns::runLengthSumTo(size_t index) const
{
unsigned length = 0;
for (size_t i = 0; i <= index && i < runs.size(); i++)
length += runLength(i);
return length;
}
unsigned AXTextRuns::domOffset(unsigned renderedTextOffset) const
{
unsigned cumulativeDomOffset = 0;
unsigned previousEndDomOffset = 0;
for (size_t i = 0; i < size(); i++) {
const auto& domOffsets = at(i).domOffsets();
for (const auto& domOffsetPair : domOffsets) {
ASSERT(domOffsetPair[0] >= previousEndDomOffset);
if (domOffsetPair[0] < previousEndDomOffset)
return renderedTextOffset;
// domOffsetPair[0] represents the start DOM offset of this run. Subtracting it
// from the previous run's end DOM offset, we know how much whitespace was collapsed,
// and thus know the offset between the DOM text and what was actually rendered.
// For example, given domOffsets: [2, 10], [13, 18]
// The first offset to rendered text is 2 (2 - 0), e.g. because of two leading
// whitespaces that were trimmed: " foo"
// The second offset to rendered text is 3 (13 - 10), e.g. because of three
// collapsed whitespaces in between the first and second runs.
cumulativeDomOffset += domOffsetPair[0] - previousEndDomOffset;
// Using the example above, these values would be 0 and 8 for the first run,
// and 8 and 13 for the second run. Text that would fits this example would be:
// " Charlie Delta", rendered as: "Charlie Delta".
unsigned startRenderedTextOffset = domOffsetPair[0] - cumulativeDomOffset;
unsigned endRenderedTextOffset = domOffsetPair[1] - cumulativeDomOffset;
if (renderedTextOffset >= startRenderedTextOffset && renderedTextOffset <= endRenderedTextOffset) {
// The rendered text offset is in range of this run. We can get the DOM offset
// by adding the accumulated difference between the rendered text and DOM text.
return renderedTextOffset + cumulativeDomOffset;
}
previousEndDomOffset = domOffsetPair[1];
}
}
// We were provided with a rendered-text offset that didn't actually fit into our
// runs. This should never happen.
ASSERT_NOT_REACHED();
return renderedTextOffset;
}
FloatRect AXTextRuns::localRect(unsigned start, unsigned end, FontOrientation orientation) const
{
unsigned smallerOffset = start;
unsigned largerOffset = end;
if (smallerOffset > largerOffset)
std::swap(smallerOffset, largerOffset);
// Hardcode Affinity::Downstream to avoid unnecessarily accounting for the end of the line above.
unsigned runIndexOfSmallerOffset = indexForOffset(smallerOffset, Affinity::Downstream);
unsigned runIndexOfLargerOffset = indexForOffset(largerOffset, Affinity::Downstream);
auto computeAdvance = [&] (const AXTextRun& run, unsigned offsetOfFirstCharacterInRun, unsigned startIndex, unsigned endIndex) {
const auto& characterAdvances = run.advances();
float totalAdvance = 0;
unsigned startIndexInRun = startIndex - offsetOfFirstCharacterInRun;
unsigned endIndexInRun = endIndex - offsetOfFirstCharacterInRun;
ASSERT(startIndexInRun <= endIndexInRun);
for (size_t i = startIndexInRun; i < endIndexInRun; i++)
totalAdvance += (float)characterAdvances[i];
return totalAdvance;
};
// FIXME: Probably want a special case for hard linebreaks (<br>s). Investigate how the main-thread does this.
// FIXME: We'll need to flip the result rect based on writing mode.
unsigned offsetFromOriginInDirection = 0;
unsigned maxWidthInDirection = 0;
float measuredHeightInDirection = 0.0f;
float heightBeforeRuns = 0.0f;
for (unsigned i = 0; i <= runIndexOfLargerOffset; i++) {
const auto& run = at(i);
if (i < runIndexOfSmallerOffset) {
// Each text run represents a line, so count up the height of lines prior to our range start.
heightBeforeRuns += run.lineHeight;
} else {
unsigned measuredWidthInDirection = 0;
if (i == runIndexOfSmallerOffset) {
unsigned offsetOfFirstCharacterInRun = !i ? 0 : runLengthSumTo(i - 1);
ASSERT(smallerOffset >= offsetOfFirstCharacterInRun);
if (smallerOffset < offsetOfFirstCharacterInRun)
smallerOffset = offsetOfFirstCharacterInRun;
// Measure the characters in this run (accomplished by smallerOffset - offsetOfFirstCharacterInRun)
// prior to the offset.
unsigned widthPriorToStart = 0;
if (smallerOffset - offsetOfFirstCharacterInRun > 0)
widthPriorToStart = computeAdvance(run, offsetOfFirstCharacterInRun, offsetOfFirstCharacterInRun, smallerOffset);
// If the larger offset goes beyond this line, use the end of the current line to for computing this run's bounds.
unsigned endOffsetInLine = runIndexOfSmallerOffset == runIndexOfLargerOffset
? largerOffset
: !i ? run.length() : runLengthSumTo(i - 1) + run.length();
if (endOffsetInLine - smallerOffset > 0)
measuredWidthInDirection = computeAdvance(run, offsetOfFirstCharacterInRun, smallerOffset, endOffsetInLine);
if (!measuredWidthInDirection) {
bool isCollapsedRange = (runIndexOfSmallerOffset == runIndexOfLargerOffset && smallerOffset == largerOffset);
if (isCollapsedRange) {
// If this is a collapsed range (start.offset == end.offset), we want to return the width of a cursor.
// Use 2px for this, matching CaretRectComputation::caretWidth. This overall behavior for collapsed
// ranges matches that of CaretRectComputation::computeLocalCaretRect, which is downstream of
// the main-thread-text-implementation equivalent of this function, AXObjectCache::boundsForRange.
measuredWidthInDirection = 2;
} else {
// There was no measured width in this run, so we should count this as a line before the actual rect starts.
heightBeforeRuns += run.lineHeight;
}
}
if (measuredWidthInDirection)
offsetFromOriginInDirection = widthPriorToStart + run.distanceFromBoundsInDirection;
} else if (i == runIndexOfLargerOffset) {
// We're measuring the end of the range, so measure from the first character in the run up to largerOffset.
unsigned offsetOfFirstCharacterInRun = !i ? 0 : runLengthSumTo(i - 1);
ASSERT(largerOffset >= offsetOfFirstCharacterInRun);
if (largerOffset < offsetOfFirstCharacterInRun)
largerOffset = offsetOfFirstCharacterInRun;
measuredWidthInDirection = computeAdvance(run, offsetOfFirstCharacterInRun, offsetOfFirstCharacterInRun, largerOffset);
if (measuredWidthInDirection) {
// If we have an offset from origin at this point, that means this range has wrapped from the previous line. We need
// to adjust the width to now encompass the whole line, since the origin will be shifted left to 0.
if (offsetFromOriginInDirection)
measuredWidthInDirection = offsetFromOriginInDirection + maxWidthInDirection;
// Because our rect now includes the beginning of a run, set |x| to be 0, indicating the rect is not
// offset from its container.
offsetFromOriginInDirection = 0;
}
} else {
// We're in some run between runIndexOfSmallerOffset and runIndexOfLargerOffset, so measure the whole run.
// For example, this could be the "bbb" runs:
// a|aa
// bbb
// cc|c
unsigned offsetOfFirstCharacterInRun = !i ? 0 : runLengthSumTo(i - 1);
measuredWidthInDirection = computeAdvance(run, offsetOfFirstCharacterInRun, offsetOfFirstCharacterInRun, offsetOfFirstCharacterInRun + run.length());
if (measuredWidthInDirection) {
// Since we are measuring from the beginning of a run, x should be 0.
offsetFromOriginInDirection = 0;
}
}
if (measuredWidthInDirection) {
// This run is within the range specified by |start| and |end|, so if we measured a width for it,
// also add to the height. It's important to only do this if we actually measured a width, as an
// offset pointing past the last character in a run will not add any width and thus should not
// contribute any height.
measuredHeightInDirection += run.lineHeight;
}
maxWidthInDirection = std::max(maxWidthInDirection, measuredWidthInDirection);
}
}
// Compared to the main-thread implementation, we regularly produce rects that are 1-3px smaller due to the various
// levels of float rounding that happen to get here. It's better to be a bit wider to ensure AT cursors capture the
// entire range of text than it is to be too small. Concretely, too-wide is better than too-small for low-vision
// VoiceOver users who magnify the VoiceOver cursor's contents. Subjectively, the main-thread implementation feels
// a bit too large, even favoring too-wide sizes, so only bump by 1px. This is especially impactful when navigating
// character-by-character in small text.
static constexpr unsigned sizeBump = 1;
if (orientation == FontOrientation::Horizontal)
return { static_cast<float>(offsetFromOriginInDirection), heightBeforeRuns, static_cast<float>(maxWidthInDirection) + sizeBump, measuredHeightInDirection };
return { heightBeforeRuns, static_cast<float>(offsetFromOriginInDirection), measuredHeightInDirection + sizeBump, static_cast<float>(maxWidthInDirection) };
}
} // namespace WebCore
#endif // ENABLE(AX_THREAD_TEXT_APIS)