Bug 1646700 [wpt PR 24235] - Update picture-in-picture idlharness test, a=testonly
[gecko.git] / layout / base / AccessibleCaretManager.cpp
blobc6c4127e959419984d0ffd6787298e44ca995257
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/. */
7 #include "AccessibleCaretManager.h"
9 #include "AccessibleCaret.h"
10 #include "AccessibleCaretEventHub.h"
11 #include "AccessibleCaretLogger.h"
12 #include "mozilla/AsyncEventDispatcher.h"
13 #include "mozilla/AutoRestore.h"
14 #include "mozilla/dom/Element.h"
15 #include "mozilla/dom/MouseEventBinding.h"
16 #include "mozilla/dom/NodeFilterBinding.h"
17 #include "mozilla/dom/Selection.h"
18 #include "mozilla/dom/TreeWalker.h"
19 #include "mozilla/IMEStateManager.h"
20 #include "mozilla/IntegerPrintfMacros.h"
21 #include "mozilla/PresShell.h"
22 #include "mozilla/StaticPrefs_layout.h"
23 #include "nsCaret.h"
24 #include "nsContainerFrame.h"
25 #include "nsContentUtils.h"
26 #include "nsDebug.h"
27 #include "nsFocusManager.h"
28 #include "nsIFrame.h"
29 #include "nsFrameSelection.h"
30 #include "nsGenericHTMLElement.h"
31 #include "nsIHapticFeedback.h"
33 namespace mozilla {
35 #undef AC_LOG
36 #define AC_LOG(message, ...) \
37 AC_LOG_BASE("AccessibleCaretManager (%p): " message, this, ##__VA_ARGS__);
39 #undef AC_LOGV
40 #define AC_LOGV(message, ...) \
41 AC_LOGV_BASE("AccessibleCaretManager (%p): " message, this, ##__VA_ARGS__);
43 using namespace dom;
44 using Appearance = AccessibleCaret::Appearance;
45 using PositionChangedResult = AccessibleCaret::PositionChangedResult;
47 #define AC_PROCESS_ENUM_TO_STREAM(e) \
48 case (e): \
49 aStream << #e; \
50 break;
51 std::ostream& operator<<(std::ostream& aStream,
52 const AccessibleCaretManager::CaretMode& aCaretMode) {
53 using CaretMode = AccessibleCaretManager::CaretMode;
54 switch (aCaretMode) {
55 AC_PROCESS_ENUM_TO_STREAM(CaretMode::None);
56 AC_PROCESS_ENUM_TO_STREAM(CaretMode::Cursor);
57 AC_PROCESS_ENUM_TO_STREAM(CaretMode::Selection);
59 return aStream;
62 std::ostream& operator<<(
63 std::ostream& aStream,
64 const AccessibleCaretManager::UpdateCaretsHint& aHint) {
65 using UpdateCaretsHint = AccessibleCaretManager::UpdateCaretsHint;
66 switch (aHint) {
67 AC_PROCESS_ENUM_TO_STREAM(UpdateCaretsHint::Default);
68 AC_PROCESS_ENUM_TO_STREAM(UpdateCaretsHint::RespectOldAppearance);
69 AC_PROCESS_ENUM_TO_STREAM(UpdateCaretsHint::DispatchNoEvent);
71 return aStream;
73 #undef AC_PROCESS_ENUM_TO_STREAM
75 AccessibleCaretManager::AccessibleCaretManager(PresShell* aPresShell)
76 : mPresShell(aPresShell) {
77 if (!mPresShell) {
78 return;
81 mFirstCaret = MakeUnique<AccessibleCaret>(mPresShell);
82 mSecondCaret = MakeUnique<AccessibleCaret>(mPresShell);
85 AccessibleCaretManager::~AccessibleCaretManager() {
86 MOZ_RELEASE_ASSERT(!mFlushingLayout, "Going away in FlushLayout? Bad!");
89 void AccessibleCaretManager::Terminate() {
90 mFirstCaret = nullptr;
91 mSecondCaret = nullptr;
92 mActiveCaret = nullptr;
93 mPresShell = nullptr;
96 nsresult AccessibleCaretManager::OnSelectionChanged(Document* aDoc,
97 Selection* aSel,
98 int16_t aReason) {
99 Selection* selection = GetSelection();
100 AC_LOG("%s: aSel: %p, GetSelection(): %p, aReason: %d", __FUNCTION__, aSel,
101 selection, aReason);
102 if (aSel != selection) {
103 return NS_OK;
106 // eSetSelection events from the Fennec widget IME can be generated
107 // by autoSuggest / autoCorrect composition changes, or by TYPE_REPLACE_TEXT
108 // actions, either positioning cursor for text insert, or selecting
109 // text-to-be-replaced. None should affect AccessibleCaret visibility.
110 if (aReason & nsISelectionListener::IME_REASON) {
111 return NS_OK;
114 // Move the cursor by JavaScript or unknown internal call.
115 if (aReason == nsISelectionListener::NO_REASON) {
116 auto mode = static_cast<ScriptUpdateMode>(
117 StaticPrefs::layout_accessiblecaret_script_change_update_mode());
118 if (mode == kScriptAlwaysShow || (mode == kScriptUpdateVisible &&
119 (mFirstCaret->IsLogicallyVisible() ||
120 mSecondCaret->IsLogicallyVisible()))) {
121 UpdateCarets();
122 return NS_OK;
124 // Default for NO_REASON is to make hidden.
125 HideCarets();
126 return NS_OK;
129 // Move cursor by keyboard.
130 if (aReason & nsISelectionListener::KEYPRESS_REASON) {
131 HideCarets();
132 return NS_OK;
135 // OnBlur() might be called between mouse down and mouse up, so we hide carets
136 // upon mouse down anyway, and update carets upon mouse up.
137 if (aReason & nsISelectionListener::MOUSEDOWN_REASON) {
138 HideCarets();
139 return NS_OK;
142 // Range will collapse after cutting or copying text.
143 if (aReason & (nsISelectionListener::COLLAPSETOSTART_REASON |
144 nsISelectionListener::COLLAPSETOEND_REASON)) {
145 HideCarets();
146 return NS_OK;
149 // For mouse input we don't want to show the carets.
150 if (StaticPrefs::layout_accessiblecaret_hide_carets_for_mouse_input() &&
151 mLastInputSource == MouseEvent_Binding::MOZ_SOURCE_MOUSE) {
152 HideCarets();
153 return NS_OK;
156 // When we want to hide the carets for mouse input, hide them for select
157 // all action fired by keyboard as well.
158 if (StaticPrefs::layout_accessiblecaret_hide_carets_for_mouse_input() &&
159 mLastInputSource == MouseEvent_Binding::MOZ_SOURCE_KEYBOARD &&
160 (aReason & nsISelectionListener::SELECTALL_REASON)) {
161 HideCarets();
162 return NS_OK;
165 UpdateCarets();
166 return NS_OK;
169 void AccessibleCaretManager::HideCarets() {
170 if (mFirstCaret->IsLogicallyVisible() || mSecondCaret->IsLogicallyVisible()) {
171 AC_LOG("%s", __FUNCTION__);
172 mFirstCaret->SetAppearance(Appearance::None);
173 mSecondCaret->SetAppearance(Appearance::None);
174 mIsCaretPositionChanged = false;
175 DispatchCaretStateChangedEvent(CaretChangedReason::Visibilitychange);
179 void AccessibleCaretManager::UpdateCarets(const UpdateCaretsHintSet& aHint) {
180 if (!FlushLayout()) {
181 return;
184 mLastUpdateCaretMode = GetCaretMode();
186 switch (mLastUpdateCaretMode) {
187 case CaretMode::None:
188 HideCarets();
189 break;
190 case CaretMode::Cursor:
191 UpdateCaretsForCursorMode(aHint);
192 break;
193 case CaretMode::Selection:
194 UpdateCaretsForSelectionMode(aHint);
195 break;
198 UpdateShouldDisableApz();
201 bool AccessibleCaretManager::IsCaretDisplayableInCursorMode(
202 nsIFrame** aOutFrame, int32_t* aOutOffset) const {
203 RefPtr<nsCaret> caret = mPresShell->GetCaret();
204 if (!caret || !caret->IsVisible()) {
205 return false;
208 int32_t offset = 0;
209 nsIFrame* frame =
210 nsCaret::GetFrameAndOffset(GetSelection(), nullptr, 0, &offset);
212 if (!frame) {
213 return false;
216 if (!GetEditingHostForFrame(frame)) {
217 return false;
220 if (aOutFrame) {
221 *aOutFrame = frame;
224 if (aOutOffset) {
225 *aOutOffset = offset;
228 return true;
231 bool AccessibleCaretManager::HasNonEmptyTextContent(nsINode* aNode) const {
232 return nsContentUtils::HasNonEmptyTextContent(
233 aNode, nsContentUtils::eRecurseIntoChildren);
236 void AccessibleCaretManager::UpdateCaretsForCursorMode(
237 const UpdateCaretsHintSet& aHints) {
238 AC_LOG("%s, selection: %p", __FUNCTION__, GetSelection());
240 int32_t offset = 0;
241 nsIFrame* frame = nullptr;
242 if (!IsCaretDisplayableInCursorMode(&frame, &offset)) {
243 HideCarets();
244 return;
247 PositionChangedResult result = mFirstCaret->SetPosition(frame, offset);
249 switch (result) {
250 case PositionChangedResult::NotChanged:
251 case PositionChangedResult::Position:
252 case PositionChangedResult::Zoom:
253 if (!aHints.contains(UpdateCaretsHint::RespectOldAppearance)) {
254 if (HasNonEmptyTextContent(GetEditingHostForFrame(frame))) {
255 mFirstCaret->SetAppearance(Appearance::Normal);
256 } else if (
257 StaticPrefs::
258 layout_accessiblecaret_caret_shown_when_long_tapping_on_empty_content()) {
259 if (mFirstCaret->IsLogicallyVisible()) {
260 // Possible cases are: 1) SelectWordOrShortcut() sets the
261 // appearance to Normal. 2) When the caret is out of viewport and
262 // now scrolling into viewport, it has appearance NormalNotShown.
263 mFirstCaret->SetAppearance(Appearance::Normal);
264 } else {
265 // Possible cases are: a) Single tap on current empty content;
266 // OnSelectionChanged() sets the appearance to None due to
267 // MOUSEDOWN_REASON. b) Single tap on other empty content;
268 // OnBlur() sets the appearance to None.
270 // Do nothing to make the appearance remains None so that it can
271 // be distinguished from case 2). Also do not set the appearance
272 // to NormalNotShown here like the default update behavior.
274 } else {
275 mFirstCaret->SetAppearance(Appearance::NormalNotShown);
278 break;
280 case PositionChangedResult::Invisible:
281 mFirstCaret->SetAppearance(Appearance::NormalNotShown);
282 break;
285 mSecondCaret->SetAppearance(Appearance::None);
287 mIsCaretPositionChanged = (result == PositionChangedResult::Position);
289 if (!aHints.contains(UpdateCaretsHint::DispatchNoEvent) && !mActiveCaret) {
290 DispatchCaretStateChangedEvent(CaretChangedReason::Updateposition);
294 void AccessibleCaretManager::UpdateCaretsForSelectionMode(
295 const UpdateCaretsHintSet& aHints) {
296 AC_LOG("%s: selection: %p", __FUNCTION__, GetSelection());
298 int32_t startOffset = 0;
299 nsIFrame* startFrame =
300 GetFrameForFirstRangeStartOrLastRangeEnd(eDirNext, &startOffset);
302 int32_t endOffset = 0;
303 nsIFrame* endFrame =
304 GetFrameForFirstRangeStartOrLastRangeEnd(eDirPrevious, &endOffset);
306 if (!CompareTreePosition(startFrame, endFrame)) {
307 // XXX: Do we really have to hide carets if this condition isn't satisfied?
308 HideCarets();
309 return;
312 auto updateSingleCaret = [aHints](AccessibleCaret* aCaret, nsIFrame* aFrame,
313 int32_t aOffset) -> PositionChangedResult {
314 PositionChangedResult result = aCaret->SetPosition(aFrame, aOffset);
316 switch (result) {
317 case PositionChangedResult::NotChanged:
318 case PositionChangedResult::Position:
319 case PositionChangedResult::Zoom:
320 if (!aHints.contains(UpdateCaretsHint::RespectOldAppearance)) {
321 aCaret->SetAppearance(Appearance::Normal);
323 break;
325 case PositionChangedResult::Invisible:
326 aCaret->SetAppearance(Appearance::NormalNotShown);
327 break;
329 return result;
332 PositionChangedResult firstCaretResult =
333 updateSingleCaret(mFirstCaret.get(), startFrame, startOffset);
334 PositionChangedResult secondCaretResult =
335 updateSingleCaret(mSecondCaret.get(), endFrame, endOffset);
337 mIsCaretPositionChanged =
338 firstCaretResult == PositionChangedResult::Position ||
339 secondCaretResult == PositionChangedResult::Position;
341 if (mIsCaretPositionChanged) {
342 // Flush layout to make the carets intersection correct.
343 if (!FlushLayout()) {
344 return;
348 if (!aHints.contains(UpdateCaretsHint::RespectOldAppearance)) {
349 // Only check for tilt carets when the caller doesn't ask us to preserve
350 // old appearance. Otherwise we might override the appearance set by the
351 // caller.
352 if (StaticPrefs::layout_accessiblecaret_always_tilt()) {
353 UpdateCaretsForAlwaysTilt(startFrame, endFrame);
354 } else {
355 UpdateCaretsForOverlappingTilt();
359 if (!aHints.contains(UpdateCaretsHint::DispatchNoEvent) && !mActiveCaret) {
360 DispatchCaretStateChangedEvent(CaretChangedReason::Updateposition);
364 void AccessibleCaretManager::UpdateShouldDisableApz() {
365 if (mActiveCaret) {
366 // No need to disable APZ when dragging the caret.
367 mShouldDisableApz = false;
368 return;
371 if (mIsScrollStarted) {
372 // During scrolling, the caret's position is changed only if it is in a
373 // position:fixed or a "stuck" position:sticky frame subtree.
374 mShouldDisableApz = mIsCaretPositionChanged;
375 return;
378 // For other cases, we can only reliably detect whether the caret is in a
379 // position:fixed frame subtree.
380 switch (mLastUpdateCaretMode) {
381 case CaretMode::None:
382 mShouldDisableApz = false;
383 break;
384 case CaretMode::Cursor:
385 mShouldDisableApz = mFirstCaret->IsVisuallyVisible() &&
386 mFirstCaret->IsInPositionFixedSubtree();
387 break;
388 case CaretMode::Selection:
389 mShouldDisableApz = (mFirstCaret->IsVisuallyVisible() &&
390 mFirstCaret->IsInPositionFixedSubtree()) ||
391 (mSecondCaret->IsVisuallyVisible() &&
392 mSecondCaret->IsInPositionFixedSubtree());
393 break;
397 bool AccessibleCaretManager::UpdateCaretsForOverlappingTilt() {
398 if (!mFirstCaret->IsVisuallyVisible() || !mSecondCaret->IsVisuallyVisible()) {
399 return false;
402 if (!mFirstCaret->Intersects(*mSecondCaret)) {
403 mFirstCaret->SetAppearance(Appearance::Normal);
404 mSecondCaret->SetAppearance(Appearance::Normal);
405 return false;
408 if (mFirstCaret->LogicalPosition().x <= mSecondCaret->LogicalPosition().x) {
409 mFirstCaret->SetAppearance(Appearance::Left);
410 mSecondCaret->SetAppearance(Appearance::Right);
411 } else {
412 mFirstCaret->SetAppearance(Appearance::Right);
413 mSecondCaret->SetAppearance(Appearance::Left);
416 return true;
419 void AccessibleCaretManager::UpdateCaretsForAlwaysTilt(nsIFrame* aStartFrame,
420 nsIFrame* aEndFrame) {
421 // When a short LTR word in RTL environment is selected, the two carets
422 // tilted inward might be overlapped. Make them tilt outward.
423 if (UpdateCaretsForOverlappingTilt()) {
424 return;
427 if (mFirstCaret->IsVisuallyVisible()) {
428 auto startFrameWritingMode = aStartFrame->GetWritingMode();
429 mFirstCaret->SetAppearance(startFrameWritingMode.IsBidiLTR()
430 ? Appearance::Left
431 : Appearance::Right);
433 if (mSecondCaret->IsVisuallyVisible()) {
434 auto endFrameWritingMode = aEndFrame->GetWritingMode();
435 mSecondCaret->SetAppearance(
436 endFrameWritingMode.IsBidiLTR() ? Appearance::Right : Appearance::Left);
440 void AccessibleCaretManager::ProvideHapticFeedback() {
441 if (StaticPrefs::layout_accessiblecaret_hapticfeedback()) {
442 nsCOMPtr<nsIHapticFeedback> haptic =
443 do_GetService("@mozilla.org/widget/hapticfeedback;1");
444 haptic->PerformSimpleAction(haptic->LongPress);
448 nsresult AccessibleCaretManager::PressCaret(const nsPoint& aPoint,
449 EventClassID aEventClass) {
450 nsresult rv = NS_ERROR_FAILURE;
452 MOZ_ASSERT(aEventClass == eMouseEventClass || aEventClass == eTouchEventClass,
453 "Unexpected event class!");
455 using TouchArea = AccessibleCaret::TouchArea;
456 TouchArea touchArea =
457 aEventClass == eMouseEventClass ? TouchArea::CaretImage : TouchArea::Full;
459 if (mFirstCaret->Contains(aPoint, touchArea)) {
460 mActiveCaret = mFirstCaret.get();
461 SetSelectionDirection(eDirPrevious);
462 } else if (mSecondCaret->Contains(aPoint, touchArea)) {
463 mActiveCaret = mSecondCaret.get();
464 SetSelectionDirection(eDirNext);
467 if (mActiveCaret) {
468 mOffsetYToCaretLogicalPosition =
469 mActiveCaret->LogicalPosition().y - aPoint.y;
470 SetSelectionDragState(true);
471 DispatchCaretStateChangedEvent(CaretChangedReason::Presscaret);
472 rv = NS_OK;
475 return rv;
478 nsresult AccessibleCaretManager::DragCaret(const nsPoint& aPoint) {
479 MOZ_ASSERT(mActiveCaret);
480 MOZ_ASSERT(GetCaretMode() != CaretMode::None);
482 if (!mPresShell || !mPresShell->GetRootFrame() || !GetSelection()) {
483 return NS_ERROR_NULL_POINTER;
486 StopSelectionAutoScrollTimer();
487 DragCaretInternal(aPoint);
489 // We want to scroll the page even if we failed to drag the caret.
490 StartSelectionAutoScrollTimer(aPoint);
491 UpdateCarets();
492 return NS_OK;
495 nsresult AccessibleCaretManager::ReleaseCaret() {
496 MOZ_ASSERT(mActiveCaret);
498 mActiveCaret = nullptr;
499 SetSelectionDragState(false);
500 UpdateShouldDisableApz();
501 DispatchCaretStateChangedEvent(CaretChangedReason::Releasecaret);
502 return NS_OK;
505 nsresult AccessibleCaretManager::TapCaret(const nsPoint& aPoint) {
506 MOZ_ASSERT(GetCaretMode() != CaretMode::None);
508 nsresult rv = NS_ERROR_FAILURE;
510 if (GetCaretMode() == CaretMode::Cursor) {
511 DispatchCaretStateChangedEvent(CaretChangedReason::Taponcaret);
512 rv = NS_OK;
515 return rv;
518 static EnumSet<nsLayoutUtils::FrameForPointOption> GetHitTestOptions() {
519 EnumSet<nsLayoutUtils::FrameForPointOption> options = {
520 nsLayoutUtils::FrameForPointOption::IgnorePaintSuppression,
521 nsLayoutUtils::FrameForPointOption::IgnoreCrossDoc};
522 return options;
525 nsresult AccessibleCaretManager::SelectWordOrShortcut(const nsPoint& aPoint) {
526 // If the long-tap is landing on a pre-existing selection, don't replace
527 // it with a new one. Instead just return and let the context menu pop up
528 // on the pre-existing selection.
529 if (GetCaretMode() == CaretMode::Selection &&
530 GetSelection()->ContainsPoint(aPoint)) {
531 AC_LOG("%s: UpdateCarets() for current selection", __FUNCTION__);
532 UpdateCarets();
533 ProvideHapticFeedback();
534 return NS_OK;
537 if (!mPresShell) {
538 return NS_ERROR_UNEXPECTED;
541 nsIFrame* rootFrame = mPresShell->GetRootFrame();
542 if (!rootFrame) {
543 return NS_ERROR_NOT_AVAILABLE;
546 // Find the frame under point.
547 AutoWeakFrame ptFrame = nsLayoutUtils::GetFrameForPoint(
548 RelativeTo{rootFrame}, aPoint, GetHitTestOptions());
549 if (!ptFrame.GetFrame()) {
550 return NS_ERROR_FAILURE;
553 nsIFrame* focusableFrame = GetFocusableFrame(ptFrame);
555 #ifdef DEBUG_FRAME_DUMP
556 AC_LOG("%s: Found %s under (%d, %d)", __FUNCTION__, ptFrame->ListTag().get(),
557 aPoint.x, aPoint.y);
558 AC_LOG("%s: Found %s focusable", __FUNCTION__,
559 focusableFrame ? focusableFrame->ListTag().get() : "no frame");
560 #endif
562 // Get ptInFrame here so that we don't need to check whether rootFrame is
563 // alive later. Note that if ptFrame is being moved by
564 // IMEStateManager::NotifyIME() or ChangeFocusToOrClearOldFocus() below,
565 // something under the original point will be selected, which may not be the
566 // original text the user wants to select.
567 nsPoint ptInFrame = aPoint;
568 nsLayoutUtils::TransformPoint(RelativeTo{rootFrame}, RelativeTo{ptFrame},
569 ptInFrame);
571 // Firstly check long press on an empty editable content.
572 Element* newFocusEditingHost = GetEditingHostForFrame(ptFrame);
573 if (focusableFrame && newFocusEditingHost &&
574 !HasNonEmptyTextContent(newFocusEditingHost)) {
575 ChangeFocusToOrClearOldFocus(focusableFrame);
577 if (StaticPrefs::
578 layout_accessiblecaret_caret_shown_when_long_tapping_on_empty_content()) {
579 mFirstCaret->SetAppearance(Appearance::Normal);
581 // We need to update carets to get correct information before dispatching
582 // CaretStateChangedEvent.
583 UpdateCarets();
584 ProvideHapticFeedback();
585 DispatchCaretStateChangedEvent(CaretChangedReason::Longpressonemptycontent);
586 return NS_OK;
589 bool selectable = ptFrame->IsSelectable(nullptr);
591 #ifdef DEBUG_FRAME_DUMP
592 AC_LOG("%s: %s %s selectable.", __FUNCTION__, ptFrame->ListTag().get(),
593 selectable ? "is" : "is NOT");
594 #endif
596 if (!selectable) {
597 return NS_ERROR_FAILURE;
600 // Commit the composition string of the old editable focus element (if there
601 // is any) before changing the focus.
602 IMEStateManager::NotifyIME(widget::REQUEST_TO_COMMIT_COMPOSITION,
603 mPresShell->GetPresContext());
604 if (!ptFrame.IsAlive()) {
605 // Cannot continue because ptFrame died.
606 return NS_ERROR_FAILURE;
609 // ptFrame is selectable. Now change the focus.
610 ChangeFocusToOrClearOldFocus(focusableFrame);
611 if (!ptFrame.IsAlive()) {
612 // Cannot continue because ptFrame died.
613 return NS_ERROR_FAILURE;
616 // If long tap point isn't selectable frame for caret and frame selection
617 // can find a better frame for caret, we don't select a word.
618 // See https://webcompat.com/issues/15953
619 nsIFrame::ContentOffsets offsets =
620 ptFrame->GetContentOffsetsFromPoint(ptInFrame, nsIFrame::SKIP_HIDDEN);
621 if (offsets.content) {
622 RefPtr<nsFrameSelection> frameSelection = GetFrameSelection();
623 if (frameSelection) {
624 int32_t offset;
625 nsIFrame* theFrame = nsFrameSelection::GetFrameForNodeOffset(
626 offsets.content, offsets.offset, offsets.associate, &offset);
627 if (theFrame && theFrame != ptFrame) {
628 SetSelectionDragState(true);
629 frameSelection->HandleClick(
630 offsets.content, offsets.StartOffset(), offsets.EndOffset(),
631 nsFrameSelection::FocusMode::kCollapseToNewPoint,
632 offsets.associate);
633 SetSelectionDragState(false);
634 ClearMaintainedSelection();
636 if (StaticPrefs::
637 layout_accessiblecaret_caret_shown_when_long_tapping_on_empty_content()) {
638 mFirstCaret->SetAppearance(Appearance::Normal);
641 UpdateCarets();
642 ProvideHapticFeedback();
643 DispatchCaretStateChangedEvent(
644 CaretChangedReason::Longpressonemptycontent);
646 return NS_OK;
651 // Then try select a word under point.
652 nsresult rv = SelectWord(ptFrame, ptInFrame);
653 UpdateCarets();
654 ProvideHapticFeedback();
656 return rv;
659 void AccessibleCaretManager::OnScrollStart() {
660 AC_LOG("%s", __FUNCTION__);
662 AutoRestore<bool> saveAllowFlushingLayout(mAllowFlushingLayout);
663 mAllowFlushingLayout = false;
665 Maybe<PresShell::AutoAssertNoFlush> assert;
666 if (mPresShell) {
667 assert.emplace(*mPresShell);
670 mIsScrollStarted = true;
672 if (mFirstCaret->IsLogicallyVisible() || mSecondCaret->IsLogicallyVisible()) {
673 // Dispatch the event only if one of the carets is logically visible like in
674 // HideCarets().
675 DispatchCaretStateChangedEvent(CaretChangedReason::Scroll);
679 void AccessibleCaretManager::OnScrollEnd() {
680 if (mLastUpdateCaretMode != GetCaretMode()) {
681 return;
684 AutoRestore<bool> saveAllowFlushingLayout(mAllowFlushingLayout);
685 mAllowFlushingLayout = false;
687 Maybe<PresShell::AutoAssertNoFlush> assert;
688 if (mPresShell) {
689 assert.emplace(*mPresShell);
692 mIsScrollStarted = false;
694 if (GetCaretMode() == CaretMode::Cursor) {
695 if (!mFirstCaret->IsLogicallyVisible()) {
696 // If the caret is hidden (Appearance::None) due to blur, no
697 // need to update it.
698 return;
702 // For mouse input we don't want to show the carets.
703 if (StaticPrefs::layout_accessiblecaret_hide_carets_for_mouse_input() &&
704 mLastInputSource == MouseEvent_Binding::MOZ_SOURCE_MOUSE) {
705 AC_LOG("%s: HideCarets()", __FUNCTION__);
706 HideCarets();
707 return;
710 AC_LOG("%s: UpdateCarets()", __FUNCTION__);
711 UpdateCarets();
714 void AccessibleCaretManager::OnScrollPositionChanged() {
715 if (mLastUpdateCaretMode != GetCaretMode()) {
716 return;
719 AutoRestore<bool> saveAllowFlushingLayout(mAllowFlushingLayout);
720 mAllowFlushingLayout = false;
722 Maybe<PresShell::AutoAssertNoFlush> assert;
723 if (mPresShell) {
724 assert.emplace(*mPresShell);
727 if (mFirstCaret->IsLogicallyVisible() || mSecondCaret->IsLogicallyVisible()) {
728 if (mIsScrollStarted) {
729 // We don't want extra CaretStateChangedEvents dispatched when user is
730 // scrolling the page.
731 AC_LOG("%s: UpdateCarets(RespectOldAppearance | DispatchNoEvent)",
732 __FUNCTION__);
733 UpdateCarets({UpdateCaretsHint::RespectOldAppearance,
734 UpdateCaretsHint::DispatchNoEvent});
735 } else {
736 AC_LOG("%s: UpdateCarets(RespectOldAppearance)", __FUNCTION__);
737 UpdateCarets(UpdateCaretsHint::RespectOldAppearance);
742 void AccessibleCaretManager::OnReflow() {
743 if (mLastUpdateCaretMode != GetCaretMode()) {
744 return;
747 AutoRestore<bool> saveAllowFlushingLayout(mAllowFlushingLayout);
748 mAllowFlushingLayout = false;
750 Maybe<PresShell::AutoAssertNoFlush> assert;
751 if (mPresShell) {
752 assert.emplace(*mPresShell);
755 if (mFirstCaret->IsLogicallyVisible() || mSecondCaret->IsLogicallyVisible()) {
756 AC_LOG("%s: UpdateCarets(RespectOldAppearance)", __FUNCTION__);
757 UpdateCarets(UpdateCaretsHint::RespectOldAppearance);
761 void AccessibleCaretManager::OnBlur() {
762 AC_LOG("%s: HideCarets()", __FUNCTION__);
763 HideCarets();
766 void AccessibleCaretManager::OnKeyboardEvent() {
767 if (GetCaretMode() == CaretMode::Cursor) {
768 AC_LOG("%s: HideCarets()", __FUNCTION__);
769 HideCarets();
773 void AccessibleCaretManager::OnFrameReconstruction() {
774 mFirstCaret->EnsureApzAware();
775 mSecondCaret->EnsureApzAware();
778 void AccessibleCaretManager::SetLastInputSource(uint16_t aInputSource) {
779 mLastInputSource = aInputSource;
782 Selection* AccessibleCaretManager::GetSelection() const {
783 RefPtr<nsFrameSelection> fs = GetFrameSelection();
784 if (!fs) {
785 return nullptr;
787 return fs->GetSelection(SelectionType::eNormal);
790 already_AddRefed<nsFrameSelection> AccessibleCaretManager::GetFrameSelection()
791 const {
792 if (!mPresShell) {
793 return nullptr;
796 nsFocusManager* fm = nsFocusManager::GetFocusManager();
797 MOZ_ASSERT(fm);
799 nsIContent* focusedContent = fm->GetFocusedElement();
800 if (!focusedContent) {
801 // For non-editable content
802 return mPresShell->FrameSelection();
805 nsIFrame* focusFrame = focusedContent->GetPrimaryFrame();
806 if (!focusFrame) {
807 return nullptr;
810 // Prevent us from touching the nsFrameSelection associated with other
811 // PresShell.
812 RefPtr<nsFrameSelection> fs = focusFrame->GetFrameSelection();
813 if (!fs || fs->GetPresShell() != mPresShell) {
814 return nullptr;
817 return fs.forget();
820 nsAutoString AccessibleCaretManager::StringifiedSelection() const {
821 nsAutoString str;
822 RefPtr<Selection> selection = GetSelection();
823 if (selection) {
824 selection->Stringify(str, mAllowFlushingLayout
825 ? Selection::FlushFrames::Yes
826 : Selection::FlushFrames::No);
828 return str;
831 Element* AccessibleCaretManager::GetEditingHostForFrame(
832 nsIFrame* aFrame) const {
833 if (!aFrame) {
834 return nullptr;
837 auto content = aFrame->GetContent();
838 if (!content) {
839 return nullptr;
842 return content->GetEditingHost();
845 AccessibleCaretManager::CaretMode AccessibleCaretManager::GetCaretMode() const {
846 Selection* selection = GetSelection();
847 if (!selection) {
848 return CaretMode::None;
851 uint32_t rangeCount = selection->RangeCount();
852 if (rangeCount <= 0) {
853 return CaretMode::None;
856 nsFocusManager* fm = nsFocusManager::GetFocusManager();
857 MOZ_ASSERT(fm);
858 if (fm->GetFocusedWindow() != mPresShell->GetDocument()->GetWindow()) {
859 // Hide carets if the window is not focused.
860 return CaretMode::None;
863 if (selection->IsCollapsed()) {
864 return CaretMode::Cursor;
867 return CaretMode::Selection;
870 nsIFrame* AccessibleCaretManager::GetFocusableFrame(nsIFrame* aFrame) const {
871 // This implementation is similar to EventStateManager::PostHandleEvent().
872 // Look for the nearest enclosing focusable frame.
873 nsIFrame* focusableFrame = aFrame;
874 while (focusableFrame) {
875 if (focusableFrame->IsFocusable(nullptr, true)) {
876 break;
878 focusableFrame = focusableFrame->GetParent();
880 return focusableFrame;
883 void AccessibleCaretManager::ChangeFocusToOrClearOldFocus(
884 nsIFrame* aFrame) const {
885 nsFocusManager* fm = nsFocusManager::GetFocusManager();
886 MOZ_ASSERT(fm);
888 if (aFrame) {
889 nsIContent* focusableContent = aFrame->GetContent();
890 MOZ_ASSERT(focusableContent, "Focusable frame must have content!");
891 RefPtr<Element> focusableElement = Element::FromNode(focusableContent);
892 fm->SetFocus(focusableElement, nsIFocusManager::FLAG_BYLONGPRESS);
893 } else {
894 nsPIDOMWindowOuter* win = mPresShell->GetDocument()->GetWindow();
895 if (win) {
896 fm->ClearFocus(win);
897 fm->SetFocusedWindow(win);
902 nsresult AccessibleCaretManager::SelectWord(nsIFrame* aFrame,
903 const nsPoint& aPoint) const {
904 SetSelectionDragState(true);
905 nsresult rs = aFrame->SelectByTypeAtPoint(
906 mPresShell->GetPresContext(), aPoint, eSelectWord, eSelectWord, 0);
908 SetSelectionDragState(false);
909 ClearMaintainedSelection();
911 // Smart-select phone numbers if possible.
912 if (StaticPrefs::layout_accessiblecaret_extend_selection_for_phone_number()) {
913 SelectMoreIfPhoneNumber();
916 return rs;
919 void AccessibleCaretManager::SetSelectionDragState(bool aState) const {
920 RefPtr<nsFrameSelection> fs = GetFrameSelection();
921 if (fs) {
922 fs->SetDragState(aState);
926 bool AccessibleCaretManager::IsPhoneNumber(nsAString& aCandidate) const {
927 RefPtr<Document> doc = mPresShell->GetDocument();
928 nsAutoString phoneNumberRegex(u"(^\\+)?[0-9 ,\\-.()*#pw]{1,30}$"_ns);
929 return nsContentUtils::IsPatternMatching(aCandidate, phoneNumberRegex, doc)
930 .valueOr(false);
933 void AccessibleCaretManager::SelectMoreIfPhoneNumber() const {
934 nsAutoString selectedText = StringifiedSelection();
936 if (IsPhoneNumber(selectedText)) {
937 SetSelectionDirection(eDirNext);
938 ExtendPhoneNumberSelection(u"forward"_ns);
940 SetSelectionDirection(eDirPrevious);
941 ExtendPhoneNumberSelection(u"backward"_ns);
943 SetSelectionDirection(eDirNext);
947 void AccessibleCaretManager::ExtendPhoneNumberSelection(
948 const nsAString& aDirection) const {
949 if (!mPresShell) {
950 return;
953 // Extend the phone number selection until we find a boundary.
954 RefPtr<Selection> selection = GetSelection();
956 while (selection) {
957 const nsRange* anchorFocusRange = selection->GetAnchorFocusRange();
958 if (!anchorFocusRange) {
959 return;
962 // Backup the anchor focus range since both anchor node and focus node might
963 // be changed after calling Selection::Modify().
964 RefPtr<nsRange> oldAnchorFocusRange = anchorFocusRange->CloneRange();
966 // Save current focus node, focus offset and the selected text so that
967 // we can compare them with the modified ones later.
968 nsINode* oldFocusNode = selection->GetFocusNode();
969 uint32_t oldFocusOffset = selection->FocusOffset();
970 nsAutoString oldSelectedText = StringifiedSelection();
972 // Extend the selection by one char.
973 selection->Modify(u"extend"_ns, aDirection, u"character"_ns,
974 IgnoreErrors());
975 if (IsTerminated()) {
976 return;
979 // If the selection didn't change, (can't extend further), we're done.
980 if (selection->GetFocusNode() == oldFocusNode &&
981 selection->FocusOffset() == oldFocusOffset) {
982 return;
985 // If the changed selection isn't a valid phone number, we're done.
986 // Also, if the selection was extended to a new block node, the string
987 // returned by stringify() won't have a new line at the beginning or the
988 // end of the string. Therefore, if either focus node or offset is
989 // changed, but selected text is not changed, we're done, too.
990 nsAutoString selectedText = StringifiedSelection();
992 if (!IsPhoneNumber(selectedText) || oldSelectedText == selectedText) {
993 // Backout the undesired selection extend, restore the old anchor focus
994 // range before exit.
995 selection->SetAnchorFocusToRange(oldAnchorFocusRange);
996 return;
1001 void AccessibleCaretManager::SetSelectionDirection(nsDirection aDir) const {
1002 Selection* selection = GetSelection();
1003 if (selection) {
1004 selection->AdjustAnchorFocusForMultiRange(aDir);
1008 void AccessibleCaretManager::ClearMaintainedSelection() const {
1009 // Selection made by double-clicking for example will maintain the original
1010 // word selection. We should clear it so that we can drag caret freely.
1011 RefPtr<nsFrameSelection> fs = GetFrameSelection();
1012 if (fs) {
1013 fs->MaintainSelection(eSelectNoAmount);
1017 bool AccessibleCaretManager::FlushLayout() {
1018 if (mPresShell && mAllowFlushingLayout) {
1019 AutoRestore<bool> flushing(mFlushingLayout);
1020 mFlushingLayout = true;
1022 if (Document* doc = mPresShell->GetDocument()) {
1023 doc->FlushPendingNotifications(FlushType::Layout);
1027 return !IsTerminated();
1030 nsIFrame* AccessibleCaretManager::GetFrameForFirstRangeStartOrLastRangeEnd(
1031 nsDirection aDirection, int32_t* aOutOffset, nsIContent** aOutContent,
1032 int32_t* aOutContentOffset) const {
1033 if (!mPresShell) {
1034 return nullptr;
1037 MOZ_ASSERT(GetCaretMode() == CaretMode::Selection);
1038 MOZ_ASSERT(aOutOffset, "aOutOffset shouldn't be nullptr!");
1040 const nsRange* range = nullptr;
1041 RefPtr<nsINode> startNode;
1042 RefPtr<nsINode> endNode;
1043 int32_t nodeOffset = 0;
1044 CaretAssociationHint hint;
1046 RefPtr<Selection> selection = GetSelection();
1047 bool findInFirstRangeStart = aDirection == eDirNext;
1049 if (findInFirstRangeStart) {
1050 range = selection->GetRangeAt(0);
1051 startNode = range->GetStartContainer();
1052 endNode = range->GetEndContainer();
1053 nodeOffset = range->StartOffset();
1054 hint = CARET_ASSOCIATE_AFTER;
1055 } else {
1056 range = selection->GetRangeAt(selection->RangeCount() - 1);
1057 startNode = range->GetEndContainer();
1058 endNode = range->GetStartContainer();
1059 nodeOffset = range->EndOffset();
1060 hint = CARET_ASSOCIATE_BEFORE;
1063 nsCOMPtr<nsIContent> startContent = do_QueryInterface(startNode);
1064 nsIFrame* startFrame = nsFrameSelection::GetFrameForNodeOffset(
1065 startContent, nodeOffset, hint, aOutOffset);
1067 if (!startFrame) {
1068 ErrorResult err;
1069 RefPtr<TreeWalker> walker = mPresShell->GetDocument()->CreateTreeWalker(
1070 *startNode, dom::NodeFilter_Binding::SHOW_ALL, nullptr, err);
1072 if (!walker) {
1073 return nullptr;
1076 startFrame = startContent ? startContent->GetPrimaryFrame() : nullptr;
1077 while (!startFrame && startNode != endNode) {
1078 startNode = findInFirstRangeStart ? walker->NextNode(err)
1079 : walker->PreviousNode(err);
1081 if (!startNode) {
1082 break;
1085 startContent = startNode->AsContent();
1086 startFrame = startContent ? startContent->GetPrimaryFrame() : nullptr;
1089 // We are walking among the nodes in the content tree, so the node offset
1090 // relative to startNode should be set to 0.
1091 nodeOffset = 0;
1092 *aOutOffset = 0;
1095 if (startFrame) {
1096 if (aOutContent) {
1097 startContent.forget(aOutContent);
1099 if (aOutContentOffset) {
1100 *aOutContentOffset = nodeOffset;
1104 return startFrame;
1107 bool AccessibleCaretManager::RestrictCaretDraggingOffsets(
1108 nsIFrame::ContentOffsets& aOffsets) {
1109 if (!mPresShell) {
1110 return false;
1113 MOZ_ASSERT(GetCaretMode() == CaretMode::Selection);
1115 nsDirection dir = mActiveCaret == mFirstCaret.get() ? eDirPrevious : eDirNext;
1116 int32_t offset = 0;
1117 nsCOMPtr<nsIContent> content;
1118 int32_t contentOffset = 0;
1119 nsIFrame* frame = GetFrameForFirstRangeStartOrLastRangeEnd(
1120 dir, &offset, getter_AddRefs(content), &contentOffset);
1122 if (!frame) {
1123 return false;
1126 // Compare the active caret's new position (aOffsets) to the inactive caret's
1127 // position.
1128 const Maybe<int32_t> cmpToInactiveCaretPos = nsContentUtils::ComparePoints(
1129 aOffsets.content, aOffsets.StartOffset(), content, contentOffset);
1130 if (NS_WARN_IF(!cmpToInactiveCaretPos)) {
1131 // Potentially handle this properly when Selection across Shadow DOM
1132 // boundary is implemented
1133 // (https://bugzilla.mozilla.org/show_bug.cgi?id=1607497).
1134 return false;
1137 // Move one character (in the direction of dir) from the inactive caret's
1138 // position. This is the limit for the active caret's new position.
1139 nsPeekOffsetStruct limit(eSelectCluster, dir, offset, nsPoint(0, 0), true,
1140 true, false, false, false);
1141 nsresult rv = frame->PeekOffset(&limit);
1142 if (NS_FAILED(rv)) {
1143 limit.mResultContent = content;
1144 limit.mContentOffset = contentOffset;
1147 // Compare the active caret's new position (aOffsets) to the limit.
1148 const Maybe<int32_t> cmpToLimit =
1149 nsContentUtils::ComparePoints(aOffsets.content, aOffsets.StartOffset(),
1150 limit.mResultContent, limit.mContentOffset);
1151 if (NS_WARN_IF(!cmpToLimit)) {
1152 // Potentially handle this properly when Selection across Shadow DOM
1153 // boundary is implemented
1154 // (https://bugzilla.mozilla.org/show_bug.cgi?id=1607497).
1155 return false;
1158 auto SetOffsetsToLimit = [&aOffsets, &limit]() {
1159 aOffsets.content = limit.mResultContent;
1160 aOffsets.offset = limit.mContentOffset;
1161 aOffsets.secondaryOffset = limit.mContentOffset;
1164 if (!StaticPrefs::
1165 layout_accessiblecaret_allow_dragging_across_other_caret()) {
1166 if ((mActiveCaret == mFirstCaret.get() && *cmpToLimit == 1) ||
1167 (mActiveCaret == mSecondCaret.get() && *cmpToLimit == -1)) {
1168 // The active caret's position is past the limit, which we don't allow
1169 // here. So set it to the limit, resulting in one character being
1170 // selected.
1171 SetOffsetsToLimit();
1173 } else {
1174 switch (*cmpToInactiveCaretPos) {
1175 case 0:
1176 // The active caret's position is the same as the position of the
1177 // inactive caret. So set it to the limit to prevent the selection from
1178 // being collapsed, resulting in one character being selected.
1179 SetOffsetsToLimit();
1180 break;
1181 case 1:
1182 if (mActiveCaret == mFirstCaret.get()) {
1183 // First caret was moved across the second caret. After making change
1184 // to the selection, the user will drag the second caret.
1185 mActiveCaret = mSecondCaret.get();
1187 break;
1188 case -1:
1189 if (mActiveCaret == mSecondCaret.get()) {
1190 // Second caret was moved across the first caret. After making change
1191 // to the selection, the user will drag the first caret.
1192 mActiveCaret = mFirstCaret.get();
1194 break;
1198 return true;
1201 bool AccessibleCaretManager::CompareTreePosition(nsIFrame* aStartFrame,
1202 nsIFrame* aEndFrame) const {
1203 return (aStartFrame && aEndFrame &&
1204 nsLayoutUtils::CompareTreePosition(aStartFrame, aEndFrame) <= 0);
1207 nsresult AccessibleCaretManager::DragCaretInternal(const nsPoint& aPoint) {
1208 MOZ_ASSERT(mPresShell);
1210 nsIFrame* rootFrame = mPresShell->GetRootFrame();
1211 MOZ_ASSERT(rootFrame, "We need root frame to compute caret dragging!");
1213 nsPoint point = AdjustDragBoundary(
1214 nsPoint(aPoint.x, aPoint.y + mOffsetYToCaretLogicalPosition));
1216 // Find out which content we point to
1218 nsIFrame* ptFrame = nsLayoutUtils::GetFrameForPoint(
1219 RelativeTo{rootFrame}, point, GetHitTestOptions());
1220 if (!ptFrame) {
1221 return NS_ERROR_FAILURE;
1224 RefPtr<nsFrameSelection> fs = GetFrameSelection();
1225 MOZ_ASSERT(fs);
1227 nsresult result;
1228 nsIFrame* newFrame = nullptr;
1229 nsPoint newPoint;
1230 nsPoint ptInFrame = point;
1231 nsLayoutUtils::TransformPoint(RelativeTo{rootFrame}, RelativeTo{ptFrame},
1232 ptInFrame);
1233 result = fs->ConstrainFrameAndPointToAnchorSubtree(ptFrame, ptInFrame,
1234 &newFrame, newPoint);
1235 if (NS_FAILED(result) || !newFrame) {
1236 return NS_ERROR_FAILURE;
1239 if (!newFrame->IsSelectable(nullptr)) {
1240 return NS_ERROR_FAILURE;
1243 nsIFrame::ContentOffsets offsets =
1244 newFrame->GetContentOffsetsFromPoint(newPoint);
1245 if (offsets.IsNull()) {
1246 return NS_ERROR_FAILURE;
1249 if (GetCaretMode() == CaretMode::Selection &&
1250 !RestrictCaretDraggingOffsets(offsets)) {
1251 return NS_ERROR_FAILURE;
1254 ClearMaintainedSelection();
1256 const nsFrameSelection::FocusMode focusMode =
1257 (GetCaretMode() == CaretMode::Selection)
1258 ? nsFrameSelection::FocusMode::kExtendSelection
1259 : nsFrameSelection::FocusMode::kCollapseToNewPoint;
1260 fs->HandleClick(offsets.content, offsets.StartOffset(), offsets.EndOffset(),
1261 focusMode, offsets.associate);
1262 return NS_OK;
1265 nsRect AccessibleCaretManager::GetAllChildFrameRectsUnion(
1266 nsIFrame* aFrame) const {
1267 nsRect unionRect;
1269 // Drill through scroll frames, we don't want to include scrollbar child
1270 // frames below.
1271 for (nsIFrame* frame = aFrame->GetContentInsertionFrame(); frame;
1272 frame = frame->GetNextContinuation()) {
1273 nsRect frameRect;
1275 for (const auto& childList : frame->ChildLists()) {
1276 // Loop all children to union their scrollable overflow rect.
1277 for (nsIFrame* child : childList.mList) {
1278 nsRect childRect = child->GetScrollableOverflowRectRelativeToSelf();
1279 nsLayoutUtils::TransformRect(child, frame, childRect);
1281 // A TextFrame containing only '\n' has positive height and width 0, or
1282 // positive width and height 0 if it's vertical. Need to use UnionEdges
1283 // to add its rect. BRFrame rect should be non-empty.
1284 if (childRect.IsEmpty()) {
1285 frameRect = frameRect.UnionEdges(childRect);
1286 } else {
1287 frameRect = frameRect.Union(childRect);
1292 MOZ_ASSERT(!frameRect.IsEmpty(),
1293 "Editable frames should have at least one BRFrame child to make "
1294 "frameRect non-empty!");
1295 if (frame != aFrame) {
1296 nsLayoutUtils::TransformRect(frame, aFrame, frameRect);
1298 unionRect = unionRect.Union(frameRect);
1301 return unionRect;
1304 nsPoint AccessibleCaretManager::AdjustDragBoundary(
1305 const nsPoint& aPoint) const {
1306 nsPoint adjustedPoint = aPoint;
1308 int32_t focusOffset = 0;
1309 nsIFrame* focusFrame =
1310 nsCaret::GetFrameAndOffset(GetSelection(), nullptr, 0, &focusOffset);
1311 Element* editingHost = GetEditingHostForFrame(focusFrame);
1313 if (editingHost) {
1314 nsIFrame* editingHostFrame = editingHost->GetPrimaryFrame();
1315 if (editingHostFrame) {
1316 nsRect boundary = GetAllChildFrameRectsUnion(editingHostFrame);
1317 nsLayoutUtils::TransformRect(editingHostFrame, mPresShell->GetRootFrame(),
1318 boundary);
1320 // Shrink the rect to make sure we never hit the boundary.
1321 boundary.Deflate(kBoundaryAppUnits);
1323 adjustedPoint = boundary.ClampPoint(adjustedPoint);
1327 if (GetCaretMode() == CaretMode::Selection &&
1328 !StaticPrefs::
1329 layout_accessiblecaret_allow_dragging_across_other_caret()) {
1330 // Bug 1068474: Adjust the Y-coordinate so that the carets won't be in tilt
1331 // mode when a caret is being dragged surpass the other caret.
1333 // For example, when dragging the second caret, the horizontal boundary
1334 // (lower bound) of its Y-coordinate is the logical position of the first
1335 // caret. Likewise, when dragging the first caret, the horizontal boundary
1336 // (upper bound) of its Y-coordinate is the logical position of the second
1337 // caret.
1338 if (mActiveCaret == mFirstCaret.get()) {
1339 nscoord dragDownBoundaryY = mSecondCaret->LogicalPosition().y;
1340 if (dragDownBoundaryY > 0 && adjustedPoint.y > dragDownBoundaryY) {
1341 adjustedPoint.y = dragDownBoundaryY;
1343 } else {
1344 nscoord dragUpBoundaryY = mFirstCaret->LogicalPosition().y;
1345 if (adjustedPoint.y < dragUpBoundaryY) {
1346 adjustedPoint.y = dragUpBoundaryY;
1351 return adjustedPoint;
1354 void AccessibleCaretManager::StartSelectionAutoScrollTimer(
1355 const nsPoint& aPoint) const {
1356 Selection* selection = GetSelection();
1357 MOZ_ASSERT(selection);
1359 nsIFrame* anchorFrame = selection->GetPrimaryFrameForAnchorNode();
1360 if (!anchorFrame) {
1361 return;
1364 nsIScrollableFrame* scrollFrame = nsLayoutUtils::GetNearestScrollableFrame(
1365 anchorFrame, nsLayoutUtils::SCROLLABLE_SAME_DOC |
1366 nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN);
1367 if (!scrollFrame) {
1368 return;
1371 nsIFrame* capturingFrame = scrollFrame->GetScrolledFrame();
1372 if (!capturingFrame) {
1373 return;
1376 nsIFrame* rootFrame = mPresShell->GetRootFrame();
1377 MOZ_ASSERT(rootFrame);
1378 nsPoint ptInScrolled = aPoint;
1379 nsLayoutUtils::TransformPoint(RelativeTo{rootFrame},
1380 RelativeTo{capturingFrame}, ptInScrolled);
1382 RefPtr<nsFrameSelection> fs = GetFrameSelection();
1383 MOZ_ASSERT(fs);
1384 fs->StartAutoScrollTimer(capturingFrame, ptInScrolled, kAutoScrollTimerDelay);
1387 void AccessibleCaretManager::StopSelectionAutoScrollTimer() const {
1388 RefPtr<nsFrameSelection> fs = GetFrameSelection();
1389 MOZ_ASSERT(fs);
1390 fs->StopAutoScrollTimer();
1393 void AccessibleCaretManager::DispatchCaretStateChangedEvent(
1394 CaretChangedReason aReason) {
1395 if (!FlushLayout()) {
1396 return;
1399 Selection* sel = GetSelection();
1400 if (!sel) {
1401 return;
1404 Document* doc = mPresShell->GetDocument();
1405 MOZ_ASSERT(doc);
1407 CaretStateChangedEventInit init;
1408 init.mBubbles = true;
1410 const nsRange* range = sel->GetAnchorFocusRange();
1411 nsINode* commonAncestorNode = nullptr;
1412 if (range) {
1413 commonAncestorNode = range->GetClosestCommonInclusiveAncestor();
1416 if (!commonAncestorNode) {
1417 commonAncestorNode = sel->GetFrameSelection()->GetAncestorLimiter();
1420 RefPtr<DOMRect> domRect = new DOMRect(ToSupports(doc));
1421 nsRect rect = nsLayoutUtils::GetSelectionBoundingRect(sel);
1423 nsIFrame* commonAncestorFrame = nullptr;
1424 nsIFrame* rootFrame = mPresShell->GetRootFrame();
1426 if (commonAncestorNode && commonAncestorNode->IsContent()) {
1427 commonAncestorFrame = commonAncestorNode->AsContent()->GetPrimaryFrame();
1430 if (commonAncestorFrame && rootFrame) {
1431 nsLayoutUtils::TransformRect(rootFrame, commonAncestorFrame, rect);
1432 nsRect clampedRect =
1433 nsLayoutUtils::ClampRectToScrollFrames(commonAncestorFrame, rect);
1434 nsLayoutUtils::TransformRect(commonAncestorFrame, rootFrame, clampedRect);
1435 rect = clampedRect;
1436 init.mSelectionVisible = !clampedRect.IsEmpty();
1437 } else {
1438 init.mSelectionVisible = true;
1441 // The rect computed above is relative to rootFrame, which is the (layout)
1442 // viewport frame. However, the consumers of this event expect the bounds
1443 // of the selection relative to the screen (visual viewport origin), so
1444 // translate between the two.
1445 rect -= mPresShell->GetVisualViewportOffsetRelativeToLayoutViewport();
1447 domRect->SetLayoutRect(rect);
1449 // Send isEditable info w/ event detail. This info can help determine
1450 // whether to show cut command on selection dialog or not.
1451 init.mSelectionEditable =
1452 commonAncestorFrame && GetEditingHostForFrame(commonAncestorFrame);
1454 init.mBoundingClientRect = domRect;
1455 init.mReason = aReason;
1456 init.mCollapsed = sel->IsCollapsed();
1457 init.mCaretVisible =
1458 mFirstCaret->IsLogicallyVisible() || mSecondCaret->IsLogicallyVisible();
1459 init.mCaretVisuallyVisible =
1460 mFirstCaret->IsVisuallyVisible() || mSecondCaret->IsVisuallyVisible();
1461 init.mSelectedTextContent = StringifiedSelection();
1463 RefPtr<CaretStateChangedEvent> event = CaretStateChangedEvent::Constructor(
1464 doc, u"mozcaretstatechanged"_ns, init);
1466 event->SetTrusted(true);
1467 event->WidgetEventPtr()->mFlags.mOnlyChromeDispatch = true;
1469 AC_LOG("%s: reason %" PRIu32 ", collapsed %d, caretVisible %" PRIu32,
1470 __FUNCTION__, static_cast<uint32_t>(init.mReason), init.mCollapsed,
1471 static_cast<uint32_t>(init.mCaretVisible));
1473 (new AsyncEventDispatcher(doc, event))->PostDOMEvent();
1476 } // namespace mozilla