Bug 1890689 apply drift correction to input rate instead of output rate r=pehrsons
[gecko.git] / editor / libeditor / SelectionState.h
blob704246d7c2cc61aa95a5ade44921dd9c149011f0
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 #ifndef mozilla_SelectionState_h
7 #define mozilla_SelectionState_h
9 #include "mozilla/EditorDOMPoint.h"
10 #include "mozilla/EditorForwards.h"
11 #include "mozilla/Maybe.h"
12 #include "mozilla/OwningNonNull.h"
13 #include "mozilla/dom/Document.h"
14 #include "nsCOMPtr.h"
15 #include "nsDirection.h"
16 #include "nsINode.h"
17 #include "nsRange.h"
18 #include "nsTArray.h"
19 #include "nscore.h"
21 class nsCycleCollectionTraversalCallback;
22 class nsRange;
23 namespace mozilla {
24 namespace dom {
25 class Element;
26 class Selection;
27 class Text;
28 } // namespace dom
30 /**
31 * A helper struct for saving/setting ranges.
33 struct RangeItem final {
34 RangeItem() : mStartOffset(0), mEndOffset(0) {}
36 private:
37 // Private destructor, to discourage deletion outside of Release():
38 ~RangeItem() = default;
40 public:
41 void StoreRange(const nsRange& aRange);
42 void StoreRange(const EditorRawDOMPoint& aStartPoint,
43 const EditorRawDOMPoint& aEndPoint) {
44 MOZ_ASSERT(aStartPoint.IsSet());
45 MOZ_ASSERT(aEndPoint.IsSet());
46 mStartContainer = aStartPoint.GetContainer();
47 mStartOffset = aStartPoint.Offset();
48 mEndContainer = aEndPoint.GetContainer();
49 mEndOffset = aEndPoint.Offset();
51 void Clear() {
52 mStartContainer = mEndContainer = nullptr;
53 mStartOffset = mEndOffset = 0;
55 already_AddRefed<nsRange> GetRange() const;
57 // Same as the API of dom::AbstractRange
58 [[nodiscard]] nsINode* GetRoot() const;
59 [[nodiscard]] bool Collapsed() const {
60 return mStartContainer == mEndContainer && mStartOffset == mEndOffset;
62 [[nodiscard]] bool IsPositioned() const {
63 return mStartContainer && mEndContainer;
65 [[nodiscard]] bool Equals(const RangeItem& aOther) const {
66 return mStartContainer == aOther.mStartContainer &&
67 mEndContainer == aOther.mEndContainer &&
68 mStartOffset == aOther.mStartOffset &&
69 mEndOffset == aOther.mEndOffset;
71 template <typename EditorDOMPointType = EditorDOMPoint>
72 EditorDOMPointType StartPoint() const {
73 return EditorDOMPointType(mStartContainer, mStartOffset);
75 template <typename EditorDOMPointType = EditorDOMPoint>
76 EditorDOMPointType EndPoint() const {
77 return EditorDOMPointType(mEndContainer, mEndOffset);
80 NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(RangeItem)
81 NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(RangeItem)
83 nsCOMPtr<nsINode> mStartContainer;
84 nsCOMPtr<nsINode> mEndContainer;
85 uint32_t mStartOffset;
86 uint32_t mEndOffset;
89 /**
90 * mozilla::SelectionState
92 * Class for recording selection info. Stores selection as collection of
93 * { {startnode, startoffset} , {endnode, endoffset} } tuples. Can't store
94 * ranges since dom gravity will possibly change the ranges.
97 class SelectionState final {
98 public:
99 SelectionState() = default;
100 explicit SelectionState(const AutoRangeArray& aRanges);
103 * Same as the API as dom::Selection
105 [[nodiscard]] bool IsCollapsed() const {
106 if (mArray.Length() != 1) {
107 return false;
109 return mArray[0]->Collapsed();
112 void RemoveAllRanges() {
113 mArray.Clear();
114 mDirection = eDirNext;
117 [[nodiscard]] uint32_t RangeCount() const { return mArray.Length(); }
120 * Saving all ranges of aSelection.
122 void SaveSelection(dom::Selection& aSelection);
125 * Setting aSelection to have all ranges stored by this instance.
127 MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult
128 RestoreSelection(dom::Selection& aSelection);
131 * Setting aRanges to have all ranges stored by this instance.
133 void ApplyTo(AutoRangeArray& aRanges);
136 * HasOnlyCollapsedRange() returns true only when there is a positioned range
137 * which is collapsed. I.e., the selection represents a caret point.
139 [[nodiscard]] bool HasOnlyCollapsedRange() const {
140 if (mArray.Length() != 1) {
141 return false;
143 if (!mArray[0]->IsPositioned() || !mArray[0]->Collapsed()) {
144 return false;
146 return true;
150 * Equals() returns true only when there are same number of ranges and
151 * all their containers and offsets are exactly same. This won't check
152 * the validity of each range with the current DOM tree.
154 [[nodiscard]] bool Equals(const SelectionState& aOther) const;
157 * Returns common root node of all ranges' start and end containers.
158 * Some of them have different root nodes, this returns nullptr.
160 [[nodiscard]] nsINode* GetCommonRootNode() const {
161 nsINode* rootNode = nullptr;
162 for (const RefPtr<RangeItem>& rangeItem : mArray) {
163 nsINode* newRootNode = rangeItem->GetRoot();
164 if (!newRootNode || (rootNode && rootNode != newRootNode)) {
165 return nullptr;
167 rootNode = newRootNode;
169 return rootNode;
172 private:
173 CopyableAutoTArray<RefPtr<RangeItem>, 1> mArray;
174 nsDirection mDirection = eDirNext;
176 friend class RangeUpdater;
177 friend void ImplCycleCollectionTraverse(nsCycleCollectionTraversalCallback&,
178 SelectionState&, const char*,
179 uint32_t);
180 friend void ImplCycleCollectionUnlink(SelectionState&);
183 inline void ImplCycleCollectionTraverse(
184 nsCycleCollectionTraversalCallback& aCallback, SelectionState& aField,
185 const char* aName, uint32_t aFlags = 0) {
186 ImplCycleCollectionTraverse(aCallback, aField.mArray, aName, aFlags);
189 inline void ImplCycleCollectionUnlink(SelectionState& aField) {
190 ImplCycleCollectionUnlink(aField.mArray);
193 class MOZ_STACK_CLASS RangeUpdater final {
194 public:
195 RangeUpdater();
197 void RegisterRangeItem(RangeItem& aRangeItem);
198 void DropRangeItem(RangeItem& aRangeItem);
199 void RegisterSelectionState(SelectionState& aSelectionState);
200 void DropSelectionState(SelectionState& aSelectionState);
202 // editor selection gravity routines. Note that we can't always depend on
203 // DOM Range gravity to do what we want to the "real" selection. For
204 // instance, if you move a node, that corresponds to deleting it and
205 // reinserting it. DOM Range gravity will promote the selection out of the
206 // node on deletion, which is not what you want if you know you are
207 // reinserting it.
208 template <typename PT, typename CT>
209 nsresult SelAdjCreateNode(const EditorDOMPointBase<PT, CT>& aPoint);
210 template <typename PT, typename CT>
211 nsresult SelAdjInsertNode(const EditorDOMPointBase<PT, CT>& aPoint);
212 void SelAdjDeleteNode(nsINode& aNode);
215 * SelAdjSplitNode() is called immediately after spliting aOriginalNode
216 * and inserted aNewContent into the DOM tree.
218 * @param aOriginalContent The node which was split.
219 * @param aSplitOffset The old offset in aOriginalContent at splitting
220 * it.
221 * @param aNewContent The new content node which was inserted into
222 * the DOM tree.
224 nsresult SelAdjSplitNode(nsIContent& aOriginalContent, uint32_t aSplitOffset,
225 nsIContent& aNewContent);
228 * SelAdjJoinNodes() is called immediately after joining aRemovedContent and
229 * the container of aStartOfRightContent.
231 * @param aStartOfRightContent The container is joined content node which
232 * now has all children or text data which were
233 * in aRemovedContent. And this points where
234 * the joined position.
235 * @param aRemovedContent The removed content.
236 * @param aOldPointAtRightContent The point where the right content node was
237 * before joining them. The offset must have
238 * been initialized before the joining.
240 nsresult SelAdjJoinNodes(const EditorRawDOMPoint& aStartOfRightContent,
241 const nsIContent& aRemovedContent,
242 const EditorDOMPoint& aOldPointAtRightContent);
243 void SelAdjInsertText(const dom::Text& aTextNode, uint32_t aOffset,
244 uint32_t aInsertedLength);
245 void SelAdjDeleteText(const dom::Text& aTextNode, uint32_t aOffset,
246 uint32_t aDeletedLength);
247 void SelAdjReplaceText(const dom::Text& aTextNode, uint32_t aOffset,
248 uint32_t aReplacedLength, uint32_t aInsertedLength);
249 // the following gravity routines need will/did sandwiches, because the other
250 // gravity routines will be called inside of these sandwiches, but should be
251 // ignored.
252 void WillReplaceContainer() {
253 // XXX Isn't this possible with mutation event listener?
254 NS_WARNING_ASSERTION(!mLocked, "Has already been locked");
255 mLocked = true;
257 void DidReplaceContainer(const dom::Element& aRemovedElement,
258 dom::Element& aInsertedElement);
259 void WillRemoveContainer() {
260 // XXX Isn't this possible with mutation event listener?
261 NS_WARNING_ASSERTION(!mLocked, "Has already been locked");
262 mLocked = true;
264 void DidRemoveContainer(const dom::Element& aRemovedElement,
265 nsINode& aRemovedElementContainerNode,
266 uint32_t aOldOffsetOfRemovedElement,
267 uint32_t aOldChildCountOfRemovedElement);
268 void WillInsertContainer() {
269 // XXX Isn't this possible with mutation event listener?
270 NS_WARNING_ASSERTION(!mLocked, "Has already been locked");
271 mLocked = true;
273 void DidInsertContainer() {
274 NS_WARNING_ASSERTION(mLocked, "Not locked");
275 mLocked = false;
277 void DidMoveNode(const nsINode& aOldParent, uint32_t aOldOffset,
278 const nsINode& aNewParent, uint32_t aNewOffset);
280 private:
281 // TODO: A lot of loop in these methods check whether each item `nullptr` or
282 // not. We should make it not nullable later.
283 nsTArray<RefPtr<RangeItem>> mArray;
284 bool mLocked;
288 * Helper class for using SelectionState. Stack based class for doing
289 * preservation of dom points across editor actions.
292 class MOZ_STACK_CLASS AutoTrackDOMPoint final {
293 public:
294 AutoTrackDOMPoint() = delete;
295 AutoTrackDOMPoint(RangeUpdater& aRangeUpdater, nsCOMPtr<nsINode>* aNode,
296 uint32_t* aOffset)
297 : mRangeUpdater(aRangeUpdater),
298 mNode(aNode),
299 mOffset(aOffset),
300 mRangeItem(do_AddRef(new RangeItem())),
301 mWasConnected(aNode && (*aNode)->IsInComposedDoc()) {
302 mRangeItem->mStartContainer = *mNode;
303 mRangeItem->mEndContainer = *mNode;
304 mRangeItem->mStartOffset = *mOffset;
305 mRangeItem->mEndOffset = *mOffset;
306 mDocument = (*mNode)->OwnerDoc();
307 mRangeUpdater.RegisterRangeItem(mRangeItem);
310 AutoTrackDOMPoint(RangeUpdater& aRangeUpdater, EditorDOMPoint* aPoint)
311 : mRangeUpdater(aRangeUpdater),
312 mNode(nullptr),
313 mOffset(nullptr),
314 mPoint(Some(aPoint->IsSet() ? aPoint : nullptr)),
315 mRangeItem(do_AddRef(new RangeItem())),
316 mWasConnected(aPoint && aPoint->IsInComposedDoc()) {
317 if (!aPoint->IsSet()) {
318 mIsTracking = false;
319 return; // Nothing should be tracked.
321 mRangeItem->mStartContainer = aPoint->GetContainer();
322 mRangeItem->mEndContainer = aPoint->GetContainer();
323 mRangeItem->mStartOffset = aPoint->Offset();
324 mRangeItem->mEndOffset = aPoint->Offset();
325 mDocument = aPoint->GetContainer()->OwnerDoc();
326 mRangeUpdater.RegisterRangeItem(mRangeItem);
329 ~AutoTrackDOMPoint() { FlushAndStopTracking(); }
331 void FlushAndStopTracking() {
332 if (!mIsTracking) {
333 return;
335 mIsTracking = false;
336 if (mPoint.isSome()) {
337 mRangeUpdater.DropRangeItem(mRangeItem);
338 // Setting `mPoint` with invalid DOM point causes hitting `NS_ASSERTION()`
339 // and the number of times may be too many. (E.g., 1533913.html hits
340 // over 700 times!) We should just put warning instead.
341 if (NS_WARN_IF(!mRangeItem->mStartContainer)) {
342 mPoint.ref()->Clear();
343 return;
345 // If the node was removed from the original document, clear the instance
346 // since the user should not keep handling the adopted or orphan node
347 // anymore.
348 if (NS_WARN_IF(mWasConnected &&
349 !mRangeItem->mStartContainer->IsInComposedDoc()) ||
350 NS_WARN_IF(mRangeItem->mStartContainer->OwnerDoc() != mDocument)) {
351 mPoint.ref()->Clear();
352 return;
354 if (NS_WARN_IF(mRangeItem->mStartContainer->Length() <
355 mRangeItem->mStartOffset)) {
356 mPoint.ref()->SetToEndOf(mRangeItem->mStartContainer);
357 return;
359 mPoint.ref()->Set(mRangeItem->mStartContainer, mRangeItem->mStartOffset);
360 return;
362 mRangeUpdater.DropRangeItem(mRangeItem);
363 *mNode = mRangeItem->mStartContainer;
364 *mOffset = mRangeItem->mStartOffset;
365 if (!(*mNode)) {
366 return;
368 // If the node was removed from the original document, clear the instances
369 // since the user should not keep handling the adopted or orphan node
370 // anymore.
371 if (NS_WARN_IF(mWasConnected && !(*mNode)->IsInComposedDoc()) ||
372 NS_WARN_IF((*mNode)->OwnerDoc() != mDocument)) {
373 *mNode = nullptr;
374 *mOffset = 0;
378 void StopTracking() { mIsTracking = false; }
380 private:
381 RangeUpdater& mRangeUpdater;
382 // Allow tracking nsINode until nsNode is gone
383 nsCOMPtr<nsINode>* mNode;
384 uint32_t* mOffset;
385 Maybe<EditorDOMPoint*> mPoint;
386 OwningNonNull<RangeItem> mRangeItem;
387 RefPtr<dom::Document> mDocument;
388 bool mIsTracking = true;
389 bool mWasConnected;
392 class MOZ_STACK_CLASS AutoTrackDOMRange final {
393 public:
394 AutoTrackDOMRange() = delete;
395 AutoTrackDOMRange(RangeUpdater& aRangeUpdater, EditorDOMPoint* aStartPoint,
396 EditorDOMPoint* aEndPoint)
397 : mRangeRefPtr(nullptr), mRangeOwningNonNull(nullptr) {
398 mStartPointTracker.emplace(aRangeUpdater, aStartPoint);
399 mEndPointTracker.emplace(aRangeUpdater, aEndPoint);
401 AutoTrackDOMRange(RangeUpdater& aRangeUpdater, EditorDOMRange* aRange)
402 : mRangeRefPtr(nullptr), mRangeOwningNonNull(nullptr) {
403 mStartPointTracker.emplace(
404 aRangeUpdater, const_cast<EditorDOMPoint*>(&aRange->StartRef()));
405 mEndPointTracker.emplace(aRangeUpdater,
406 const_cast<EditorDOMPoint*>(&aRange->EndRef()));
408 AutoTrackDOMRange(RangeUpdater& aRangeUpdater, RefPtr<nsRange>* aRange)
409 : mStartPoint((*aRange)->StartRef()),
410 mEndPoint((*aRange)->EndRef()),
411 mRangeRefPtr(aRange),
412 mRangeOwningNonNull(nullptr) {
413 mStartPointTracker.emplace(aRangeUpdater, &mStartPoint);
414 mEndPointTracker.emplace(aRangeUpdater, &mEndPoint);
416 AutoTrackDOMRange(RangeUpdater& aRangeUpdater, OwningNonNull<nsRange>* aRange)
417 : mStartPoint((*aRange)->StartRef()),
418 mEndPoint((*aRange)->EndRef()),
419 mRangeRefPtr(nullptr),
420 mRangeOwningNonNull(aRange) {
421 mStartPointTracker.emplace(aRangeUpdater, &mStartPoint);
422 mEndPointTracker.emplace(aRangeUpdater, &mEndPoint);
424 ~AutoTrackDOMRange() { FlushAndStopTracking(); }
426 void FlushAndStopTracking() {
427 if (!mStartPointTracker && !mEndPointTracker) {
428 return;
430 mStartPointTracker.reset();
431 mEndPointTracker.reset();
432 if (!mRangeRefPtr && !mRangeOwningNonNull) {
433 // This must be created with EditorDOMRange or EditorDOMPoints. In the
434 // cases, destroying mStartPointTracker and mEndPointTracker has done
435 // everything which we need to do.
436 return;
438 // Otherwise, update the DOM ranges by ourselves.
439 if (mRangeRefPtr) {
440 if (!mStartPoint.IsSet() || !mEndPoint.IsSet()) {
441 (*mRangeRefPtr)->Reset();
442 return;
444 (*mRangeRefPtr)
445 ->SetStartAndEnd(mStartPoint.ToRawRangeBoundary(),
446 mEndPoint.ToRawRangeBoundary());
447 return;
449 if (mRangeOwningNonNull) {
450 if (!mStartPoint.IsSet() || !mEndPoint.IsSet()) {
451 (*mRangeOwningNonNull)->Reset();
452 return;
454 (*mRangeOwningNonNull)
455 ->SetStartAndEnd(mStartPoint.ToRawRangeBoundary(),
456 mEndPoint.ToRawRangeBoundary());
457 return;
461 void StopTracking() {
462 if (mStartPointTracker) {
463 mStartPointTracker->StopTracking();
465 if (mEndPointTracker) {
466 mEndPointTracker->StopTracking();
469 void StopTrackingStartBoundary() {
470 MOZ_ASSERT(!mRangeRefPtr,
471 "StopTrackingStartBoundary() is not available when tracking "
472 "RefPtr<nsRange>");
473 MOZ_ASSERT(!mRangeOwningNonNull,
474 "StopTrackingStartBoundary() is not available when tracking "
475 "OwningNonNull<nsRange>");
476 if (!mStartPointTracker) {
477 return;
479 mStartPointTracker->StopTracking();
481 void StopTrackingEndBoundary() {
482 MOZ_ASSERT(!mRangeRefPtr,
483 "StopTrackingEndBoundary() is not available when tracking "
484 "RefPtr<nsRange>");
485 MOZ_ASSERT(!mRangeOwningNonNull,
486 "StopTrackingEndBoundary() is not available when tracking "
487 "OwningNonNull<nsRange>");
488 if (!mEndPointTracker) {
489 return;
491 mEndPointTracker->StopTracking();
494 private:
495 Maybe<AutoTrackDOMPoint> mStartPointTracker;
496 Maybe<AutoTrackDOMPoint> mEndPointTracker;
497 EditorDOMPoint mStartPoint;
498 EditorDOMPoint mEndPoint;
499 RefPtr<nsRange>* mRangeRefPtr;
500 OwningNonNull<nsRange>* mRangeOwningNonNull;
504 * Another helper class for SelectionState. Stack based class for doing
505 * Will/DidReplaceContainer()
508 class MOZ_STACK_CLASS AutoReplaceContainerSelNotify final {
509 public:
510 AutoReplaceContainerSelNotify() = delete;
511 // FYI: Marked as `MOZ_CAN_RUN_SCRIPT` for avoiding to use strong pointers
512 // for the members.
513 MOZ_CAN_RUN_SCRIPT
514 AutoReplaceContainerSelNotify(RangeUpdater& aRangeUpdater,
515 dom::Element& aOriginalElement,
516 dom::Element& aNewElement)
517 : mRangeUpdater(aRangeUpdater),
518 mOriginalElement(aOriginalElement),
519 mNewElement(aNewElement) {
520 mRangeUpdater.WillReplaceContainer();
523 ~AutoReplaceContainerSelNotify() {
524 mRangeUpdater.DidReplaceContainer(mOriginalElement, mNewElement);
527 private:
528 RangeUpdater& mRangeUpdater;
529 dom::Element& mOriginalElement;
530 dom::Element& mNewElement;
534 * Another helper class for SelectionState. Stack based class for doing
535 * Will/DidRemoveContainer()
538 class MOZ_STACK_CLASS AutoRemoveContainerSelNotify final {
539 public:
540 AutoRemoveContainerSelNotify() = delete;
541 AutoRemoveContainerSelNotify(RangeUpdater& aRangeUpdater,
542 const EditorRawDOMPoint& aAtRemovingElement)
543 : mRangeUpdater(aRangeUpdater),
544 mRemovingElement(*aAtRemovingElement.GetChild()->AsElement()),
545 mParentNode(*aAtRemovingElement.GetContainer()),
546 mOffsetInParent(aAtRemovingElement.Offset()),
547 mChildCountOfRemovingElement(mRemovingElement->GetChildCount()) {
548 MOZ_ASSERT(aAtRemovingElement.IsSet());
549 mRangeUpdater.WillRemoveContainer();
552 ~AutoRemoveContainerSelNotify() {
553 mRangeUpdater.DidRemoveContainer(mRemovingElement, mParentNode,
554 mOffsetInParent,
555 mChildCountOfRemovingElement);
558 private:
559 RangeUpdater& mRangeUpdater;
560 OwningNonNull<dom::Element> mRemovingElement;
561 OwningNonNull<nsINode> mParentNode;
562 uint32_t mOffsetInParent;
563 uint32_t mChildCountOfRemovingElement;
567 * Another helper class for SelectionState. Stack based class for doing
568 * Will/DidInsertContainer()
569 * XXX The lock state isn't useful if the edit action is triggered from
570 * a mutation event listener so that looks like that we can remove
571 * this class.
574 class MOZ_STACK_CLASS AutoInsertContainerSelNotify final {
575 private:
576 RangeUpdater& mRangeUpdater;
578 public:
579 AutoInsertContainerSelNotify() = delete;
580 explicit AutoInsertContainerSelNotify(RangeUpdater& aRangeUpdater)
581 : mRangeUpdater(aRangeUpdater) {
582 mRangeUpdater.WillInsertContainer();
585 ~AutoInsertContainerSelNotify() { mRangeUpdater.DidInsertContainer(); }
589 * Another helper class for SelectionState. Stack based class for doing
590 * DidMoveNode()
593 class MOZ_STACK_CLASS AutoMoveNodeSelNotify final {
594 public:
595 AutoMoveNodeSelNotify() = delete;
596 AutoMoveNodeSelNotify(RangeUpdater& aRangeUpdater,
597 const EditorRawDOMPoint& aOldPoint,
598 const EditorRawDOMPoint& aNewPoint)
599 : mRangeUpdater(aRangeUpdater),
600 mOldParent(*aOldPoint.GetContainer()),
601 mNewParent(*aNewPoint.GetContainer()),
602 mOldOffset(aOldPoint.Offset()),
603 mNewOffset(aNewPoint.Offset()) {
604 MOZ_ASSERT(aOldPoint.IsSet());
605 MOZ_ASSERT(aNewPoint.IsSet());
608 ~AutoMoveNodeSelNotify() {
609 mRangeUpdater.DidMoveNode(mOldParent, mOldOffset, mNewParent, mNewOffset);
612 private:
613 RangeUpdater& mRangeUpdater;
614 nsINode& mOldParent;
615 nsINode& mNewParent;
616 const uint32_t mOldOffset;
617 const uint32_t mNewOffset;
620 } // namespace mozilla
622 #endif // #ifndef mozilla_SelectionState_h