tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

SelectionMovementUtils.cpp (38515B)


      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 "SelectionMovementUtils.h"
      8 
      9 #include "ErrorList.h"
     10 #include "WordMovementType.h"
     11 #include "mozilla/CaretAssociationHint.h"
     12 #include "mozilla/ContentIterator.h"
     13 #include "mozilla/Maybe.h"
     14 #include "mozilla/PresShell.h"
     15 #include "mozilla/dom/Document.h"
     16 #include "mozilla/dom/Element.h"
     17 #include "mozilla/dom/Selection.h"
     18 #include "mozilla/dom/ShadowRoot.h"
     19 #include "mozilla/intl/BidiEmbeddingLevel.h"
     20 #include "nsBidiPresUtils.h"
     21 #include "nsBlockFrame.h"
     22 #include "nsCOMPtr.h"
     23 #include "nsCaret.h"
     24 #include "nsFrameSelection.h"
     25 #include "nsFrameTraversal.h"
     26 #include "nsIContent.h"
     27 #include "nsIFrame.h"
     28 #include "nsIFrameInlines.h"
     29 #include "nsLayoutUtils.h"
     30 #include "nsPresContext.h"
     31 #include "nsTextFrame.h"
     32 
     33 namespace mozilla {
     34 using namespace dom;
     35 template Result<RangeBoundary, nsresult>
     36 SelectionMovementUtils::MoveRangeBoundaryToSomewhere(
     37    const RangeBoundary& aRangeBoundary, nsDirection aDirection,
     38    CaretAssociationHint aHint, intl::BidiEmbeddingLevel aCaretBidiLevel,
     39    nsSelectionAmount aAmount, PeekOffsetOptions aOptions,
     40    const dom::Element* aAncestorLimiter);
     41 
     42 template Result<RawRangeBoundary, nsresult>
     43 SelectionMovementUtils::MoveRangeBoundaryToSomewhere(
     44    const RawRangeBoundary& aRangeBoundary, nsDirection aDirection,
     45    CaretAssociationHint aHint, intl::BidiEmbeddingLevel aCaretBidiLevel,
     46    nsSelectionAmount aAmount, PeekOffsetOptions aOptions,
     47    const dom::Element* aAncestorLimiter);
     48 
     49 template <typename ParentType, typename RefType>
     50 Result<RangeBoundaryBase<ParentType, RefType>, nsresult>
     51 SelectionMovementUtils::MoveRangeBoundaryToSomewhere(
     52    const RangeBoundaryBase<ParentType, RefType>& aRangeBoundary,
     53    nsDirection aDirection, CaretAssociationHint aHint,
     54    intl::BidiEmbeddingLevel aCaretBidiLevel, nsSelectionAmount aAmount,
     55    PeekOffsetOptions aOptions, const dom::Element* aAncestorLimiter) {
     56  MOZ_ASSERT(aDirection == eDirNext || aDirection == eDirPrevious);
     57  MOZ_ASSERT(aAmount == eSelectCharacter || aAmount == eSelectCluster ||
     58             aAmount == eSelectWord || aAmount == eSelectBeginLine ||
     59             aAmount == eSelectEndLine || aAmount == eSelectParagraph);
     60 
     61  if (!aRangeBoundary.IsSetAndValid()) {
     62    return Err(NS_ERROR_FAILURE);
     63  }
     64  if (!aRangeBoundary.GetContainer()->IsContent()) {
     65    return Err(NS_ERROR_FAILURE);
     66  }
     67  Result<PeekOffsetStruct, nsresult> result = PeekOffsetForCaretMove(
     68      aRangeBoundary.GetContainer()->AsContent(),
     69      *aRangeBoundary.Offset(
     70          RangeBoundaryBase<ParentType,
     71                            RefType>::OffsetFilter::kValidOrInvalidOffsets),
     72      aDirection, aHint, aCaretBidiLevel, aAmount, nsPoint{0, 0}, aOptions,
     73      aAncestorLimiter);
     74  if (result.isErr()) {
     75    return Err(NS_ERROR_FAILURE);
     76  }
     77  const PeekOffsetStruct& pos = result.unwrap();
     78  if (NS_WARN_IF(!pos.mResultContent)) {
     79    return RangeBoundaryBase<ParentType, RefType>{};
     80  }
     81 
     82  return RangeBoundaryBase<ParentType, RefType>{
     83      pos.mResultContent, static_cast<uint32_t>(pos.mContentOffset)};
     84 }
     85 
     86 // FYI: This was done during a call of GetPrimaryFrameForCaretAtFocusNode.
     87 // Therefore, this may not be intended by the original author.
     88 
     89 // static
     90 Result<PeekOffsetStruct, nsresult>
     91 SelectionMovementUtils::PeekOffsetForCaretMove(
     92    nsIContent* aContent, uint32_t aOffset, nsDirection aDirection,
     93    CaretAssociationHint aHint, intl::BidiEmbeddingLevel aCaretBidiLevel,
     94    const nsSelectionAmount aAmount, const nsPoint& aDesiredCaretPos,
     95    PeekOffsetOptions aOptions, const Element* aAncestorLimiter) {
     96  const PrimaryFrameData frameForFocus =
     97      SelectionMovementUtils::GetPrimaryFrameForCaret(
     98          aContent, aOffset, aOptions.contains(PeekOffsetOption::Visual), aHint,
     99          aCaretBidiLevel);
    100  if (!frameForFocus) {
    101    return Err(NS_ERROR_FAILURE);
    102  }
    103 
    104  aOptions += {PeekOffsetOption::JumpLines, PeekOffsetOption::IsKeyboardSelect};
    105  PeekOffsetStruct pos(
    106      aAmount, aDirection,
    107      static_cast<int32_t>(frameForFocus.mOffsetInFrameContent),
    108      aDesiredCaretPos, aOptions, eDefaultBehavior, aAncestorLimiter);
    109  nsresult rv = frameForFocus->PeekOffset(&pos);
    110  if (NS_FAILED(rv)) {
    111    return Err(rv);
    112  }
    113  return pos;
    114 }
    115 
    116 // static
    117 nsPrevNextBidiLevels SelectionMovementUtils::GetPrevNextBidiLevels(
    118    nsIContent* aNode, uint32_t aContentOffset, CaretAssociationHint aHint,
    119    bool aJumpLines, const Element* aAncestorLimiter) {
    120  // Get the level of the frames on each side
    121  nsDirection direction;
    122 
    123  nsPrevNextBidiLevels levels{};
    124  levels.SetData(nullptr, nullptr, intl::BidiEmbeddingLevel::LTR(),
    125                 intl::BidiEmbeddingLevel::LTR());
    126 
    127  FrameAndOffset currentFrameAndOffset =
    128      SelectionMovementUtils::GetFrameForNodeOffset(aNode, aContentOffset,
    129                                                    aHint);
    130  if (!currentFrameAndOffset) {
    131    return levels;
    132  }
    133 
    134  auto [frameStart, frameEnd] = currentFrameAndOffset->GetOffsets();
    135 
    136  if (0 == frameStart && 0 == frameEnd) {
    137    direction = eDirPrevious;
    138  } else if (static_cast<uint32_t>(frameStart) ==
    139             currentFrameAndOffset.mOffsetInFrameContent) {
    140    direction = eDirPrevious;
    141  } else if (static_cast<uint32_t>(frameEnd) ==
    142             currentFrameAndOffset.mOffsetInFrameContent) {
    143    direction = eDirNext;
    144  } else {
    145    // we are neither at the beginning nor at the end of the frame, so we have
    146    // no worries
    147    intl::BidiEmbeddingLevel currentLevel =
    148        currentFrameAndOffset->GetEmbeddingLevel();
    149    levels.SetData(currentFrameAndOffset.mFrame, currentFrameAndOffset.mFrame,
    150                   currentLevel, currentLevel);
    151    return levels;
    152  }
    153 
    154  PeekOffsetOptions peekOffsetOptions{PeekOffsetOption::StopAtScroller};
    155  if (aJumpLines) {
    156    peekOffsetOptions += PeekOffsetOption::JumpLines;
    157  }
    158  nsIFrame* newFrame = currentFrameAndOffset
    159                           ->GetFrameFromDirection(direction, peekOffsetOptions,
    160                                                   aAncestorLimiter)
    161                           .mFrame;
    162 
    163  FrameBidiData currentBidi = currentFrameAndOffset->GetBidiData();
    164  intl::BidiEmbeddingLevel currentLevel = currentBidi.embeddingLevel;
    165  intl::BidiEmbeddingLevel newLevel =
    166      newFrame ? newFrame->GetEmbeddingLevel() : currentBidi.baseLevel;
    167 
    168  // If not jumping lines, disregard br frames, since they might be positioned
    169  // incorrectly.
    170  // XXX This could be removed once bug 339786 is fixed.
    171  if (!aJumpLines) {
    172    if (currentFrameAndOffset->IsBrFrame()) {
    173      currentFrameAndOffset = {nullptr, 0u};
    174      currentLevel = currentBidi.baseLevel;
    175    }
    176    if (newFrame && newFrame->IsBrFrame()) {
    177      newFrame = nullptr;
    178      newLevel = currentBidi.baseLevel;
    179    }
    180  }
    181 
    182  if (direction == eDirNext) {
    183    levels.SetData(currentFrameAndOffset.mFrame, newFrame, currentLevel,
    184                   newLevel);
    185  } else {
    186    levels.SetData(newFrame, currentFrameAndOffset.mFrame, newLevel,
    187                   currentLevel);
    188  }
    189 
    190  return levels;
    191 }
    192 
    193 // static
    194 Result<nsIFrame*, nsresult> SelectionMovementUtils::GetFrameFromLevel(
    195    nsIFrame* aFrameIn, nsDirection aDirection,
    196    intl::BidiEmbeddingLevel aBidiLevel) {
    197  if (!aFrameIn) {
    198    return Err(NS_ERROR_NULL_POINTER);
    199  }
    200 
    201  intl::BidiEmbeddingLevel foundLevel = intl::BidiEmbeddingLevel::LTR();
    202 
    203  nsFrameIterator frameIterator(aFrameIn->PresContext(), aFrameIn,
    204                                nsFrameIterator::Type::Leaf,
    205                                false,  // aVisual
    206                                false,  // aLockInScrollView
    207                                false,  // aFollowOOFs
    208                                false   // aSkipPopupChecks
    209  );
    210 
    211  nsIFrame* foundFrame = aFrameIn;
    212  nsIFrame* theFrame = nullptr;
    213  do {
    214    theFrame = foundFrame;
    215    foundFrame = frameIterator.Traverse(aDirection == eDirNext);
    216    if (!foundFrame) {
    217      return Err(NS_ERROR_FAILURE);
    218    }
    219    foundLevel = foundFrame->GetEmbeddingLevel();
    220 
    221  } while (foundLevel > aBidiLevel);
    222 
    223  MOZ_ASSERT(theFrame);
    224  return theFrame;
    225 }
    226 
    227 bool SelectionMovementUtils::AdjustFrameForLineStart(nsIFrame*& aFrame,
    228                                                     uint32_t& aFrameOffset) {
    229  if (!aFrame->HasSignificantTerminalNewline()) {
    230    return false;
    231  }
    232 
    233  auto [start, end] = aFrame->GetOffsets();
    234  if (aFrameOffset != static_cast<uint32_t>(end)) {
    235    return false;
    236  }
    237 
    238  nsIFrame* nextSibling = aFrame->GetNextSibling();
    239  if (!nextSibling) {
    240    return false;
    241  }
    242 
    243  aFrame = nextSibling;
    244  std::tie(start, end) = aFrame->GetOffsets();
    245  aFrameOffset = start;
    246  return true;
    247 }
    248 
    249 static bool IsDisplayContents(const nsIContent* aContent) {
    250  return aContent->IsElement() && aContent->AsElement()->IsDisplayContents();
    251 }
    252 
    253 // static
    254 FrameAndOffset SelectionMovementUtils::GetFrameForNodeOffset(
    255    const nsIContent* aNode, uint32_t aOffset, CaretAssociationHint aHint) {
    256  if (!aNode) {
    257    return {};
    258  }
    259 
    260  if (static_cast<int32_t>(aOffset) < 0) {
    261    return {};
    262  }
    263 
    264  if (!aNode->GetPrimaryFrame() && !IsDisplayContents(aNode)) {
    265    return {};
    266  }
    267 
    268  nsIFrame *returnFrame = nullptr, *lastFrame = aNode->GetPrimaryFrame();
    269  const nsIContent* theNode = nullptr;
    270  uint32_t offsetInFrameContent, offsetInLastFrameContent = aOffset;
    271 
    272  while (true) {
    273    if (returnFrame) {
    274      lastFrame = returnFrame;
    275      offsetInLastFrameContent = offsetInFrameContent;
    276    }
    277    offsetInFrameContent = aOffset;
    278 
    279    theNode = aNode;
    280 
    281    if (aNode->IsElement()) {
    282      uint32_t childIndex = 0;
    283      uint32_t numChildren = theNode->GetChildCount();
    284 
    285      if (aHint == CaretAssociationHint::Before) {
    286        if (aOffset > 0) {
    287          childIndex = aOffset - 1;
    288        } else {
    289          childIndex = aOffset;
    290        }
    291      } else {
    292        MOZ_ASSERT(aHint == CaretAssociationHint::After);
    293        if (aOffset >= numChildren) {
    294          if (numChildren > 0) {
    295            childIndex = numChildren - 1;
    296          } else {
    297            childIndex = 0;
    298          }
    299        } else {
    300          childIndex = aOffset;
    301        }
    302      }
    303 
    304      if (childIndex > 0 || numChildren > 0) {
    305        nsCOMPtr<nsIContent> childNode =
    306            theNode->GetChildAt_Deprecated(childIndex);
    307 
    308        if (!childNode) {
    309          break;
    310        }
    311 
    312        theNode = childNode;
    313      }
    314 
    315      // Now that we have the child node, check if it too
    316      // can contain children. If so, descend into child.
    317      if (theNode->IsElement() && theNode->GetChildCount() &&
    318          !theNode->HasIndependentSelection()) {
    319        aNode = theNode;
    320        aOffset = aOffset > childIndex ? theNode->GetChildCount() : 0;
    321        continue;
    322      }
    323 
    324      // Check to see if theNode is a text node. If it is, translate
    325      // aOffset into an offset into the text node.
    326      if (const Text* textNode = Text::FromNode(theNode)) {
    327        if (theNode->GetPrimaryFrame()) {
    328          if (aOffset > childIndex) {
    329            uint32_t textLength = textNode->Length();
    330 
    331            offsetInFrameContent = textLength;
    332          } else {
    333            offsetInFrameContent = 0;
    334          }
    335        } else {
    336          uint32_t numChildren = aNode->GetChildCount();
    337          uint32_t newChildIndex = aHint == CaretAssociationHint::Before
    338                                       ? childIndex - 1
    339                                       : childIndex + 1;
    340 
    341          if (newChildIndex < numChildren) {
    342            nsCOMPtr<nsIContent> newChildNode =
    343                aNode->GetChildAt_Deprecated(newChildIndex);
    344            if (!newChildNode) {
    345              return {};
    346            }
    347 
    348            aNode = newChildNode;
    349            aOffset = aHint == CaretAssociationHint::Before
    350                          ? aNode->GetChildCount()
    351                          : 0;
    352            continue;
    353          }  // newChildIndex is illegal which means we're at first or last
    354          // child. Just use original node to get the frame.
    355          theNode = aNode;
    356        }
    357      }
    358    }
    359 
    360    // If the node is a ShadowRoot, the frame needs to be adjusted,
    361    // because a ShadowRoot does not get a frame. Its children are rendered
    362    // as children of the host.
    363    if (const ShadowRoot* shadow = ShadowRoot::FromNode(theNode)) {
    364      theNode = shadow->GetHost();
    365    }
    366 
    367    returnFrame = theNode->GetPrimaryFrame();
    368    if (returnFrame) {
    369      // FIXME: offsetInFrameContent has not been updated for theNode yet when
    370      // theNode is different from aNode. E.g., if a child at aNode and aOffset
    371      // is an <img>, theNode is now the <img> but offsetInFrameContent is the
    372      // offset for aNode.
    373      break;
    374    }
    375 
    376    if (aHint == CaretAssociationHint::Before) {
    377      if (aOffset > 0) {
    378        --aOffset;
    379        continue;
    380      }
    381      break;
    382    }
    383    if (aOffset < theNode->GetChildCount()) {
    384      ++aOffset;
    385      continue;
    386    }
    387    break;
    388  }  // end while
    389 
    390  if (!returnFrame) {
    391    if (!lastFrame) {
    392      return {};
    393    }
    394    returnFrame = lastFrame;
    395    offsetInFrameContent = offsetInLastFrameContent;
    396  }
    397 
    398  // If we ended up here and were asked to position the caret after a visible
    399  // break, let's return the frame on the next line instead if it exists.
    400  if (aOffset > 0 && (uint32_t)aOffset >= aNode->Length() &&
    401      theNode == aNode->GetLastChild()) {
    402    nsIFrame* newFrame;
    403    nsLayoutUtils::IsInvisibleBreak(theNode, &newFrame);
    404    if (newFrame) {
    405      returnFrame = newFrame;
    406      offsetInFrameContent = 0;
    407    }
    408  }
    409 
    410  // find the child frame containing the offset we want
    411  int32_t unused = 0;
    412  returnFrame->GetChildFrameContainingOffset(
    413      static_cast<int32_t>(offsetInFrameContent),
    414      aHint == CaretAssociationHint::After, &unused, &returnFrame);
    415  return {returnFrame, offsetInFrameContent};
    416 }
    417 
    418 // static
    419 RawRangeBoundary SelectionMovementUtils::GetFirstVisiblePointAtLeaf(
    420    const AbstractRange& aRange) {
    421  MOZ_ASSERT(aRange.IsPositioned());
    422  MOZ_ASSERT_IF(aRange.IsStaticRange(), aRange.AsStaticRange()->IsValid());
    423 
    424  // Currently, this is designed for non-collapsed range because this tries to
    425  // return a point in aRange.  Therefore, if we need to return a nearest point
    426  // even outside aRange, we should add another utility method for making it
    427  // accept the outer range.
    428  MOZ_ASSERT(!aRange.Collapsed());
    429 
    430  // The result should be a good point to put a UI to show something about the
    431  // start boundary of aRange.  Therefore, we should find a content which is
    432  // visible or first unselectable one.
    433 
    434  // FIXME: ContentIterator does not support iterating content across shadow DOM
    435  // boundaries.  We should improve it and here support it as an option.
    436 
    437  // If the start boundary is in a visible and selectable `Text`, let's return
    438  // the start boundary as-is.
    439  if (Text* const text = Text::FromNode(aRange.GetStartContainer())) {
    440    nsIFrame* const textFrame = text->GetPrimaryFrame();
    441    if (textFrame && textFrame->IsSelectable()) {
    442      return aRange.StartRef().AsRaw();
    443    }
    444  }
    445 
    446  // Iterate start of each node in the range so that the following loop checks
    447  // containers first, then, inner containers and leaf nodes.
    448  UnsafePreContentIterator iter;
    449  if (aRange.IsDynamicRange()) {
    450    if (NS_WARN_IF(NS_FAILED(iter.InitWithoutValidatingPoints(
    451            aRange.StartRef().AsRaw(), aRange.EndRef().AsRaw())))) {
    452      return {nullptr, nullptr};
    453    }
    454  } else {
    455    if (NS_WARN_IF(NS_FAILED(
    456            iter.Init(aRange.StartRef().AsRaw(), aRange.EndRef().AsRaw())))) {
    457      return {nullptr, nullptr};
    458    }
    459  }
    460 
    461  // We need to ignore unselectable nodes if the range started from an
    462  // unselectable node, for example, if starting from the document start but
    463  // only in <dialog> which is shown as a modal one is selectable, we want to
    464  // treat the visible selection starts from the start of the first visible
    465  // thing in the <dialog>.
    466  // Additionally, let's stop when we find first unselectable element in a
    467  // selectable node.  Then, the caller can show something at the end edge of
    468  // the unselectable element rather than the leaf to make it clear that the
    469  // selection range starts before the unselectable element.
    470  bool foundSelectableContainer = [&]() {
    471    nsIContent* const startContainer =
    472        nsIContent::FromNode(aRange.GetStartContainer());
    473    return startContainer && startContainer->IsSelectable();
    474  }();
    475  for (iter.First(); !iter.IsDone(); iter.Next()) {
    476    nsIContent* const content =
    477        nsIContent::FromNodeOrNull(iter.GetCurrentNode());
    478    if (MOZ_UNLIKELY(!content)) {
    479      break;
    480    }
    481    nsIFrame* const primaryFrame = content->GetPrimaryFrame();
    482    // If the content does not have any layout information, let's continue.
    483    if (!primaryFrame) {
    484      continue;
    485    }
    486 
    487    // FYI: We don't need to skip invisible <br> at scanning start of visible
    488    // thing like what we're doing in GetVisibleRangeEnd() because if we reached
    489    // it, the selection range starts from end of the line so that putting UI
    490    // around it is reasonable.
    491 
    492    // If the frame is unselectable, we need to stop scanning now if we're
    493    // scanning in a selectable range.
    494    if (!primaryFrame->IsSelectable()) {
    495      // If we have not found a selectable content yet (this is the case when
    496      // only a part of the document is selectable like the <dialog> case
    497      // explained above), we should just ignore the unselectable content until
    498      // we find first selectable element.  Then, the caller can show something
    499      // before the first child of the first selectable container in the range.
    500      if (!foundSelectableContainer) {
    501        continue;
    502      }
    503      // If we have already found a selectable content and now we reached an
    504      // unselectable element, we should return the point of the unselectable
    505      // element.  Then, the caller can show something at the start edge of the
    506      // unselectable element to show users that the range contains the
    507      // unselectable element.
    508      return {content->GetParentNode(), content->GetPreviousSibling()};
    509    }
    510    // We found a visible (and maybe selectable) Text, return the start of it.
    511    if (content->IsText()) {
    512      return {content, 0u};
    513    }
    514    // We found a replaced element such as <br>, <img>, form widget return the
    515    // point at the content.
    516    if (primaryFrame->IsReplaced()) {
    517      return {content->GetParentNode(), content->GetPreviousSibling()};
    518    }
    519    // <button> is a special case, whose frame is not treated as a replaced
    520    // element, but we don't want to shrink the range into it.
    521    if (content->IsHTMLElement(nsGkAtoms::button)) {
    522      return {content->GetParentNode(), content->GetPreviousSibling()};
    523    }
    524    // We found a leaf node like <span></span>.  Return start of it.
    525    if (!content->HasChildren()) {
    526      return {content, 0u};
    527    }
    528    foundSelectableContainer = true;
    529  }
    530  // If there is no visible and selectable things but the start container is
    531  // selectable, return the original point as is.
    532  if (foundSelectableContainer) {
    533    return aRange.StartRef().AsRaw();
    534  }
    535  // If the range is completely invisible, return unset boundary.
    536  return {nullptr, nullptr};
    537 }
    538 
    539 // static
    540 RawRangeBoundary SelectionMovementUtils::GetLastVisiblePointAtLeaf(
    541    const AbstractRange& aRange) {
    542  MOZ_ASSERT(aRange.IsPositioned());
    543  MOZ_ASSERT_IF(aRange.IsStaticRange(), aRange.AsStaticRange()->IsValid());
    544 
    545  // Currently, this is designed for non-collapsed range because this tries to
    546  // return a point in aRange.  Therefore, if we need to return a nearest point
    547  // even outside aRange, we should add another utility method for making it
    548  // accept the outer range.
    549  MOZ_ASSERT(!aRange.Collapsed());
    550 
    551  // The result should be a good point to put a UI to show something about the
    552  // end boundary of aRange.  Therefore, we should find a leaf content which is
    553  // visible or first unselectable one.
    554 
    555  // FIXME: ContentIterator does not support iterating content across shadow DOM
    556  // boundaries.  We should improve it and here support it as an option.
    557 
    558  // If the end boundary is in a visible and selectable `Text`, let's return the
    559  // end boundary as-is.
    560  if (Text* const text = Text::FromNode(aRange.GetEndContainer())) {
    561    nsIFrame* const textFrame = text->GetPrimaryFrame();
    562    if (textFrame && textFrame->IsSelectable()) {
    563      return aRange.EndRef().AsRaw();
    564    }
    565  }
    566 
    567  // Iterate end of each node in the range so that the following loop checks
    568  // containers first, then, inner containers and leaf nodes.
    569  UnsafePostContentIterator iter;
    570  if (aRange.IsDynamicRange()) {
    571    if (NS_WARN_IF(NS_FAILED(iter.InitWithoutValidatingPoints(
    572            aRange.StartRef().AsRaw(), aRange.EndRef().AsRaw())))) {
    573      return {nullptr, nullptr};
    574    }
    575  } else {
    576    if (NS_WARN_IF(NS_FAILED(
    577            iter.Init(aRange.StartRef().AsRaw(), aRange.EndRef().AsRaw())))) {
    578      return {nullptr, nullptr};
    579    }
    580  }
    581 
    582  // We need to ignore unselectable nodes if the range ends in an unselectable
    583  // node, for example, if ending at the document end but only in <dialog> which
    584  // is shown as a modal one, is selectable, we want to treat the visible
    585  // selection ends at the end of the last visible thing in the <dialog>.
    586  // Additionally, let's stop when we find first unselectable element in a
    587  // selectable node.  Then, the caller can show something at the end edge of
    588  // the unselectable element rather than the leaf to make it clear that the
    589  // selection range ends are the unselectable element.
    590  bool foundSelectableContainer = [&]() {
    591    nsIContent* const endContainer =
    592        nsIContent::FromNode(aRange.GetEndContainer());
    593    return endContainer && endContainer->IsSelectable();
    594  }();
    595  for (iter.Last(); !iter.IsDone(); iter.Prev()) {
    596    nsIContent* const content =
    597        nsIContent::FromNodeOrNull(iter.GetCurrentNode());
    598    if (!content) {
    599      break;
    600    }
    601    nsIFrame* const primaryFrame = content->GetPrimaryFrame();
    602    // If the content does not have any layout information, let's continue.
    603    if (!primaryFrame) {
    604      continue;
    605    }
    606    // If we reached an invisible <br>, we should skip it because
    607    // AccessibleCaretManager wants to put the caret for end boundary before the
    608    // <br> instead of at the end edge of the block.
    609    if (nsLayoutUtils::IsInvisibleBreak(content)) {
    610      if (primaryFrame->IsSelectable()) {
    611        foundSelectableContainer = true;
    612      }
    613      continue;
    614    }
    615    // If the frame is unselectable, we need to stop scanning now if we're
    616    // scanning in a selectable range.
    617    if (!primaryFrame->IsSelectable()) {
    618      // If we have not found a selectable content yet (this is the case when
    619      // only a part of the document is selectable like the <dialog> case
    620      // explained above), we should just ignore the unselectable content until
    621      // we find first selectable element.  Then, the caller can show something
    622      // after the last child of the last selectable container in the range.
    623      if (!foundSelectableContainer) {
    624        continue;
    625      }
    626      // If we have already found a selectable content and now we reached an
    627      // unselectable element, we should return the point after the unselectable
    628      // element.  Then, the caller can show something at the end edge of the
    629      // unselectable element to show users that the range contains the
    630      // unselectable element.
    631      return {content->GetParentNode(), content};
    632    }
    633    // We found a visible (and maybe selectable) Text, return the end of it.
    634    if (Text* const text = Text::FromNode(content)) {
    635      return {text, text->TextDataLength()};
    636    }
    637    // We found a replaced element such as <br>, <img>, form widget return the
    638    // point after the content.
    639    if (primaryFrame->IsReplaced()) {
    640      return {content->GetParentNode(), content};
    641    }
    642    // <button> is a special case, whose frame is not treated as a replaced
    643    // element, but we don't want to shrink the range into it.
    644    if (content->IsHTMLElement(nsGkAtoms::button)) {
    645      return {content->GetParentNode(), content};
    646    }
    647    // We found a leaf node like <span></span>.  Return end of it.
    648    if (!content->HasChildren()) {
    649      return {content, 0u};
    650    }
    651    foundSelectableContainer = true;
    652  }
    653  // If there is no visible and selectable things but the end container is
    654  // selectable, return the original point as is.
    655  if (foundSelectableContainer) {
    656    return aRange.EndRef().AsRaw();
    657  }
    658  // If the range is completely invisible, return unset boundary.
    659  return {nullptr, nullptr};
    660 }
    661 
    662 /**
    663 * Find the first frame in an in-order traversal of the frame subtree rooted
    664 * at aFrame which is either a text frame logically at the end of a line,
    665 * or which is aStopAtFrame. Return null if no such frame is found. We don't
    666 * descend into the children of non-eLineParticipant frames.
    667 */
    668 static nsIFrame* CheckForTrailingTextFrameRecursive(nsIFrame* aFrame,
    669                                                    nsIFrame* aStopAtFrame) {
    670  if (aFrame == aStopAtFrame ||
    671      ((aFrame->IsTextFrame() &&
    672        (static_cast<nsTextFrame*>(aFrame))->IsAtEndOfLine()))) {
    673    return aFrame;
    674  }
    675  if (!aFrame->IsLineParticipant()) {
    676    return nullptr;
    677  }
    678 
    679  for (nsIFrame* f : aFrame->PrincipalChildList()) {
    680    if (nsIFrame* r = CheckForTrailingTextFrameRecursive(f, aStopAtFrame)) {
    681      return r;
    682    }
    683  }
    684  return nullptr;
    685 }
    686 
    687 static nsLineBox* FindContainingLine(nsIFrame* aFrame) {
    688  while (aFrame && aFrame->IsLineParticipant()) {
    689    nsIFrame* parent = aFrame->GetParent();
    690    nsBlockFrame* blockParent = do_QueryFrame(parent);
    691    if (blockParent) {
    692      bool isValid;
    693      nsBlockInFlowLineIterator iter(blockParent, aFrame, &isValid);
    694      return isValid ? iter.GetLine().get() : nullptr;
    695    }
    696    aFrame = parent;
    697  }
    698  return nullptr;
    699 }
    700 
    701 static void AdjustCaretFrameForLineEnd(nsIFrame** aFrame, uint32_t* aOffset,
    702                                       bool aEditableOnly) {
    703  nsLineBox* line = FindContainingLine(*aFrame);
    704  if (!line) {
    705    return;
    706  }
    707  uint32_t count = line->GetChildCount();
    708  for (nsIFrame* f = line->mFirstChild; count > 0;
    709       --count, f = f->GetNextSibling()) {
    710    nsIFrame* r = CheckForTrailingTextFrameRecursive(f, *aFrame);
    711    if (r == *aFrame) {
    712      return;
    713    }
    714    if (!r) {
    715      continue;
    716    }
    717    // If found text frame is non-editable but the start frame content is
    718    // editable, we don't want to put caret into the non-editable text node.
    719    // We should return the given frame as-is in this case.
    720    if (aEditableOnly && !r->GetContent()->IsEditable()) {
    721      return;
    722    }
    723    // We found our frame.
    724    MOZ_ASSERT(r->IsTextFrame(), "Expected text frame");
    725    *aFrame = r;
    726    *aOffset = (static_cast<nsTextFrame*>(r))->GetContentEnd();
    727    return;
    728    // FYI: Setting the caret association hint was done during a call of
    729    // GetPrimaryFrameForCaretAtFocusNode.  Therefore, this may not be intended
    730    // by the original author.
    731  }
    732 }
    733 
    734 CaretFrameData SelectionMovementUtils::GetCaretFrameForNodeOffset(
    735    const nsFrameSelection* aFrameSelection, nsIContent* aContentNode,
    736    uint32_t aOffset, CaretAssociationHint aFrameHint,
    737    intl::BidiEmbeddingLevel aBidiLevel,
    738    ForceEditableRegion aForceEditableRegion) {
    739  if (!aContentNode || !aContentNode->IsInComposedDoc()) {
    740    return {};
    741  }
    742 
    743  CaretFrameData result;
    744  result.mHint = aFrameHint;
    745  if (aFrameSelection) {
    746    PresShell* presShell = aFrameSelection->GetPresShell();
    747    if (!presShell) {
    748      return {};
    749    }
    750 
    751    if (!aContentNode || !aContentNode->IsInComposedDoc() ||
    752        presShell->GetDocument() != aContentNode->GetComposedDoc()) {
    753      return {};
    754    }
    755 
    756    result.mHint = aFrameSelection->GetHint();
    757  }
    758 
    759  MOZ_ASSERT_IF(aForceEditableRegion == ForceEditableRegion::Yes,
    760                aContentNode->IsEditable());
    761 
    762  const FrameAndOffset frameAndOffset =
    763      SelectionMovementUtils::GetFrameForNodeOffset(aContentNode, aOffset,
    764                                                    aFrameHint);
    765  if (!frameAndOffset) {
    766    return {};
    767  }
    768  result.mFrame = result.mUnadjustedFrame = frameAndOffset.mFrame;
    769  result.mOffsetInFrameContent = frameAndOffset.mOffsetInFrameContent;
    770 
    771  if (SelectionMovementUtils::AdjustFrameForLineStart(
    772          result.mFrame, result.mOffsetInFrameContent)) {
    773    result.mHint = CaretAssociationHint::After;
    774  } else {
    775    // if the frame is after a text frame that's logically at the end of the
    776    // line (e.g. if the frame is a <br> frame), then put the caret at the end
    777    // of that text frame instead. This way, the caret will be positioned as if
    778    // trailing whitespace was not trimmed.
    779    AdjustCaretFrameForLineEnd(
    780        &result.mFrame, &result.mOffsetInFrameContent,
    781        aForceEditableRegion == ForceEditableRegion::Yes);
    782  }
    783 
    784  // Mamdouh : modification of the caret to work at rtl and ltr with Bidi
    785  //
    786  // Direction Style from visibility->mDirection
    787  // ------------------
    788  if (!result->PresContext()->BidiEnabled()) {
    789    return result;
    790  }
    791 
    792  // If there has been a reflow, take the caret Bidi level to be the level of
    793  // the current frame
    794  if (aBidiLevel & BIDI_LEVEL_UNDEFINED) {
    795    aBidiLevel = result->GetEmbeddingLevel();
    796  }
    797 
    798  nsIFrame* frameBefore;
    799  nsIFrame* frameAfter;
    800  intl::BidiEmbeddingLevel
    801      levelBefore;  // Bidi level of the character before the caret
    802  intl::BidiEmbeddingLevel
    803      levelAfter;  // Bidi level of the character after the caret
    804 
    805  auto [start, end] = result->GetOffsets();
    806  if (start == 0 || end == 0 ||
    807      static_cast<uint32_t>(start) == result.mOffsetInFrameContent ||
    808      static_cast<uint32_t>(end) == result.mOffsetInFrameContent) {
    809    nsPrevNextBidiLevels levels = SelectionMovementUtils::GetPrevNextBidiLevels(
    810        aContentNode, aOffset, result.mHint, false,
    811        aFrameSelection
    812            ? aFrameSelection
    813                  ->GetAncestorLimiterOrIndependentSelectionRootElement()
    814            : nullptr);
    815 
    816    /* Boundary condition, we need to know the Bidi levels of the characters
    817     * before and after the caret */
    818    if (levels.mFrameBefore || levels.mFrameAfter) {
    819      frameBefore = levels.mFrameBefore;
    820      frameAfter = levels.mFrameAfter;
    821      levelBefore = levels.mLevelBefore;
    822      levelAfter = levels.mLevelAfter;
    823 
    824      if ((levelBefore != levelAfter) || (aBidiLevel != levelBefore)) {
    825        aBidiLevel =
    826            std::max(aBidiLevel, std::min(levelBefore, levelAfter));  // rule c3
    827        aBidiLevel =
    828            std::min(aBidiLevel, std::max(levelBefore, levelAfter));  // rule c4
    829        if (aBidiLevel == levelBefore ||                              // rule c1
    830            (aBidiLevel > levelBefore && aBidiLevel < levelAfter &&
    831             aBidiLevel.IsSameDirection(levelBefore)) ||  // rule c5
    832            (aBidiLevel < levelBefore && aBidiLevel > levelAfter &&
    833             aBidiLevel.IsSameDirection(levelBefore)))  // rule c9
    834        {
    835          if (result.mFrame != frameBefore) {
    836            if (frameBefore) {  // if there is a frameBefore, move into it
    837              result.mFrame = frameBefore;
    838              std::tie(start, end) = result->GetOffsets();
    839              result.mOffsetInFrameContent = end;
    840            } else {
    841              // if there is no frameBefore, we must be at the beginning of
    842              // the line so we stay with the current frame. Exception: when
    843              // the first frame on the line has a different Bidi level from
    844              // the paragraph level, there is no real frame for the caret to
    845              // be in. We have to find the visually first frame on the line.
    846              intl::BidiEmbeddingLevel baseLevel = frameAfter->GetBaseLevel();
    847              if (baseLevel != levelAfter) {
    848                PeekOffsetStruct pos(eSelectBeginLine, eDirPrevious, 0,
    849                                     nsPoint(0, 0),
    850                                     {PeekOffsetOption::StopAtScroller,
    851                                      PeekOffsetOption::Visual});
    852                if (NS_SUCCEEDED(frameAfter->PeekOffset(&pos))) {
    853                  result.mFrame = pos.mResultFrame;
    854                  result.mOffsetInFrameContent = pos.mContentOffset;
    855                }
    856              }
    857            }
    858          }
    859        } else if (aBidiLevel == levelAfter ||  // rule c2
    860                   (aBidiLevel > levelBefore && aBidiLevel < levelAfter &&
    861                    aBidiLevel.IsSameDirection(levelAfter)) ||  // rule c6
    862                   (aBidiLevel < levelBefore && aBidiLevel > levelAfter &&
    863                    aBidiLevel.IsSameDirection(levelAfter)))  // rule c10
    864        {
    865          if (result.mFrame != frameAfter) {
    866            if (frameAfter) {
    867              // if there is a frameAfter, move into it
    868              result.mFrame = frameAfter;
    869              std::tie(start, end) = result->GetOffsets();
    870              result.mOffsetInFrameContent = start;
    871            } else {
    872              // if there is no frameAfter, we must be at the end of the line
    873              // so we stay with the current frame.
    874              // Exception: when the last frame on the line has a different
    875              // Bidi level from the paragraph level, there is no real frame
    876              // for the caret to be in. We have to find the visually last
    877              // frame on the line.
    878              intl::BidiEmbeddingLevel baseLevel = frameBefore->GetBaseLevel();
    879              if (baseLevel != levelBefore) {
    880                PeekOffsetStruct pos(eSelectEndLine, eDirNext, 0, nsPoint(0, 0),
    881                                     {PeekOffsetOption::StopAtScroller,
    882                                      PeekOffsetOption::Visual});
    883                if (NS_SUCCEEDED(frameBefore->PeekOffset(&pos))) {
    884                  result.mFrame = pos.mResultFrame;
    885                  result.mOffsetInFrameContent = pos.mContentOffset;
    886                }
    887              }
    888            }
    889          }
    890        } else if (aBidiLevel > levelBefore &&
    891                   aBidiLevel < levelAfter &&  // rule c7/8
    892                   // before and after have the same parity
    893                   levelBefore.IsSameDirection(levelAfter) &&
    894                   // caret has different parity
    895                   !aBidiLevel.IsSameDirection(levelAfter)) {
    896          MOZ_ASSERT_IF(aFrameSelection && aFrameSelection->GetPresShell(),
    897                        aFrameSelection->GetPresShell()->GetPresContext() ==
    898                            frameAfter->PresContext());
    899          Result<nsIFrame*, nsresult> frameOrError =
    900              SelectionMovementUtils::GetFrameFromLevel(frameAfter, eDirNext,
    901                                                        aBidiLevel);
    902          if (MOZ_LIKELY(frameOrError.isOk())) {
    903            result.mFrame = frameOrError.unwrap();
    904            std::tie(start, end) = result->GetOffsets();
    905            levelAfter = result->GetEmbeddingLevel();
    906            if (aBidiLevel.IsRTL()) {
    907              // c8: caret to the right of the rightmost character
    908              result.mOffsetInFrameContent = levelAfter.IsRTL() ? start : end;
    909            } else {
    910              // c7: caret to the left of the leftmost character
    911              result.mOffsetInFrameContent = levelAfter.IsRTL() ? end : start;
    912            }
    913          }
    914        } else if (aBidiLevel < levelBefore &&
    915                   aBidiLevel > levelAfter &&  // rule c11/12
    916                   // before and after have the same parity
    917                   levelBefore.IsSameDirection(levelAfter) &&
    918                   // caret has different parity
    919                   !aBidiLevel.IsSameDirection(levelAfter)) {
    920          MOZ_ASSERT_IF(aFrameSelection && aFrameSelection->GetPresShell(),
    921                        aFrameSelection->GetPresShell()->GetPresContext() ==
    922                            frameBefore->PresContext());
    923          Result<nsIFrame*, nsresult> frameOrError =
    924              SelectionMovementUtils::GetFrameFromLevel(
    925                  frameBefore, eDirPrevious, aBidiLevel);
    926          if (MOZ_LIKELY(frameOrError.isOk())) {
    927            result.mFrame = frameOrError.unwrap();
    928            std::tie(start, end) = result->GetOffsets();
    929            levelBefore = result->GetEmbeddingLevel();
    930            if (aBidiLevel.IsRTL()) {
    931              // c12: caret to the left of the leftmost character
    932              result.mOffsetInFrameContent = levelBefore.IsRTL() ? end : start;
    933            } else {
    934              // c11: caret to the right of the rightmost character
    935              result.mOffsetInFrameContent = levelBefore.IsRTL() ? start : end;
    936            }
    937          }
    938        }
    939      }
    940    }
    941  }
    942 
    943  return result;
    944 }
    945 
    946 // static
    947 PrimaryFrameData SelectionMovementUtils::GetPrimaryFrameForCaret(
    948    nsIContent* aContent, uint32_t aOffset, bool aVisual,
    949    CaretAssociationHint aHint, intl::BidiEmbeddingLevel aCaretBidiLevel) {
    950  MOZ_ASSERT(aContent);
    951 
    952  {
    953    const PrimaryFrameData result =
    954        SelectionMovementUtils::GetPrimaryOrCaretFrameForNodeOffset(
    955            aContent, aOffset, aVisual, aHint, aCaretBidiLevel);
    956    if (result) {
    957      return result;
    958    }
    959  }
    960 
    961  // If aContent is whitespace only, we promote focus node to parent because
    962  // whitespace only node might have no frame.
    963 
    964  if (!aContent->TextIsOnlyWhitespace()) {
    965    return {};
    966  }
    967 
    968  nsIContent* parent = aContent->GetParent();
    969  if (NS_WARN_IF(!parent)) {
    970    return {};
    971  }
    972  const Maybe<uint32_t> offset = parent->ComputeIndexOf(aContent);
    973  if (NS_WARN_IF(offset.isNothing())) {
    974    return {};
    975  }
    976  return SelectionMovementUtils::GetPrimaryOrCaretFrameForNodeOffset(
    977      parent, *offset, aVisual, aHint, aCaretBidiLevel);
    978 }
    979 
    980 // static
    981 PrimaryFrameData SelectionMovementUtils::GetPrimaryOrCaretFrameForNodeOffset(
    982    nsIContent* aContent, uint32_t aOffset, bool aVisual,
    983    CaretAssociationHint aHint, intl::BidiEmbeddingLevel aCaretBidiLevel) {
    984  if (aVisual) {
    985    const CaretFrameData result =
    986        SelectionMovementUtils::GetCaretFrameForNodeOffset(
    987            nullptr, aContent, aOffset, aHint, aCaretBidiLevel,
    988            aContent && aContent->IsEditable() ? ForceEditableRegion::Yes
    989                                               : ForceEditableRegion::No);
    990    return result;
    991  }
    992 
    993  return {
    994      SelectionMovementUtils::GetFrameForNodeOffset(aContent, aOffset, aHint),
    995      aHint};
    996 }
    997 
    998 }  // namespace mozilla