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