Bug 1772588 [wpt PR 34302] - [wpt] Add test for block-in-inline offsetParent., a...
[gecko.git] / editor / libeditor / SelectionState.h
blob6d71d70d23b6b24ef959eba511381a00a4a7ff14
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 "nsCOMPtr.h"
14 #include "nsDirection.h"
15 #include "nsINode.h"
16 #include "nsRange.h"
17 #include "nsTArray.h"
18 #include "nscore.h"
20 class nsCycleCollectionTraversalCallback;
21 class nsRange;
22 namespace mozilla {
23 namespace dom {
24 class Element;
25 class Selection;
26 class Text;
27 } // namespace dom
29 /**
30 * A helper struct for saving/setting ranges.
32 struct RangeItem final {
33 RangeItem() : mStartOffset(0), mEndOffset(0) {}
35 private:
36 // Private destructor, to discourage deletion outside of Release():
37 ~RangeItem() = default;
39 public:
40 void StoreRange(const nsRange& aRange);
41 void StoreRange(const EditorRawDOMPoint& aStartPoint,
42 const EditorRawDOMPoint& aEndPoint) {
43 MOZ_ASSERT(aStartPoint.IsSet());
44 MOZ_ASSERT(aEndPoint.IsSet());
45 mStartContainer = aStartPoint.GetContainer();
46 mStartOffset = aStartPoint.Offset();
47 mEndContainer = aEndPoint.GetContainer();
48 mEndOffset = aEndPoint.Offset();
50 void Clear() {
51 mStartContainer = mEndContainer = nullptr;
52 mStartOffset = mEndOffset = 0;
54 already_AddRefed<nsRange> GetRange() const;
56 // Same as the API of dom::AbstractRange
57 [[nodiscard]] nsINode* GetRoot() const;
58 [[nodiscard]] bool Collapsed() const {
59 return mStartContainer == mEndContainer && mStartOffset == mEndOffset;
61 [[nodiscard]] bool IsPositioned() const {
62 return mStartContainer && mEndContainer;
64 [[nodiscard]] bool Equals(const RangeItem& aOther) const {
65 return mStartContainer == aOther.mStartContainer &&
66 mEndContainer == aOther.mEndContainer &&
67 mStartOffset == aOther.mStartOffset &&
68 mEndOffset == aOther.mEndOffset;
70 EditorDOMPoint StartPoint() const {
71 return EditorDOMPoint(mStartContainer, mStartOffset);
73 EditorDOMPoint EndPoint() const {
74 return EditorDOMPoint(mEndContainer, mEndOffset);
76 EditorRawDOMPoint StartRawPoint() const {
77 return EditorRawDOMPoint(mStartContainer, mStartOffset);
79 EditorRawDOMPoint EndRawPoint() const {
80 return EditorRawDOMPoint(mEndContainer, mEndOffset);
83 NS_INLINE_DECL_MAIN_THREAD_ONLY_CYCLE_COLLECTING_NATIVE_REFCOUNTING(RangeItem)
84 NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(RangeItem)
86 nsCOMPtr<nsINode> mStartContainer;
87 nsCOMPtr<nsINode> mEndContainer;
88 uint32_t mStartOffset;
89 uint32_t mEndOffset;
92 /**
93 * mozilla::SelectionState
95 * Class for recording selection info. Stores selection as collection of
96 * { {startnode, startoffset} , {endnode, endoffset} } tuples. Can't store
97 * ranges since dom gravity will possibly change the ranges.
100 class SelectionState final {
101 public:
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 * HasOnlyCollapsedRange() returns true only when there is a positioned range
132 * which is collapsed. I.e., the selection represents a caret point.
134 [[nodiscard]] bool HasOnlyCollapsedRange() const {
135 if (mArray.Length() != 1) {
136 return false;
138 if (!mArray[0]->IsPositioned() || !mArray[0]->Collapsed()) {
139 return false;
141 return true;
145 * Equals() returns true only when there are same number of ranges and
146 * all their containers and offsets are exactly same. This won't check
147 * the validity of each range with the current DOM tree.
149 [[nodiscard]] bool Equals(const SelectionState& aOther) const;
152 * Returns common root node of all ranges' start and end containers.
153 * Some of them have different root nodes, this returns nullptr.
155 [[nodiscard]] nsINode* GetCommonRootNode() const {
156 nsINode* rootNode = nullptr;
157 for (const RefPtr<RangeItem>& rangeItem : mArray) {
158 nsINode* newRootNode = rangeItem->GetRoot();
159 if (!newRootNode || (rootNode && rootNode != newRootNode)) {
160 return nullptr;
162 rootNode = newRootNode;
164 return rootNode;
167 private:
168 CopyableAutoTArray<RefPtr<RangeItem>, 1> mArray;
169 nsDirection mDirection = eDirNext;
171 friend class RangeUpdater;
172 friend void ImplCycleCollectionTraverse(nsCycleCollectionTraversalCallback&,
173 SelectionState&, const char*,
174 uint32_t);
175 friend void ImplCycleCollectionUnlink(SelectionState&);
178 inline void ImplCycleCollectionTraverse(
179 nsCycleCollectionTraversalCallback& aCallback, SelectionState& aField,
180 const char* aName, uint32_t aFlags = 0) {
181 ImplCycleCollectionTraverse(aCallback, aField.mArray, aName, aFlags);
184 inline void ImplCycleCollectionUnlink(SelectionState& aField) {
185 ImplCycleCollectionUnlink(aField.mArray);
188 class MOZ_STACK_CLASS RangeUpdater final {
189 public:
190 RangeUpdater();
192 void RegisterRangeItem(RangeItem& aRangeItem);
193 void DropRangeItem(RangeItem& aRangeItem);
194 void RegisterSelectionState(SelectionState& aSelectionState);
195 void DropSelectionState(SelectionState& aSelectionState);
197 // editor selection gravity routines. Note that we can't always depend on
198 // DOM Range gravity to do what we want to the "real" selection. For
199 // instance, if you move a node, that corresponds to deleting it and
200 // reinserting it. DOM Range gravity will promote the selection out of the
201 // node on deletion, which is not what you want if you know you are
202 // reinserting it.
203 template <typename PT, typename CT>
204 nsresult SelAdjCreateNode(const EditorDOMPointBase<PT, CT>& aPoint);
205 template <typename PT, typename CT>
206 nsresult SelAdjInsertNode(const EditorDOMPointBase<PT, CT>& aPoint);
207 void SelAdjDeleteNode(nsINode& aNode);
210 * SelAdjSplitNode() is called immediately after spliting aOriginalNode
211 * and inserted aNewContent into the DOM tree.
213 * @param aOriginalContent The node which was split.
214 * @param aSplitOffset The old offset in aOriginalContent at splitting
215 * it.
216 * @param aNewContent The new content node which was inserted into
217 * the DOM tree.
218 * @param aSplitNodeDirection Whether aNewNode was inserted before or after
219 * aOriginalContent.
221 nsresult SelAdjSplitNode(nsIContent& aOriginalContent, uint32_t aSplitOffset,
222 nsIContent& aNewContent,
223 SplitNodeDirection aSplitNodeDirection);
226 * SelAdjJoinNodes() is called immediately after joining aRemovedContent and
227 * the container of aStartOfRightContent.
229 * @param aStartOfRightContent The container is joined content node which
230 * now has all children or text data which were
231 * in aRemovedContent. And this points where
232 * the joined position.
233 * @param aRemovedContent The removed content.
234 * @param aOffsetOfRemovedContent The offset which aRemovedContent was in
235 * its ex-parent.
237 nsresult SelAdjJoinNodes(const EditorRawDOMPoint& aStartOfRightContent,
238 const nsIContent& aRemovedContent,
239 uint32_t aOffsetOfRemovedContent,
240 JoinNodesDirection aJoinNodesDirection);
241 void SelAdjInsertText(const dom::Text& aTextNode, uint32_t aOffset,
242 uint32_t aInsertedLength);
243 void SelAdjDeleteText(const dom::Text& aTextNode, uint32_t aOffset,
244 uint32_t aDeletedLength);
245 void SelAdjReplaceText(const dom::Text& aTextNode, uint32_t aOffset,
246 uint32_t aReplacedLength, uint32_t aInsertedLength);
247 // the following gravity routines need will/did sandwiches, because the other
248 // gravity routines will be called inside of these sandwiches, but should be
249 // ignored.
250 void WillReplaceContainer() {
251 // XXX Isn't this possible with mutation event listener?
252 NS_WARNING_ASSERTION(!mLocked, "Has already been locked");
253 mLocked = true;
255 void DidReplaceContainer(const dom::Element& aRemovedElement,
256 dom::Element& aInsertedElement);
257 void WillRemoveContainer() {
258 // XXX Isn't this possible with mutation event listener?
259 NS_WARNING_ASSERTION(!mLocked, "Has already been locked");
260 mLocked = true;
262 void DidRemoveContainer(const dom::Element& aRemovedElement,
263 nsINode& aRemovedElementContainerNode,
264 uint32_t aOldOffsetOfRemovedElement,
265 uint32_t aOldChildCountOfRemovedElement);
266 void WillInsertContainer() {
267 // XXX Isn't this possible with mutation event listener?
268 NS_WARNING_ASSERTION(!mLocked, "Has already been locked");
269 mLocked = true;
271 void DidInsertContainer() {
272 NS_WARNING_ASSERTION(mLocked, "Not locked");
273 mLocked = false;
275 void DidMoveNode(const nsINode& aOldParent, uint32_t aOldOffset,
276 const nsINode& aNewParent, uint32_t aNewOffset);
278 private:
279 // TODO: A lot of loop in these methods check whether each item `nullptr` or
280 // not. We should make it not nullable later.
281 nsTArray<RefPtr<RangeItem>> mArray;
282 bool mLocked;
286 * Helper class for using SelectionState. Stack based class for doing
287 * preservation of dom points across editor actions.
290 class MOZ_STACK_CLASS AutoTrackDOMPoint final {
291 public:
292 AutoTrackDOMPoint() = delete;
293 AutoTrackDOMPoint(RangeUpdater& aRangeUpdater, nsCOMPtr<nsINode>* aNode,
294 uint32_t* aOffset)
295 : mRangeUpdater(aRangeUpdater),
296 mNode(aNode),
297 mOffset(aOffset),
298 mRangeItem(do_AddRef(new RangeItem())) {
299 mRangeItem->mStartContainer = *mNode;
300 mRangeItem->mEndContainer = *mNode;
301 mRangeItem->mStartOffset = *mOffset;
302 mRangeItem->mEndOffset = *mOffset;
303 mRangeUpdater.RegisterRangeItem(mRangeItem);
306 AutoTrackDOMPoint(RangeUpdater& aRangeUpdater, EditorDOMPoint* aPoint)
307 : mRangeUpdater(aRangeUpdater),
308 mNode(nullptr),
309 mOffset(nullptr),
310 mPoint(Some(aPoint->IsSet() ? aPoint : nullptr)),
311 mRangeItem(do_AddRef(new RangeItem())) {
312 if (!aPoint->IsSet()) {
313 return; // Nothing should be tracked.
315 mRangeItem->mStartContainer = aPoint->GetContainer();
316 mRangeItem->mEndContainer = aPoint->GetContainer();
317 mRangeItem->mStartOffset = aPoint->Offset();
318 mRangeItem->mEndOffset = aPoint->Offset();
319 mRangeUpdater.RegisterRangeItem(mRangeItem);
322 ~AutoTrackDOMPoint() {
323 if (mPoint.isSome()) {
324 if (!mPoint.ref()) {
325 return; // We don't track anything.
327 mRangeUpdater.DropRangeItem(mRangeItem);
328 // Setting `mPoint` with invalid DOM point causes hitting `NS_ASSERTION()`
329 // and the number of times may be too many. (E.g., 1533913.html hits
330 // over 700 times!) We should just put warning instead.
331 if (NS_WARN_IF(!mRangeItem->mStartContainer)) {
332 mPoint.ref()->Clear();
333 return;
335 if (NS_WARN_IF(mRangeItem->mStartContainer->Length() <
336 mRangeItem->mStartOffset)) {
337 mPoint.ref()->SetToEndOf(mRangeItem->mStartContainer);
338 return;
340 mPoint.ref()->Set(mRangeItem->mStartContainer, mRangeItem->mStartOffset);
341 return;
343 mRangeUpdater.DropRangeItem(mRangeItem);
344 *mNode = mRangeItem->mStartContainer;
345 *mOffset = mRangeItem->mStartOffset;
348 private:
349 RangeUpdater& mRangeUpdater;
350 // Allow tracking nsINode until nsNode is gone
351 nsCOMPtr<nsINode>* mNode;
352 uint32_t* mOffset;
353 Maybe<EditorDOMPoint*> mPoint;
354 OwningNonNull<RangeItem> mRangeItem;
357 class MOZ_STACK_CLASS AutoTrackDOMRange final {
358 public:
359 AutoTrackDOMRange() = delete;
360 AutoTrackDOMRange(RangeUpdater& aRangeUpdater, EditorDOMPoint* aStartPoint,
361 EditorDOMPoint* aEndPoint)
362 : mRangeRefPtr(nullptr), mRangeOwningNonNull(nullptr) {
363 mStartPointTracker.emplace(aRangeUpdater, aStartPoint);
364 mEndPointTracker.emplace(aRangeUpdater, aEndPoint);
366 AutoTrackDOMRange(RangeUpdater& aRangeUpdater, EditorDOMRange* aRange)
367 : mRangeRefPtr(nullptr), mRangeOwningNonNull(nullptr) {
368 mStartPointTracker.emplace(
369 aRangeUpdater, const_cast<EditorDOMPoint*>(&aRange->StartRef()));
370 mEndPointTracker.emplace(aRangeUpdater,
371 const_cast<EditorDOMPoint*>(&aRange->EndRef()));
373 AutoTrackDOMRange(RangeUpdater& aRangeUpdater, RefPtr<nsRange>* aRange)
374 : mStartPoint((*aRange)->StartRef()),
375 mEndPoint((*aRange)->EndRef()),
376 mRangeRefPtr(aRange),
377 mRangeOwningNonNull(nullptr) {
378 mStartPointTracker.emplace(aRangeUpdater, &mStartPoint);
379 mEndPointTracker.emplace(aRangeUpdater, &mEndPoint);
381 AutoTrackDOMRange(RangeUpdater& aRangeUpdater, OwningNonNull<nsRange>* aRange)
382 : mStartPoint((*aRange)->StartRef()),
383 mEndPoint((*aRange)->EndRef()),
384 mRangeRefPtr(nullptr),
385 mRangeOwningNonNull(aRange) {
386 mStartPointTracker.emplace(aRangeUpdater, &mStartPoint);
387 mEndPointTracker.emplace(aRangeUpdater, &mEndPoint);
389 ~AutoTrackDOMRange() {
390 if (!mRangeRefPtr && !mRangeOwningNonNull) {
391 // The destructor of the trackers will update automatically.
392 return;
394 // Otherwise, destroy them now.
395 mStartPointTracker.reset();
396 mEndPointTracker.reset();
397 if (mRangeRefPtr) {
398 (*mRangeRefPtr)
399 ->SetStartAndEnd(mStartPoint.ToRawRangeBoundary(),
400 mEndPoint.ToRawRangeBoundary());
401 return;
403 if (mRangeOwningNonNull) {
404 (*mRangeOwningNonNull)
405 ->SetStartAndEnd(mStartPoint.ToRawRangeBoundary(),
406 mEndPoint.ToRawRangeBoundary());
407 return;
411 private:
412 Maybe<AutoTrackDOMPoint> mStartPointTracker;
413 Maybe<AutoTrackDOMPoint> mEndPointTracker;
414 EditorDOMPoint mStartPoint;
415 EditorDOMPoint mEndPoint;
416 RefPtr<nsRange>* mRangeRefPtr;
417 OwningNonNull<nsRange>* mRangeOwningNonNull;
421 * Another helper class for SelectionState. Stack based class for doing
422 * Will/DidReplaceContainer()
425 class MOZ_STACK_CLASS AutoReplaceContainerSelNotify final {
426 public:
427 AutoReplaceContainerSelNotify() = delete;
428 // FYI: Marked as `MOZ_CAN_RUN_SCRIPT` for avoiding to use strong pointers
429 // for the members.
430 MOZ_CAN_RUN_SCRIPT
431 AutoReplaceContainerSelNotify(RangeUpdater& aRangeUpdater,
432 dom::Element& aOriginalElement,
433 dom::Element& aNewElement)
434 : mRangeUpdater(aRangeUpdater),
435 mOriginalElement(aOriginalElement),
436 mNewElement(aNewElement) {
437 mRangeUpdater.WillReplaceContainer();
440 ~AutoReplaceContainerSelNotify() {
441 mRangeUpdater.DidReplaceContainer(mOriginalElement, mNewElement);
444 private:
445 RangeUpdater& mRangeUpdater;
446 dom::Element& mOriginalElement;
447 dom::Element& mNewElement;
451 * Another helper class for SelectionState. Stack based class for doing
452 * Will/DidRemoveContainer()
455 class MOZ_STACK_CLASS AutoRemoveContainerSelNotify final {
456 public:
457 AutoRemoveContainerSelNotify() = delete;
458 AutoRemoveContainerSelNotify(RangeUpdater& aRangeUpdater,
459 const EditorRawDOMPoint& aAtRemovingElement)
460 : mRangeUpdater(aRangeUpdater),
461 mRemovingElement(*aAtRemovingElement.GetChild()->AsElement()),
462 mParentNode(*aAtRemovingElement.GetContainer()),
463 mOffsetInParent(aAtRemovingElement.Offset()),
464 mChildCountOfRemovingElement(mRemovingElement->GetChildCount()) {
465 MOZ_ASSERT(aAtRemovingElement.IsSet());
466 mRangeUpdater.WillRemoveContainer();
469 ~AutoRemoveContainerSelNotify() {
470 mRangeUpdater.DidRemoveContainer(mRemovingElement, mParentNode,
471 mOffsetInParent,
472 mChildCountOfRemovingElement);
475 private:
476 RangeUpdater& mRangeUpdater;
477 OwningNonNull<dom::Element> mRemovingElement;
478 OwningNonNull<nsINode> mParentNode;
479 uint32_t mOffsetInParent;
480 uint32_t mChildCountOfRemovingElement;
484 * Another helper class for SelectionState. Stack based class for doing
485 * Will/DidInsertContainer()
486 * XXX The lock state isn't useful if the edit action is triggered from
487 * a mutation event listener so that looks like that we can remove
488 * this class.
491 class MOZ_STACK_CLASS AutoInsertContainerSelNotify final {
492 private:
493 RangeUpdater& mRangeUpdater;
495 public:
496 AutoInsertContainerSelNotify() = delete;
497 explicit AutoInsertContainerSelNotify(RangeUpdater& aRangeUpdater)
498 : mRangeUpdater(aRangeUpdater) {
499 mRangeUpdater.WillInsertContainer();
502 ~AutoInsertContainerSelNotify() { mRangeUpdater.DidInsertContainer(); }
506 * Another helper class for SelectionState. Stack based class for doing
507 * DidMoveNode()
510 class MOZ_STACK_CLASS AutoMoveNodeSelNotify final {
511 public:
512 AutoMoveNodeSelNotify() = delete;
513 AutoMoveNodeSelNotify(RangeUpdater& aRangeUpdater,
514 const EditorRawDOMPoint& aOldPoint,
515 const EditorRawDOMPoint& aNewPoint)
516 : mRangeUpdater(aRangeUpdater),
517 mOldParent(*aOldPoint.GetContainer()),
518 mNewParent(*aNewPoint.GetContainer()),
519 mOldOffset(aOldPoint.Offset()),
520 mNewOffset(aNewPoint.Offset()) {
521 MOZ_ASSERT(aOldPoint.IsSet());
522 MOZ_ASSERT(aNewPoint.IsSet());
525 ~AutoMoveNodeSelNotify() {
526 mRangeUpdater.DidMoveNode(mOldParent, mOldOffset, mNewParent, mNewOffset);
529 private:
530 RangeUpdater& mRangeUpdater;
531 nsINode& mOldParent;
532 nsINode& mNewParent;
533 const uint32_t mOldOffset;
534 const uint32_t mNewOffset;
537 } // namespace mozilla
539 #endif // #ifndef mozilla_SelectionState_h