Bug 1861709 replace AudioCallbackDriver::ThreadRunning() assertions that mean to...
[gecko.git] / layout / base / AccessibleCaretManager.cpp
blob379fea276099d6142705da3316a7ab6ade3af3b6
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 <utility>
11 #include "AccessibleCaret.h"
12 #include "AccessibleCaretEventHub.h"
13 #include "AccessibleCaretLogger.h"
14 #include "mozilla/AsyncEventDispatcher.h"
15 #include "mozilla/AutoRestore.h"
16 #include "mozilla/dom/Element.h"
17 #include "mozilla/dom/MouseEventBinding.h"
18 #include "mozilla/dom/NodeFilterBinding.h"
19 #include "mozilla/dom/Selection.h"
20 #include "mozilla/dom/TreeWalker.h"
21 #include "mozilla/IMEStateManager.h"
22 #include "mozilla/IntegerPrintfMacros.h"
23 #include "mozilla/PresShell.h"
24 #include "mozilla/StaticAnalysisFunctions.h"
25 #include "mozilla/StaticPrefs_layout.h"
26 #include "nsCaret.h"
27 #include "nsContainerFrame.h"
28 #include "nsContentUtils.h"
29 #include "nsDebug.h"
30 #include "nsFocusManager.h"
31 #include "nsIFrame.h"
32 #include "nsFrameSelection.h"
33 #include "nsGenericHTMLElement.h"
34 #include "nsIHapticFeedback.h"
35 #include "nsIScrollableFrame.h"
36 #include "nsLayoutUtils.h"
37 #include "nsServiceManagerUtils.h"
39 namespace mozilla {
41 #undef AC_LOG
42 #define AC_LOG(message, ...) \
43 AC_LOG_BASE("AccessibleCaretManager (%p): " message, this, ##__VA_ARGS__);
45 #undef AC_LOGV
46 #define AC_LOGV(message, ...) \
47 AC_LOGV_BASE("AccessibleCaretManager (%p): " message, this, ##__VA_ARGS__);
49 using namespace dom;
50 using Appearance = AccessibleCaret::Appearance;
51 using PositionChangedResult = AccessibleCaret::PositionChangedResult;
53 #define AC_PROCESS_ENUM_TO_STREAM(e) \
54 case (e): \
55 aStream << #e; \
56 break;
57 std::ostream& operator<<(std::ostream& aStream,
58 const AccessibleCaretManager::CaretMode& aCaretMode) {
59 using CaretMode = AccessibleCaretManager::CaretMode;
60 switch (aCaretMode) {
61 AC_PROCESS_ENUM_TO_STREAM(CaretMode::None);
62 AC_PROCESS_ENUM_TO_STREAM(CaretMode::Cursor);
63 AC_PROCESS_ENUM_TO_STREAM(CaretMode::Selection);
65 return aStream;
68 std::ostream& operator<<(
69 std::ostream& aStream,
70 const AccessibleCaretManager::UpdateCaretsHint& aHint) {
71 using UpdateCaretsHint = AccessibleCaretManager::UpdateCaretsHint;
72 switch (aHint) {
73 AC_PROCESS_ENUM_TO_STREAM(UpdateCaretsHint::Default);
74 AC_PROCESS_ENUM_TO_STREAM(UpdateCaretsHint::RespectOldAppearance);
75 AC_PROCESS_ENUM_TO_STREAM(UpdateCaretsHint::DispatchNoEvent);
77 return aStream;
79 #undef AC_PROCESS_ENUM_TO_STREAM
81 AccessibleCaretManager::AccessibleCaretManager(PresShell* aPresShell)
82 : AccessibleCaretManager{
83 aPresShell,
84 Carets{aPresShell ? MakeUnique<AccessibleCaret>(aPresShell) : nullptr,
85 aPresShell ? MakeUnique<AccessibleCaret>(aPresShell)
86 : nullptr}} {}
88 AccessibleCaretManager::AccessibleCaretManager(PresShell* aPresShell,
89 Carets aCarets)
90 : mPresShell{aPresShell}, mCarets{std::move(aCarets)} {}
92 AccessibleCaretManager::LayoutFlusher::~LayoutFlusher() {
93 MOZ_RELEASE_ASSERT(!mFlushing, "Going away in MaybeFlush? Bad!");
96 void AccessibleCaretManager::Terminate() {
97 mCarets.Terminate();
98 mActiveCaret = nullptr;
99 mPresShell = nullptr;
102 nsresult AccessibleCaretManager::OnSelectionChanged(Document* aDoc,
103 Selection* aSel,
104 int16_t aReason) {
105 Selection* selection = GetSelection();
106 AC_LOG("%s: aSel: %p, GetSelection(): %p, aReason: %d", __FUNCTION__, aSel,
107 selection, aReason);
108 if (aSel != selection) {
109 return NS_OK;
112 // eSetSelection events from the Fennec widget IME can be generated
113 // by autoSuggest / autoCorrect composition changes, or by TYPE_REPLACE_TEXT
114 // actions, either positioning cursor for text insert, or selecting
115 // text-to-be-replaced. None should affect AccessibleCaret visibility.
116 if (aReason & nsISelectionListener::IME_REASON) {
117 return NS_OK;
120 // Move the cursor by JavaScript or unknown internal call.
121 if (aReason == nsISelectionListener::NO_REASON ||
122 aReason == nsISelectionListener::JS_REASON) {
123 auto mode = static_cast<ScriptUpdateMode>(
124 StaticPrefs::layout_accessiblecaret_script_change_update_mode());
125 if (mode == kScriptAlwaysShow ||
126 (mode == kScriptUpdateVisible && mCarets.HasLogicallyVisibleCaret())) {
127 UpdateCarets();
128 return NS_OK;
130 // Default for NO_REASON is to make hidden.
131 HideCaretsAndDispatchCaretStateChangedEvent();
132 return NS_OK;
135 // Move cursor by keyboard.
136 if (aReason & nsISelectionListener::KEYPRESS_REASON) {
137 HideCaretsAndDispatchCaretStateChangedEvent();
138 return NS_OK;
141 // OnBlur() might be called between mouse down and mouse up, so we hide carets
142 // upon mouse down anyway, and update carets upon mouse up.
143 if (aReason & nsISelectionListener::MOUSEDOWN_REASON) {
144 HideCaretsAndDispatchCaretStateChangedEvent();
145 return NS_OK;
148 // Range will collapse after cutting or copying text.
149 if (aReason & (nsISelectionListener::COLLAPSETOSTART_REASON |
150 nsISelectionListener::COLLAPSETOEND_REASON)) {
151 HideCaretsAndDispatchCaretStateChangedEvent();
152 return NS_OK;
155 // For mouse input we don't want to show the carets.
156 if (StaticPrefs::layout_accessiblecaret_hide_carets_for_mouse_input() &&
157 mLastInputSource == MouseEvent_Binding::MOZ_SOURCE_MOUSE) {
158 HideCaretsAndDispatchCaretStateChangedEvent();
159 return NS_OK;
162 // When we want to hide the carets for mouse input, hide them for select
163 // all action fired by keyboard as well.
164 if (StaticPrefs::layout_accessiblecaret_hide_carets_for_mouse_input() &&
165 mLastInputSource == MouseEvent_Binding::MOZ_SOURCE_KEYBOARD &&
166 (aReason & nsISelectionListener::SELECTALL_REASON)) {
167 HideCaretsAndDispatchCaretStateChangedEvent();
168 return NS_OK;
171 UpdateCarets();
172 return NS_OK;
175 void AccessibleCaretManager::HideCaretsAndDispatchCaretStateChangedEvent() {
176 if (mCarets.HasLogicallyVisibleCaret()) {
177 AC_LOG("%s", __FUNCTION__);
178 mCarets.GetFirst()->SetAppearance(Appearance::None);
179 mCarets.GetSecond()->SetAppearance(Appearance::None);
180 mIsCaretPositionChanged = false;
181 DispatchCaretStateChangedEvent(CaretChangedReason::Visibilitychange);
185 auto AccessibleCaretManager::MaybeFlushLayout() -> Terminated {
186 if (mPresShell) {
187 // `MaybeFlush` doesn't access the PresShell after flushing, so it's OK to
188 // mark it as live.
189 mLayoutFlusher.MaybeFlush(MOZ_KnownLive(*mPresShell));
192 return IsTerminated();
195 void AccessibleCaretManager::UpdateCarets(const UpdateCaretsHintSet& aHint) {
196 if (MaybeFlushLayout() == Terminated::Yes) {
197 return;
200 mLastUpdateCaretMode = GetCaretMode();
202 switch (mLastUpdateCaretMode) {
203 case CaretMode::None:
204 HideCaretsAndDispatchCaretStateChangedEvent();
205 break;
206 case CaretMode::Cursor:
207 UpdateCaretsForCursorMode(aHint);
208 break;
209 case CaretMode::Selection:
210 UpdateCaretsForSelectionMode(aHint);
211 break;
214 mDesiredAsyncPanZoomState.Update(*this);
217 bool AccessibleCaretManager::IsCaretDisplayableInCursorMode(
218 nsIFrame** aOutFrame, int32_t* aOutOffset) const {
219 RefPtr<nsCaret> caret = mPresShell->GetCaret();
220 if (!caret || !caret->IsVisible()) {
221 return false;
224 int32_t offset = 0;
225 nsIFrame* frame =
226 nsCaret::GetFrameAndOffset(GetSelection(), nullptr, 0, &offset);
228 if (!frame) {
229 return false;
232 if (!GetEditingHostForFrame(frame)) {
233 return false;
236 if (aOutFrame) {
237 *aOutFrame = frame;
240 if (aOutOffset) {
241 *aOutOffset = offset;
244 return true;
247 bool AccessibleCaretManager::HasNonEmptyTextContent(nsINode* aNode) const {
248 return nsContentUtils::HasNonEmptyTextContent(
249 aNode, nsContentUtils::eRecurseIntoChildren);
252 void AccessibleCaretManager::UpdateCaretsForCursorMode(
253 const UpdateCaretsHintSet& aHints) {
254 AC_LOG("%s, selection: %p", __FUNCTION__, GetSelection());
256 int32_t offset = 0;
257 nsIFrame* frame = nullptr;
258 if (!IsCaretDisplayableInCursorMode(&frame, &offset)) {
259 HideCaretsAndDispatchCaretStateChangedEvent();
260 return;
263 PositionChangedResult result = mCarets.GetFirst()->SetPosition(frame, offset);
265 switch (result) {
266 case PositionChangedResult::NotChanged:
267 case PositionChangedResult::Position:
268 case PositionChangedResult::Zoom:
269 if (!aHints.contains(UpdateCaretsHint::RespectOldAppearance)) {
270 if (HasNonEmptyTextContent(GetEditingHostForFrame(frame))) {
271 mCarets.GetFirst()->SetAppearance(Appearance::Normal);
272 } else if (
273 StaticPrefs::
274 layout_accessiblecaret_caret_shown_when_long_tapping_on_empty_content()) {
275 if (mCarets.GetFirst()->IsLogicallyVisible()) {
276 // Possible cases are: 1) SelectWordOrShortcut() sets the
277 // appearance to Normal. 2) When the caret is out of viewport and
278 // now scrolling into viewport, it has appearance NormalNotShown.
279 mCarets.GetFirst()->SetAppearance(Appearance::Normal);
280 } else {
281 // Possible cases are: a) Single tap on current empty content;
282 // OnSelectionChanged() sets the appearance to None due to
283 // MOUSEDOWN_REASON. b) Single tap on other empty content;
284 // OnBlur() sets the appearance to None.
286 // Do nothing to make the appearance remains None so that it can
287 // be distinguished from case 2). Also do not set the appearance
288 // to NormalNotShown here like the default update behavior.
290 } else {
291 mCarets.GetFirst()->SetAppearance(Appearance::NormalNotShown);
294 break;
296 case PositionChangedResult::Invisible:
297 mCarets.GetFirst()->SetAppearance(Appearance::NormalNotShown);
298 break;
301 mCarets.GetSecond()->SetAppearance(Appearance::None);
303 mIsCaretPositionChanged = (result == PositionChangedResult::Position);
305 if (!aHints.contains(UpdateCaretsHint::DispatchNoEvent) && !mActiveCaret) {
306 DispatchCaretStateChangedEvent(CaretChangedReason::Updateposition);
310 void AccessibleCaretManager::UpdateCaretsForSelectionMode(
311 const UpdateCaretsHintSet& aHints) {
312 AC_LOG("%s: selection: %p", __FUNCTION__, GetSelection());
314 int32_t startOffset = 0;
315 nsIFrame* startFrame =
316 GetFrameForFirstRangeStartOrLastRangeEnd(eDirNext, &startOffset);
318 int32_t endOffset = 0;
319 nsIFrame* endFrame =
320 GetFrameForFirstRangeStartOrLastRangeEnd(eDirPrevious, &endOffset);
322 if (!CompareTreePosition(startFrame, endFrame)) {
323 // XXX: Do we really have to hide carets if this condition isn't satisfied?
324 HideCaretsAndDispatchCaretStateChangedEvent();
325 return;
328 auto updateSingleCaret = [aHints](AccessibleCaret* aCaret, nsIFrame* aFrame,
329 int32_t aOffset) -> PositionChangedResult {
330 PositionChangedResult result = aCaret->SetPosition(aFrame, aOffset);
332 switch (result) {
333 case PositionChangedResult::NotChanged:
334 case PositionChangedResult::Position:
335 case PositionChangedResult::Zoom:
336 if (!aHints.contains(UpdateCaretsHint::RespectOldAppearance)) {
337 aCaret->SetAppearance(Appearance::Normal);
339 break;
341 case PositionChangedResult::Invisible:
342 aCaret->SetAppearance(Appearance::NormalNotShown);
343 break;
345 return result;
348 PositionChangedResult firstCaretResult =
349 updateSingleCaret(mCarets.GetFirst(), startFrame, startOffset);
350 PositionChangedResult secondCaretResult =
351 updateSingleCaret(mCarets.GetSecond(), endFrame, endOffset);
353 mIsCaretPositionChanged =
354 firstCaretResult == PositionChangedResult::Position ||
355 secondCaretResult == PositionChangedResult::Position;
357 if (mIsCaretPositionChanged) {
358 // Flush layout to make the carets intersection correct.
359 if (MaybeFlushLayout() == Terminated::Yes) {
360 return;
364 if (!aHints.contains(UpdateCaretsHint::RespectOldAppearance)) {
365 // Only check for tilt carets when the caller doesn't ask us to preserve
366 // old appearance. Otherwise we might override the appearance set by the
367 // caller.
368 if (StaticPrefs::layout_accessiblecaret_always_tilt()) {
369 UpdateCaretsForAlwaysTilt(startFrame, endFrame);
370 } else {
371 UpdateCaretsForOverlappingTilt();
375 if (!aHints.contains(UpdateCaretsHint::DispatchNoEvent) && !mActiveCaret) {
376 DispatchCaretStateChangedEvent(CaretChangedReason::Updateposition);
380 void AccessibleCaretManager::DesiredAsyncPanZoomState::Update(
381 const AccessibleCaretManager& aAccessibleCaretManager) {
382 if (aAccessibleCaretManager.mActiveCaret) {
383 // No need to disable APZ when dragging the caret.
384 mValue = Value::Enabled;
385 return;
388 if (aAccessibleCaretManager.mIsScrollStarted) {
389 // During scrolling, the caret's position is changed only if it is in a
390 // position:fixed or a "stuck" position:sticky frame subtree.
391 mValue = aAccessibleCaretManager.mIsCaretPositionChanged ? Value::Disabled
392 : Value::Enabled;
393 return;
396 // For other cases, we can only reliably detect whether the caret is in a
397 // position:fixed frame subtree.
398 switch (aAccessibleCaretManager.mLastUpdateCaretMode) {
399 case CaretMode::None:
400 mValue = Value::Enabled;
401 break;
402 case CaretMode::Cursor:
403 mValue =
404 (aAccessibleCaretManager.mCarets.GetFirst()->IsVisuallyVisible() &&
405 aAccessibleCaretManager.mCarets.GetFirst()
406 ->IsInPositionFixedSubtree())
407 ? Value::Disabled
408 : Value::Enabled;
409 break;
410 case CaretMode::Selection:
411 mValue =
412 ((aAccessibleCaretManager.mCarets.GetFirst()->IsVisuallyVisible() &&
413 aAccessibleCaretManager.mCarets.GetFirst()
414 ->IsInPositionFixedSubtree()) ||
415 (aAccessibleCaretManager.mCarets.GetSecond()->IsVisuallyVisible() &&
416 aAccessibleCaretManager.mCarets.GetSecond()
417 ->IsInPositionFixedSubtree()))
418 ? Value::Disabled
419 : Value::Enabled;
420 break;
424 bool AccessibleCaretManager::UpdateCaretsForOverlappingTilt() {
425 if (!mCarets.GetFirst()->IsVisuallyVisible() ||
426 !mCarets.GetSecond()->IsVisuallyVisible()) {
427 return false;
430 if (!mCarets.GetFirst()->Intersects(*mCarets.GetSecond())) {
431 mCarets.GetFirst()->SetAppearance(Appearance::Normal);
432 mCarets.GetSecond()->SetAppearance(Appearance::Normal);
433 return false;
436 if (mCarets.GetFirst()->LogicalPosition().x <=
437 mCarets.GetSecond()->LogicalPosition().x) {
438 mCarets.GetFirst()->SetAppearance(Appearance::Left);
439 mCarets.GetSecond()->SetAppearance(Appearance::Right);
440 } else {
441 mCarets.GetFirst()->SetAppearance(Appearance::Right);
442 mCarets.GetSecond()->SetAppearance(Appearance::Left);
445 return true;
448 void AccessibleCaretManager::UpdateCaretsForAlwaysTilt(
449 const nsIFrame* aStartFrame, const nsIFrame* aEndFrame) {
450 // When a short LTR word in RTL environment is selected, the two carets
451 // tilted inward might be overlapped. Make them tilt outward.
452 if (UpdateCaretsForOverlappingTilt()) {
453 return;
456 if (mCarets.GetFirst()->IsVisuallyVisible()) {
457 auto startFrameWritingMode = aStartFrame->GetWritingMode();
458 mCarets.GetFirst()->SetAppearance(startFrameWritingMode.IsBidiLTR()
459 ? Appearance::Left
460 : Appearance::Right);
462 if (mCarets.GetSecond()->IsVisuallyVisible()) {
463 auto endFrameWritingMode = aEndFrame->GetWritingMode();
464 mCarets.GetSecond()->SetAppearance(
465 endFrameWritingMode.IsBidiLTR() ? Appearance::Right : Appearance::Left);
469 void AccessibleCaretManager::ProvideHapticFeedback() {
470 if (StaticPrefs::layout_accessiblecaret_hapticfeedback()) {
471 if (nsCOMPtr<nsIHapticFeedback> haptic =
472 do_GetService("@mozilla.org/widget/hapticfeedback;1")) {
473 haptic->PerformSimpleAction(haptic->LongPress);
478 nsresult AccessibleCaretManager::PressCaret(const nsPoint& aPoint,
479 EventClassID aEventClass) {
480 nsresult rv = NS_ERROR_FAILURE;
482 MOZ_ASSERT(aEventClass == eMouseEventClass || aEventClass == eTouchEventClass,
483 "Unexpected event class!");
485 using TouchArea = AccessibleCaret::TouchArea;
486 TouchArea touchArea =
487 aEventClass == eMouseEventClass ? TouchArea::CaretImage : TouchArea::Full;
489 if (mCarets.GetFirst()->Contains(aPoint, touchArea)) {
490 mActiveCaret = mCarets.GetFirst();
491 SetSelectionDirection(eDirPrevious);
492 } else if (mCarets.GetSecond()->Contains(aPoint, touchArea)) {
493 mActiveCaret = mCarets.GetSecond();
494 SetSelectionDirection(eDirNext);
497 if (mActiveCaret) {
498 mOffsetYToCaretLogicalPosition =
499 mActiveCaret->LogicalPosition().y - aPoint.y;
500 SetSelectionDragState(true);
501 DispatchCaretStateChangedEvent(CaretChangedReason::Presscaret, &aPoint);
502 rv = NS_OK;
505 return rv;
508 nsresult AccessibleCaretManager::DragCaret(const nsPoint& aPoint) {
509 MOZ_ASSERT(mActiveCaret);
510 MOZ_ASSERT(GetCaretMode() != CaretMode::None);
512 if (!mPresShell || !mPresShell->GetRootFrame() || !GetSelection()) {
513 return NS_ERROR_NULL_POINTER;
516 StopSelectionAutoScrollTimer();
517 DragCaretInternal(aPoint);
519 // We want to scroll the page even if we failed to drag the caret.
520 StartSelectionAutoScrollTimer(aPoint);
521 UpdateCarets();
523 if (StaticPrefs::layout_accessiblecaret_magnifier_enabled()) {
524 DispatchCaretStateChangedEvent(CaretChangedReason::Dragcaret, &aPoint);
526 return NS_OK;
529 nsresult AccessibleCaretManager::ReleaseCaret() {
530 MOZ_ASSERT(mActiveCaret);
532 mActiveCaret = nullptr;
533 SetSelectionDragState(false);
534 mDesiredAsyncPanZoomState.Update(*this);
535 DispatchCaretStateChangedEvent(CaretChangedReason::Releasecaret);
536 return NS_OK;
539 nsresult AccessibleCaretManager::TapCaret(const nsPoint& aPoint) {
540 MOZ_ASSERT(GetCaretMode() != CaretMode::None);
542 nsresult rv = NS_ERROR_FAILURE;
544 if (GetCaretMode() == CaretMode::Cursor) {
545 DispatchCaretStateChangedEvent(CaretChangedReason::Taponcaret, &aPoint);
546 rv = NS_OK;
549 return rv;
552 static EnumSet<nsLayoutUtils::FrameForPointOption> GetHitTestOptions() {
553 EnumSet<nsLayoutUtils::FrameForPointOption> options = {
554 nsLayoutUtils::FrameForPointOption::IgnorePaintSuppression,
555 nsLayoutUtils::FrameForPointOption::IgnoreCrossDoc};
556 return options;
559 nsresult AccessibleCaretManager::SelectWordOrShortcut(const nsPoint& aPoint) {
560 // If the long-tap is landing on a pre-existing selection, don't replace
561 // it with a new one. Instead just return and let the context menu pop up
562 // on the pre-existing selection.
563 if (GetCaretMode() == CaretMode::Selection &&
564 GetSelection()->ContainsPoint(aPoint)) {
565 AC_LOG("%s: UpdateCarets() for current selection", __FUNCTION__);
566 UpdateCarets();
567 ProvideHapticFeedback();
568 return NS_OK;
571 if (!mPresShell) {
572 return NS_ERROR_UNEXPECTED;
575 nsIFrame* rootFrame = mPresShell->GetRootFrame();
576 if (!rootFrame) {
577 return NS_ERROR_NOT_AVAILABLE;
580 // Find the frame under point.
581 AutoWeakFrame ptFrame = nsLayoutUtils::GetFrameForPoint(
582 RelativeTo{rootFrame}, aPoint, GetHitTestOptions());
583 if (!ptFrame.GetFrame()) {
584 return NS_ERROR_FAILURE;
587 nsIFrame* focusableFrame = GetFocusableFrame(ptFrame);
589 #ifdef DEBUG_FRAME_DUMP
590 AC_LOG("%s: Found %s under (%d, %d)", __FUNCTION__, ptFrame->ListTag().get(),
591 aPoint.x, aPoint.y);
592 AC_LOG("%s: Found %s focusable", __FUNCTION__,
593 focusableFrame ? focusableFrame->ListTag().get() : "no frame");
594 #endif
596 // Get ptInFrame here so that we don't need to check whether rootFrame is
597 // alive later. Note that if ptFrame is being moved by
598 // IMEStateManager::NotifyIME() or ChangeFocusToOrClearOldFocus() below,
599 // something under the original point will be selected, which may not be the
600 // original text the user wants to select.
601 nsPoint ptInFrame = aPoint;
602 nsLayoutUtils::TransformPoint(RelativeTo{rootFrame}, RelativeTo{ptFrame},
603 ptInFrame);
605 // Firstly check long press on an empty editable content.
606 Element* newFocusEditingHost = GetEditingHostForFrame(ptFrame);
607 if (focusableFrame && newFocusEditingHost &&
608 !HasNonEmptyTextContent(newFocusEditingHost)) {
609 ChangeFocusToOrClearOldFocus(focusableFrame);
611 if (StaticPrefs::
612 layout_accessiblecaret_caret_shown_when_long_tapping_on_empty_content()) {
613 mCarets.GetFirst()->SetAppearance(Appearance::Normal);
615 // We need to update carets to get correct information before dispatching
616 // CaretStateChangedEvent.
617 UpdateCarets();
618 ProvideHapticFeedback();
619 DispatchCaretStateChangedEvent(CaretChangedReason::Longpressonemptycontent);
620 return NS_OK;
623 bool selectable = ptFrame->IsSelectable(nullptr);
625 #ifdef DEBUG_FRAME_DUMP
626 AC_LOG("%s: %s %s selectable.", __FUNCTION__, ptFrame->ListTag().get(),
627 selectable ? "is" : "is NOT");
628 #endif
630 if (!selectable) {
631 return NS_ERROR_FAILURE;
634 // Commit the composition string of the old editable focus element (if there
635 // is any) before changing the focus.
636 IMEStateManager::NotifyIME(widget::REQUEST_TO_COMMIT_COMPOSITION,
637 mPresShell->GetPresContext());
638 if (!ptFrame.IsAlive()) {
639 // Cannot continue because ptFrame died.
640 return NS_ERROR_FAILURE;
643 // ptFrame is selectable. Now change the focus.
644 ChangeFocusToOrClearOldFocus(focusableFrame);
645 if (!ptFrame.IsAlive()) {
646 // Cannot continue because ptFrame died.
647 return NS_ERROR_FAILURE;
650 // If long tap point isn't selectable frame for caret and frame selection
651 // can find a better frame for caret, we don't select a word.
652 // See https://webcompat.com/issues/15953
653 nsIFrame::ContentOffsets offsets =
654 ptFrame->GetContentOffsetsFromPoint(ptInFrame, nsIFrame::SKIP_HIDDEN);
655 if (offsets.content) {
656 RefPtr<nsFrameSelection> frameSelection = GetFrameSelection();
657 if (frameSelection) {
658 int32_t offset;
659 nsIFrame* theFrame = nsFrameSelection::GetFrameForNodeOffset(
660 offsets.content, offsets.offset, offsets.associate, &offset);
661 if (theFrame && theFrame != ptFrame) {
662 SetSelectionDragState(true);
663 frameSelection->HandleClick(
664 MOZ_KnownLive(offsets.content) /* bug 1636889 */,
665 offsets.StartOffset(), offsets.EndOffset(),
666 nsFrameSelection::FocusMode::kCollapseToNewPoint,
667 offsets.associate);
668 SetSelectionDragState(false);
669 ClearMaintainedSelection();
671 if (StaticPrefs::
672 layout_accessiblecaret_caret_shown_when_long_tapping_on_empty_content()) {
673 mCarets.GetFirst()->SetAppearance(Appearance::Normal);
676 UpdateCarets();
677 ProvideHapticFeedback();
678 DispatchCaretStateChangedEvent(
679 CaretChangedReason::Longpressonemptycontent);
681 return NS_OK;
686 // Then try select a word under point.
687 nsresult rv = SelectWord(ptFrame, ptInFrame);
688 UpdateCarets();
689 ProvideHapticFeedback();
691 return rv;
694 void AccessibleCaretManager::OnScrollStart() {
695 AC_LOG("%s", __FUNCTION__);
697 nsAutoScriptBlocker scriptBlocker;
698 AutoRestore<bool> saveAllowFlushingLayout(mLayoutFlusher.mAllowFlushing);
699 mLayoutFlusher.mAllowFlushing = false;
701 Maybe<PresShell::AutoAssertNoFlush> assert;
702 if (mPresShell) {
703 assert.emplace(*mPresShell);
706 mIsScrollStarted = true;
708 if (mCarets.HasLogicallyVisibleCaret()) {
709 // Dispatch the event only if one of the carets is logically visible like in
710 // HideCaretsAndDispatchCaretStateChangedEvent().
711 DispatchCaretStateChangedEvent(CaretChangedReason::Scroll);
715 void AccessibleCaretManager::OnScrollEnd() {
716 nsAutoScriptBlocker scriptBlocker;
717 AutoRestore<bool> saveAllowFlushingLayout(mLayoutFlusher.mAllowFlushing);
718 mLayoutFlusher.mAllowFlushing = false;
720 Maybe<PresShell::AutoAssertNoFlush> assert;
721 if (mPresShell) {
722 assert.emplace(*mPresShell);
725 mIsScrollStarted = false;
727 if (GetCaretMode() == CaretMode::Cursor) {
728 if (!mCarets.GetFirst()->IsLogicallyVisible()) {
729 // If the caret is hidden (Appearance::None) due to blur, no
730 // need to update it.
731 return;
735 // For mouse and keyboard input, we don't want to show the carets.
736 if (StaticPrefs::layout_accessiblecaret_hide_carets_for_mouse_input() &&
737 (mLastInputSource == MouseEvent_Binding::MOZ_SOURCE_MOUSE ||
738 mLastInputSource == MouseEvent_Binding::MOZ_SOURCE_KEYBOARD)) {
739 AC_LOG("%s: HideCaretsAndDispatchCaretStateChangedEvent()", __FUNCTION__);
740 HideCaretsAndDispatchCaretStateChangedEvent();
741 return;
744 AC_LOG("%s: UpdateCarets()", __FUNCTION__);
745 UpdateCarets();
748 void AccessibleCaretManager::OnScrollPositionChanged() {
749 nsAutoScriptBlocker scriptBlocker;
750 AutoRestore<bool> saveAllowFlushingLayout(mLayoutFlusher.mAllowFlushing);
751 mLayoutFlusher.mAllowFlushing = false;
753 Maybe<PresShell::AutoAssertNoFlush> assert;
754 if (mPresShell) {
755 assert.emplace(*mPresShell);
758 if (mCarets.HasLogicallyVisibleCaret()) {
759 if (mIsScrollStarted) {
760 // We don't want extra CaretStateChangedEvents dispatched when user is
761 // scrolling the page.
762 AC_LOG("%s: UpdateCarets(RespectOldAppearance | DispatchNoEvent)",
763 __FUNCTION__);
764 UpdateCarets({UpdateCaretsHint::RespectOldAppearance,
765 UpdateCaretsHint::DispatchNoEvent});
766 } else {
767 AC_LOG("%s: UpdateCarets(RespectOldAppearance)", __FUNCTION__);
768 UpdateCarets(UpdateCaretsHint::RespectOldAppearance);
773 void AccessibleCaretManager::OnReflow() {
774 nsAutoScriptBlocker scriptBlocker;
775 AutoRestore<bool> saveAllowFlushingLayout(mLayoutFlusher.mAllowFlushing);
776 mLayoutFlusher.mAllowFlushing = false;
778 Maybe<PresShell::AutoAssertNoFlush> assert;
779 if (mPresShell) {
780 assert.emplace(*mPresShell);
783 if (mCarets.HasLogicallyVisibleCaret()) {
784 AC_LOG("%s: UpdateCarets(RespectOldAppearance)", __FUNCTION__);
785 UpdateCarets(UpdateCaretsHint::RespectOldAppearance);
789 void AccessibleCaretManager::OnBlur() {
790 AC_LOG("%s: HideCaretsAndDispatchCaretStateChangedEvent()", __FUNCTION__);
791 HideCaretsAndDispatchCaretStateChangedEvent();
794 void AccessibleCaretManager::OnKeyboardEvent() {
795 if (GetCaretMode() == CaretMode::Cursor) {
796 AC_LOG("%s: HideCaretsAndDispatchCaretStateChangedEvent()", __FUNCTION__);
797 HideCaretsAndDispatchCaretStateChangedEvent();
801 void AccessibleCaretManager::SetLastInputSource(uint16_t aInputSource) {
802 mLastInputSource = aInputSource;
805 bool AccessibleCaretManager::ShouldDisableApz() const {
806 return mDesiredAsyncPanZoomState.Get() ==
807 DesiredAsyncPanZoomState::Value::Disabled;
810 Selection* AccessibleCaretManager::GetSelection() const {
811 RefPtr<nsFrameSelection> fs = GetFrameSelection();
812 if (!fs) {
813 return nullptr;
815 return fs->GetSelection(SelectionType::eNormal);
818 already_AddRefed<nsFrameSelection> AccessibleCaretManager::GetFrameSelection()
819 const {
820 if (!mPresShell) {
821 return nullptr;
824 // Prevent us from touching the nsFrameSelection associated with other
825 // PresShell.
826 RefPtr<nsFrameSelection> fs = mPresShell->GetLastFocusedFrameSelection();
827 if (!fs || fs->GetPresShell() != mPresShell) {
828 return nullptr;
831 return fs.forget();
834 nsAutoString AccessibleCaretManager::StringifiedSelection() const {
835 nsAutoString str;
836 RefPtr<Selection> selection = GetSelection();
837 if (selection) {
838 selection->Stringify(str, mLayoutFlusher.mAllowFlushing
839 ? Selection::FlushFrames::Yes
840 : Selection::FlushFrames::No);
842 return str;
845 // static
846 Element* AccessibleCaretManager::GetEditingHostForFrame(
847 const nsIFrame* aFrame) {
848 if (!aFrame) {
849 return nullptr;
852 auto content = aFrame->GetContent();
853 if (!content) {
854 return nullptr;
857 return content->GetEditingHost();
860 AccessibleCaretManager::CaretMode AccessibleCaretManager::GetCaretMode() const {
861 const Selection* selection = GetSelection();
862 if (!selection) {
863 return CaretMode::None;
866 const uint32_t rangeCount = selection->RangeCount();
867 if (rangeCount <= 0) {
868 return CaretMode::None;
871 const nsFocusManager* fm = nsFocusManager::GetFocusManager();
872 MOZ_ASSERT(fm);
873 if (fm->GetFocusedWindow() != mPresShell->GetDocument()->GetWindow()) {
874 // Hide carets if the window is not focused.
875 return CaretMode::None;
878 if (selection->IsCollapsed()) {
879 return CaretMode::Cursor;
882 return CaretMode::Selection;
885 nsIFrame* AccessibleCaretManager::GetFocusableFrame(nsIFrame* aFrame) const {
886 // This implementation is similar to EventStateManager::PostHandleEvent().
887 // Look for the nearest enclosing focusable frame.
888 nsIFrame* focusableFrame = aFrame;
889 while (focusableFrame) {
890 if (focusableFrame->IsFocusable(/* aWithMouse = */ true)) {
891 break;
893 focusableFrame = focusableFrame->GetParent();
895 return focusableFrame;
898 void AccessibleCaretManager::ChangeFocusToOrClearOldFocus(
899 nsIFrame* aFrame) const {
900 RefPtr<nsFocusManager> fm = nsFocusManager::GetFocusManager();
901 MOZ_ASSERT(fm);
903 if (aFrame) {
904 nsIContent* focusableContent = aFrame->GetContent();
905 MOZ_ASSERT(focusableContent, "Focusable frame must have content!");
906 RefPtr<Element> focusableElement = Element::FromNode(focusableContent);
907 fm->SetFocus(focusableElement, nsIFocusManager::FLAG_BYLONGPRESS);
908 } else if (nsCOMPtr<nsPIDOMWindowOuter> win =
909 mPresShell->GetDocument()->GetWindow()) {
910 fm->ClearFocus(win);
911 fm->SetFocusedWindow(win);
915 nsresult AccessibleCaretManager::SelectWord(nsIFrame* aFrame,
916 const nsPoint& aPoint) const {
917 AC_LOGV("%s", __FUNCTION__);
919 SetSelectionDragState(true);
920 const RefPtr<nsPresContext> pinnedPresContext{mPresShell->GetPresContext()};
921 nsresult rs = aFrame->SelectByTypeAtPoint(pinnedPresContext, aPoint,
922 eSelectWord, eSelectWord, 0);
924 SetSelectionDragState(false);
925 ClearMaintainedSelection();
927 // Smart-select phone numbers if possible.
928 if (StaticPrefs::layout_accessiblecaret_extend_selection_for_phone_number()) {
929 SelectMoreIfPhoneNumber();
932 return rs;
935 void AccessibleCaretManager::SetSelectionDragState(bool aState) const {
936 RefPtr<nsFrameSelection> fs = GetFrameSelection();
937 if (fs) {
938 fs->SetDragState(aState);
942 bool AccessibleCaretManager::IsPhoneNumber(const nsAString& aCandidate) const {
943 RefPtr<Document> doc = mPresShell->GetDocument();
944 nsAutoString phoneNumberRegex(u"(^\\+)?[0-9 ,\\-.\\(\\)*#pw]{1,30}$"_ns);
945 return nsContentUtils::IsPatternMatching(aCandidate,
946 std::move(phoneNumberRegex), doc)
947 .valueOr(false);
950 void AccessibleCaretManager::SelectMoreIfPhoneNumber() const {
951 if (IsPhoneNumber(StringifiedSelection())) {
952 SetSelectionDirection(eDirNext);
953 ExtendPhoneNumberSelection(u"forward"_ns);
955 SetSelectionDirection(eDirPrevious);
956 ExtendPhoneNumberSelection(u"backward"_ns);
958 SetSelectionDirection(eDirNext);
962 void AccessibleCaretManager::ExtendPhoneNumberSelection(
963 const nsAString& aDirection) const {
964 if (!mPresShell) {
965 return;
968 // Extend the phone number selection until we find a boundary.
969 RefPtr<Selection> selection = GetSelection();
971 while (selection) {
972 const nsRange* anchorFocusRange = selection->GetAnchorFocusRange();
973 if (!anchorFocusRange) {
974 return;
977 // Backup the anchor focus range since both anchor node and focus node might
978 // be changed after calling Selection::Modify().
979 RefPtr<nsRange> oldAnchorFocusRange = anchorFocusRange->CloneRange();
981 // Save current focus node, focus offset and the selected text so that
982 // we can compare them with the modified ones later.
983 nsINode* oldFocusNode = selection->GetFocusNode();
984 uint32_t oldFocusOffset = selection->FocusOffset();
985 nsAutoString oldSelectedText = StringifiedSelection();
987 // Extend the selection by one char.
988 selection->Modify(u"extend"_ns, aDirection, u"character"_ns,
989 IgnoreErrors());
990 if (IsTerminated() == Terminated::Yes) {
991 return;
994 // If the selection didn't change, (can't extend further), we're done.
995 if (selection->GetFocusNode() == oldFocusNode &&
996 selection->FocusOffset() == oldFocusOffset) {
997 return;
1000 // If the changed selection isn't a valid phone number, we're done.
1001 // Also, if the selection was extended to a new block node, the string
1002 // returned by stringify() won't have a new line at the beginning or the
1003 // end of the string. Therefore, if either focus node or offset is
1004 // changed, but selected text is not changed, we're done, too.
1005 nsAutoString selectedText = StringifiedSelection();
1007 if (!IsPhoneNumber(selectedText) || oldSelectedText == selectedText) {
1008 // Backout the undesired selection extend, restore the old anchor focus
1009 // range before exit.
1010 selection->SetAnchorFocusToRange(oldAnchorFocusRange);
1011 return;
1016 void AccessibleCaretManager::SetSelectionDirection(nsDirection aDir) const {
1017 Selection* selection = GetSelection();
1018 if (selection) {
1019 selection->AdjustAnchorFocusForMultiRange(aDir);
1023 void AccessibleCaretManager::ClearMaintainedSelection() const {
1024 // Selection made by double-clicking for example will maintain the original
1025 // word selection. We should clear it so that we can drag caret freely.
1026 RefPtr<nsFrameSelection> fs = GetFrameSelection();
1027 if (fs) {
1028 fs->MaintainSelection(eSelectNoAmount);
1032 void AccessibleCaretManager::LayoutFlusher::MaybeFlush(
1033 const PresShell& aPresShell) {
1034 if (mAllowFlushing) {
1035 AutoRestore<bool> flushing(mFlushing);
1036 mFlushing = true;
1038 if (Document* doc = aPresShell.GetDocument()) {
1039 doc->FlushPendingNotifications(FlushType::Layout);
1040 // Don't access the PresShell after flushing, it could've become invalid.
1045 nsIFrame* AccessibleCaretManager::GetFrameForFirstRangeStartOrLastRangeEnd(
1046 nsDirection aDirection, int32_t* aOutOffset, nsIContent** aOutContent,
1047 int32_t* aOutContentOffset) const {
1048 if (!mPresShell) {
1049 return nullptr;
1052 MOZ_ASSERT(GetCaretMode() == CaretMode::Selection);
1053 MOZ_ASSERT(aOutOffset, "aOutOffset shouldn't be nullptr!");
1055 const nsRange* range = nullptr;
1056 RefPtr<nsINode> startNode;
1057 RefPtr<nsINode> endNode;
1058 int32_t nodeOffset = 0;
1059 CaretAssociationHint hint;
1061 RefPtr<Selection> selection = GetSelection();
1062 bool findInFirstRangeStart = aDirection == eDirNext;
1064 if (findInFirstRangeStart) {
1065 range = selection->GetRangeAt(0);
1066 startNode = range->GetStartContainer();
1067 endNode = range->GetEndContainer();
1068 nodeOffset = range->StartOffset();
1069 hint = CARET_ASSOCIATE_AFTER;
1070 } else {
1071 MOZ_ASSERT(selection->RangeCount() > 0);
1072 range = selection->GetRangeAt(selection->RangeCount() - 1);
1073 startNode = range->GetEndContainer();
1074 endNode = range->GetStartContainer();
1075 nodeOffset = range->EndOffset();
1076 hint = CARET_ASSOCIATE_BEFORE;
1079 nsCOMPtr<nsIContent> startContent = do_QueryInterface(startNode);
1080 nsIFrame* startFrame = nsFrameSelection::GetFrameForNodeOffset(
1081 startContent, nodeOffset, hint, aOutOffset);
1083 if (!startFrame) {
1084 ErrorResult err;
1085 RefPtr<TreeWalker> walker = mPresShell->GetDocument()->CreateTreeWalker(
1086 *startNode, dom::NodeFilter_Binding::SHOW_ALL, nullptr, err);
1088 if (!walker) {
1089 return nullptr;
1092 startFrame = startContent ? startContent->GetPrimaryFrame() : nullptr;
1093 while (!startFrame && startNode != endNode) {
1094 startNode = findInFirstRangeStart ? walker->NextNode(err)
1095 : walker->PreviousNode(err);
1097 if (!startNode) {
1098 break;
1101 startContent = startNode->AsContent();
1102 startFrame = startContent ? startContent->GetPrimaryFrame() : nullptr;
1105 // We are walking among the nodes in the content tree, so the node offset
1106 // relative to startNode should be set to 0.
1107 nodeOffset = 0;
1108 *aOutOffset = 0;
1111 if (startFrame) {
1112 if (aOutContent) {
1113 startContent.forget(aOutContent);
1115 if (aOutContentOffset) {
1116 *aOutContentOffset = nodeOffset;
1120 return startFrame;
1123 bool AccessibleCaretManager::RestrictCaretDraggingOffsets(
1124 nsIFrame::ContentOffsets& aOffsets) {
1125 if (!mPresShell) {
1126 return false;
1129 MOZ_ASSERT(GetCaretMode() == CaretMode::Selection);
1131 nsDirection dir =
1132 mActiveCaret == mCarets.GetFirst() ? eDirPrevious : eDirNext;
1133 int32_t offset = 0;
1134 nsCOMPtr<nsIContent> content;
1135 int32_t contentOffset = 0;
1136 nsIFrame* frame = GetFrameForFirstRangeStartOrLastRangeEnd(
1137 dir, &offset, getter_AddRefs(content), &contentOffset);
1139 if (!frame) {
1140 return false;
1143 // Compare the active caret's new position (aOffsets) to the inactive caret's
1144 // position.
1145 NS_ASSERTION(contentOffset >= 0, "contentOffset should not be negative");
1146 const Maybe<int32_t> cmpToInactiveCaretPos =
1147 nsContentUtils::ComparePoints_AllowNegativeOffsets(
1148 aOffsets.content, aOffsets.StartOffset(), content, contentOffset);
1149 if (NS_WARN_IF(!cmpToInactiveCaretPos)) {
1150 // Potentially handle this properly when Selection across Shadow DOM
1151 // boundary is implemented
1152 // (https://bugzilla.mozilla.org/show_bug.cgi?id=1607497).
1153 return false;
1156 // Move one character (in the direction of dir) from the inactive caret's
1157 // position. This is the limit for the active caret's new position.
1158 PeekOffsetStruct limit(
1159 eSelectCluster, dir, offset, nsPoint(0, 0),
1160 {PeekOffsetOption::JumpLines, PeekOffsetOption::ScrollViewStop});
1161 nsresult rv = frame->PeekOffset(&limit);
1162 if (NS_FAILED(rv)) {
1163 limit.mResultContent = content;
1164 limit.mContentOffset = contentOffset;
1167 // Compare the active caret's new position (aOffsets) to the limit.
1168 NS_ASSERTION(limit.mContentOffset >= 0,
1169 "limit.mContentOffset should not be negative");
1170 const Maybe<int32_t> cmpToLimit =
1171 nsContentUtils::ComparePoints_AllowNegativeOffsets(
1172 aOffsets.content, aOffsets.StartOffset(), limit.mResultContent,
1173 limit.mContentOffset);
1174 if (NS_WARN_IF(!cmpToLimit)) {
1175 // Potentially handle this properly when Selection across Shadow DOM
1176 // boundary is implemented
1177 // (https://bugzilla.mozilla.org/show_bug.cgi?id=1607497).
1178 return false;
1181 auto SetOffsetsToLimit = [&aOffsets, &limit]() {
1182 aOffsets.content = limit.mResultContent;
1183 aOffsets.offset = limit.mContentOffset;
1184 aOffsets.secondaryOffset = limit.mContentOffset;
1187 if (!StaticPrefs::
1188 layout_accessiblecaret_allow_dragging_across_other_caret()) {
1189 if ((mActiveCaret == mCarets.GetFirst() && *cmpToLimit == 1) ||
1190 (mActiveCaret == mCarets.GetSecond() && *cmpToLimit == -1)) {
1191 // The active caret's position is past the limit, which we don't allow
1192 // here. So set it to the limit, resulting in one character being
1193 // selected.
1194 SetOffsetsToLimit();
1196 } else {
1197 switch (*cmpToInactiveCaretPos) {
1198 case 0:
1199 // The active caret's position is the same as the position of the
1200 // inactive caret. So set it to the limit to prevent the selection from
1201 // being collapsed, resulting in one character being selected.
1202 SetOffsetsToLimit();
1203 break;
1204 case 1:
1205 if (mActiveCaret == mCarets.GetFirst()) {
1206 // First caret was moved across the second caret. After making change
1207 // to the selection, the user will drag the second caret.
1208 mActiveCaret = mCarets.GetSecond();
1210 break;
1211 case -1:
1212 if (mActiveCaret == mCarets.GetSecond()) {
1213 // Second caret was moved across the first caret. After making change
1214 // to the selection, the user will drag the first caret.
1215 mActiveCaret = mCarets.GetFirst();
1217 break;
1221 return true;
1224 bool AccessibleCaretManager::CompareTreePosition(nsIFrame* aStartFrame,
1225 nsIFrame* aEndFrame) const {
1226 return (aStartFrame && aEndFrame &&
1227 nsLayoutUtils::CompareTreePosition(aStartFrame, aEndFrame) <= 0);
1230 nsresult AccessibleCaretManager::DragCaretInternal(const nsPoint& aPoint) {
1231 MOZ_ASSERT(mPresShell);
1233 nsIFrame* rootFrame = mPresShell->GetRootFrame();
1234 MOZ_ASSERT(rootFrame, "We need root frame to compute caret dragging!");
1236 nsPoint point = AdjustDragBoundary(
1237 nsPoint(aPoint.x, aPoint.y + mOffsetYToCaretLogicalPosition));
1239 // Find out which content we point to
1241 nsIFrame* ptFrame = nsLayoutUtils::GetFrameForPoint(
1242 RelativeTo{rootFrame}, point, GetHitTestOptions());
1243 if (!ptFrame) {
1244 return NS_ERROR_FAILURE;
1247 RefPtr<nsFrameSelection> fs = GetFrameSelection();
1248 MOZ_ASSERT(fs);
1250 nsresult result;
1251 nsIFrame* newFrame = nullptr;
1252 nsPoint newPoint;
1253 nsPoint ptInFrame = point;
1254 nsLayoutUtils::TransformPoint(RelativeTo{rootFrame}, RelativeTo{ptFrame},
1255 ptInFrame);
1256 result = fs->ConstrainFrameAndPointToAnchorSubtree(ptFrame, ptInFrame,
1257 &newFrame, newPoint);
1258 if (NS_FAILED(result) || !newFrame) {
1259 return NS_ERROR_FAILURE;
1262 if (!newFrame->IsSelectable(nullptr)) {
1263 return NS_ERROR_FAILURE;
1266 nsIFrame::ContentOffsets offsets =
1267 newFrame->GetContentOffsetsFromPoint(newPoint);
1268 if (offsets.IsNull()) {
1269 return NS_ERROR_FAILURE;
1272 if (GetCaretMode() == CaretMode::Selection &&
1273 !RestrictCaretDraggingOffsets(offsets)) {
1274 return NS_ERROR_FAILURE;
1277 ClearMaintainedSelection();
1279 const nsFrameSelection::FocusMode focusMode =
1280 (GetCaretMode() == CaretMode::Selection)
1281 ? nsFrameSelection::FocusMode::kExtendSelection
1282 : nsFrameSelection::FocusMode::kCollapseToNewPoint;
1283 fs->HandleClick(MOZ_KnownLive(offsets.content) /* bug 1636889 */,
1284 offsets.StartOffset(), offsets.EndOffset(), focusMode,
1285 offsets.associate);
1286 return NS_OK;
1289 // static
1290 nsRect AccessibleCaretManager::GetAllChildFrameRectsUnion(nsIFrame* aFrame) {
1291 nsRect unionRect;
1293 // Drill through scroll frames, we don't want to include scrollbar child
1294 // frames below.
1295 for (nsIFrame* frame = aFrame->GetContentInsertionFrame(); frame;
1296 frame = frame->GetNextContinuation()) {
1297 nsRect frameRect;
1299 for (const auto& childList : frame->ChildLists()) {
1300 // Loop all children to union their scrollable overflow rect.
1301 for (nsIFrame* child : childList.mList) {
1302 nsRect childRect = child->ScrollableOverflowRectRelativeToSelf();
1303 nsLayoutUtils::TransformRect(child, frame, childRect);
1305 // A TextFrame containing only '\n' has positive height and width 0, or
1306 // positive width and height 0 if it's vertical. Need to use UnionEdges
1307 // to add its rect. BRFrame rect should be non-empty.
1308 if (childRect.IsEmpty()) {
1309 frameRect = frameRect.UnionEdges(childRect);
1310 } else {
1311 frameRect = frameRect.Union(childRect);
1316 MOZ_ASSERT(!frameRect.IsEmpty(),
1317 "Editable frames should have at least one BRFrame child to make "
1318 "frameRect non-empty!");
1319 if (frame != aFrame) {
1320 nsLayoutUtils::TransformRect(frame, aFrame, frameRect);
1322 unionRect = unionRect.Union(frameRect);
1325 return unionRect;
1328 nsPoint AccessibleCaretManager::AdjustDragBoundary(
1329 const nsPoint& aPoint) const {
1330 nsPoint adjustedPoint = aPoint;
1332 int32_t focusOffset = 0;
1333 nsIFrame* focusFrame =
1334 nsCaret::GetFrameAndOffset(GetSelection(), nullptr, 0, &focusOffset);
1335 Element* editingHost = GetEditingHostForFrame(focusFrame);
1337 if (editingHost) {
1338 nsIFrame* editingHostFrame = editingHost->GetPrimaryFrame();
1339 if (editingHostFrame) {
1340 nsRect boundary =
1341 AccessibleCaretManager::GetAllChildFrameRectsUnion(editingHostFrame);
1342 nsLayoutUtils::TransformRect(editingHostFrame, mPresShell->GetRootFrame(),
1343 boundary);
1345 // Shrink the rect to make sure we never hit the boundary.
1346 boundary.Deflate(kBoundaryAppUnits);
1348 adjustedPoint = boundary.ClampPoint(adjustedPoint);
1352 if (GetCaretMode() == CaretMode::Selection &&
1353 !StaticPrefs::
1354 layout_accessiblecaret_allow_dragging_across_other_caret()) {
1355 // Bug 1068474: Adjust the Y-coordinate so that the carets won't be in tilt
1356 // mode when a caret is being dragged surpass the other caret.
1358 // For example, when dragging the second caret, the horizontal boundary
1359 // (lower bound) of its Y-coordinate is the logical position of the first
1360 // caret. Likewise, when dragging the first caret, the horizontal boundary
1361 // (upper bound) of its Y-coordinate is the logical position of the second
1362 // caret.
1363 if (mActiveCaret == mCarets.GetFirst()) {
1364 nscoord dragDownBoundaryY = mCarets.GetSecond()->LogicalPosition().y;
1365 if (dragDownBoundaryY > 0 && adjustedPoint.y > dragDownBoundaryY) {
1366 adjustedPoint.y = dragDownBoundaryY;
1368 } else {
1369 nscoord dragUpBoundaryY = mCarets.GetFirst()->LogicalPosition().y;
1370 if (adjustedPoint.y < dragUpBoundaryY) {
1371 adjustedPoint.y = dragUpBoundaryY;
1376 return adjustedPoint;
1379 void AccessibleCaretManager::StartSelectionAutoScrollTimer(
1380 const nsPoint& aPoint) const {
1381 Selection* selection = GetSelection();
1382 MOZ_ASSERT(selection);
1384 nsIFrame* anchorFrame = selection->GetPrimaryFrameForAnchorNode();
1385 if (!anchorFrame) {
1386 return;
1389 nsIScrollableFrame* scrollFrame = nsLayoutUtils::GetNearestScrollableFrame(
1390 anchorFrame, nsLayoutUtils::SCROLLABLE_SAME_DOC |
1391 nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN);
1392 if (!scrollFrame) {
1393 return;
1396 nsIFrame* capturingFrame = scrollFrame->GetScrolledFrame();
1397 if (!capturingFrame) {
1398 return;
1401 nsIFrame* rootFrame = mPresShell->GetRootFrame();
1402 MOZ_ASSERT(rootFrame);
1403 nsPoint ptInScrolled = aPoint;
1404 nsLayoutUtils::TransformPoint(RelativeTo{rootFrame},
1405 RelativeTo{capturingFrame}, ptInScrolled);
1407 RefPtr<nsFrameSelection> fs = GetFrameSelection();
1408 MOZ_ASSERT(fs);
1409 fs->StartAutoScrollTimer(capturingFrame, ptInScrolled, kAutoScrollTimerDelay);
1412 void AccessibleCaretManager::StopSelectionAutoScrollTimer() const {
1413 RefPtr<nsFrameSelection> fs = GetFrameSelection();
1414 MOZ_ASSERT(fs);
1415 fs->StopAutoScrollTimer();
1418 void AccessibleCaretManager::DispatchCaretStateChangedEvent(
1419 CaretChangedReason aReason, const nsPoint* aPoint) {
1420 if (MaybeFlushLayout() == Terminated::Yes) {
1421 return;
1424 const Selection* sel = GetSelection();
1425 if (!sel) {
1426 return;
1429 Document* doc = mPresShell->GetDocument();
1430 MOZ_ASSERT(doc);
1432 CaretStateChangedEventInit init;
1433 init.mBubbles = true;
1435 const nsRange* range = sel->GetAnchorFocusRange();
1436 nsINode* commonAncestorNode = nullptr;
1437 if (range) {
1438 commonAncestorNode = range->GetClosestCommonInclusiveAncestor();
1441 if (!commonAncestorNode) {
1442 commonAncestorNode = sel->GetFrameSelection()->GetAncestorLimiter();
1445 RefPtr<DOMRect> domRect = new DOMRect(ToSupports(doc));
1446 nsRect rect = nsLayoutUtils::GetSelectionBoundingRect(sel);
1448 nsIFrame* commonAncestorFrame = nullptr;
1449 nsIFrame* rootFrame = mPresShell->GetRootFrame();
1451 if (commonAncestorNode && commonAncestorNode->IsContent()) {
1452 commonAncestorFrame = commonAncestorNode->AsContent()->GetPrimaryFrame();
1455 if (commonAncestorFrame && rootFrame) {
1456 nsLayoutUtils::TransformRect(rootFrame, commonAncestorFrame, rect);
1457 nsRect clampedRect =
1458 nsLayoutUtils::ClampRectToScrollFrames(commonAncestorFrame, rect);
1459 nsLayoutUtils::TransformRect(commonAncestorFrame, rootFrame, clampedRect);
1460 rect = clampedRect;
1461 init.mSelectionVisible = !clampedRect.IsEmpty();
1462 } else {
1463 init.mSelectionVisible = true;
1466 domRect->SetLayoutRect(rect);
1468 // Send isEditable info w/ event detail. This info can help determine
1469 // whether to show cut command on selection dialog or not.
1470 init.mSelectionEditable =
1471 commonAncestorFrame && GetEditingHostForFrame(commonAncestorFrame);
1473 init.mBoundingClientRect = domRect;
1474 init.mReason = aReason;
1475 init.mCollapsed = sel->IsCollapsed();
1476 init.mCaretVisible = mCarets.HasLogicallyVisibleCaret();
1477 init.mCaretVisuallyVisible = mCarets.HasVisuallyVisibleCaret();
1478 init.mSelectedTextContent = StringifiedSelection();
1480 if (aPoint) {
1481 CSSIntPoint pt = CSSPixel::FromAppUnitsRounded(*aPoint);
1482 init.mClientX = pt.x;
1483 init.mClientY = pt.y;
1486 RefPtr<CaretStateChangedEvent> event = CaretStateChangedEvent::Constructor(
1487 doc, u"mozcaretstatechanged"_ns, init);
1488 event->SetTrusted(true);
1490 AC_LOG("%s: reason %" PRIu32 ", collapsed %d, caretVisible %" PRIu32,
1491 __FUNCTION__, static_cast<uint32_t>(init.mReason), init.mCollapsed,
1492 static_cast<uint32_t>(init.mCaretVisible));
1494 (new AsyncEventDispatcher(doc, event.forget(), ChromeOnlyDispatch::eYes))
1495 ->PostDOMEvent();
1498 AccessibleCaretManager::Carets::Carets(UniquePtr<AccessibleCaret> aFirst,
1499 UniquePtr<AccessibleCaret> aSecond)
1500 : mFirst{std::move(aFirst)}, mSecond{std::move(aSecond)} {}
1502 } // namespace mozilla