/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #ifndef AnchorPositioningUtils_h_ #define AnchorPositioningUtils_h_ #include "mozilla/Maybe.h" #include "mozilla/WritingModes.h" #include "nsRect.h" class nsAtom; class nsIFrame; template class nsTArray; template class CopyableTArray; namespace mozilla { class nsDisplayListBuilder; struct AnchorPosInfo { // Border-box of the anchor frame, offset against the positioned frame's // absolute containing block's padding box. nsRect mRect; // See `AnchorPosOffsetData::mCompensatesForScroll`. bool mCompensatesForScroll; }; class DistanceToNearestScrollContainer { public: DistanceToNearestScrollContainer() = default; explicit DistanceToNearestScrollContainer(uint32_t aDistance) : mDistance{aDistance} {} bool Valid() const { return mDistance != kInvalid; } bool operator==(const DistanceToNearestScrollContainer&) const = default; bool operator!=(const DistanceToNearestScrollContainer&) const = default; private: // 0 is invalid - a frame itself cannot be its own nearest scroll container. static constexpr uint32_t kInvalid = 0; // Ancestor hops to the nearest scroll container. Note that scroll containers // between abspos/fixedpos frames and their containing blocks are irrelevant, // so the distance should be measured from the out-of-flow frame, not the // placeholder frame. uint32_t mDistance = kInvalid; }; struct AnchorPosOffsetData { // Origin of the referenced anchor, w.r.t. containing block at the time of // resolution. nsPoint mOrigin; // Does this anchor's offset compensate for scroll? // https://drafts.csswg.org/css-anchor-position-1/#compensate-for-scroll bool mCompensatesForScroll = false; // Distance to this anchor's nearest scroll container. DistanceToNearestScrollContainer mDistanceToNearestScrollContainer; }; class ScopedNameRef { public: ScopedNameRef(const nsAtom* aAtom, const StyleCascadeLevel& aTreeScope) : mName(aAtom), mTreeScope(aTreeScope) {} const nsAtom* mName = nullptr; StyleCascadeLevel mTreeScope = StyleCascadeLevel::Default(); }; class nsScopedNameRefHashKey : public PLDHashEntryHdr { public: using KeyType = ScopedNameRef; using KeyTypePointer = const ScopedNameRef*; explicit nsScopedNameRefHashKey(const ScopedNameRef* aKey) : mAtom(aKey->mName), mTreeScope(aKey->mTreeScope) { MOZ_ASSERT(aKey); MOZ_ASSERT(aKey->mName); } nsScopedNameRefHashKey(const nsScopedNameRefHashKey& aOther) = delete; nsScopedNameRefHashKey(nsScopedNameRefHashKey&& aOther) = default; ~nsScopedNameRefHashKey() = default; KeyType GetKey() const { return ScopedNameRef(mAtom, mTreeScope); } bool KeyEquals(KeyTypePointer aKey) const { // This should work because a positioned element can't make two references // with the same name in different tree scopes. Further scope resolution is // hard to do here because the map does not have all the context. return aKey->mName == mAtom.get(); } static KeyTypePointer KeyToPointer(const KeyType& aKey) { return &aKey; } static PLDHashNumber HashKey(KeyTypePointer aKey) { return MOZ_LIKELY(aKey && aKey->mName) ? aKey->mName->hash() : 0; } enum { ALLOW_MEMMOVE = true }; private: RefPtr mAtom; StyleCascadeLevel mTreeScope; }; // Resolved anchor positioning data. struct AnchorPosResolutionData { // Size of the referenced anchor. nsSize mSize; // Offset resolution data. Nothing if the anchor did not resolve, or if the // anchor was only referred to by its size. Maybe mOffsetData; StyleCascadeLevel mAnchorTreeScope; }; // Data required for an anchor positioned frame, including: // * If valid anchors are found, // * Cached offset/size resolution, if resolution was valid, // * Compensating for scroll [1] // * Default scroll shift [2] // // [1]: https://drafts.csswg.org/css-anchor-position-1/#compensate-for-scroll // [2]: https://drafts.csswg.org/css-anchor-position-1/#default-scroll-shift class AnchorPosReferenceData { private: using ResolutionMap = nsBaseHashtable, mozilla::Maybe>; public: // Backup data for attempting a different `@position-try` style, when // the default anchor remains the same. // These entries correspond 1:1 to that of `AnchorPosReferenceData`. struct PositionTryBackup { mozilla::PhysicalAxes mCompensatingForScroll; nsPoint mDefaultScrollShift; nsRect mAdjustedContainingBlock; SideBits mScrollCompensatedSides; nsMargin mInsets; }; using Value = mozilla::Maybe; AnchorPosReferenceData() = default; AnchorPosReferenceData(const AnchorPosReferenceData&) = delete; AnchorPosReferenceData(AnchorPosReferenceData&&) = default; AnchorPosReferenceData& operator=(const AnchorPosReferenceData&) = delete; AnchorPosReferenceData& operator=(AnchorPosReferenceData&&) = default; struct Result { bool mAlreadyResolved; Value* mEntry; }; Result InsertOrModify(const ScopedNameRef& aKey, bool aNeedOffset); const Value* Lookup(const ScopedNameRef& aKey) const; bool IsEmpty() const { return mMap.IsEmpty(); } ResolutionMap::const_iterator begin() const { return mMap.cbegin(); } ResolutionMap::const_iterator end() const { return mMap.cend(); } void AdjustCompensatingForScroll(const mozilla::PhysicalAxes& aAxes) { mCompensatingForScroll += aAxes; } mozilla::PhysicalAxes CompensatingForScrollAxes() const { return mCompensatingForScroll; } PositionTryBackup TryPositionWithSameDefaultAnchor() { auto compensatingForScroll = std::exchange(mCompensatingForScroll, {}); auto defaultScrollShift = std::exchange(mDefaultScrollShift, {}); auto adjustedContainingBlock = std::exchange(mAdjustedContainingBlock, {}); auto containingBlockSidesAttachedToAnchor = std::exchange(mScrollCompensatedSides, SideBits::eNone); auto insets = std::exchange(mInsets, nsMargin{}); return {compensatingForScroll, defaultScrollShift, adjustedContainingBlock, containingBlockSidesAttachedToAnchor, insets}; } void UndoTryPositionWithSameDefaultAnchor(PositionTryBackup&& aBackup) { mCompensatingForScroll = aBackup.mCompensatingForScroll; mDefaultScrollShift = aBackup.mDefaultScrollShift; mAdjustedContainingBlock = aBackup.mAdjustedContainingBlock; mScrollCompensatedSides = aBackup.mScrollCompensatedSides; mInsets = aBackup.mInsets; } // Distance from the default anchor to the nearest scroll container. DistanceToNearestScrollContainer mDistanceToDefaultScrollContainer; // https://drafts.csswg.org/css-anchor-position-1/#default-scroll-shift nsPoint mDefaultScrollShift; // Rect of the original containg block. nsRect mOriginalContainingBlockRect; // Adjusted containing block, by position-area or grid, as per // https://drafts.csswg.org/css-position/#original-cb // TODO(dshin, bug 2004596): "or" should be "and/or." nsRect mAdjustedContainingBlock; // TODO(dshin, bug 1987962): Remembered scroll offset // https://drafts.csswg.org/css-anchor-position-1/#remembered-scroll-offset // Name of the default used anchor. Not necessarily positioned frame's // style, because of fallbacks. RefPtr mDefaultAnchorName; // Flag indicating which sides of the containing block attach to the // scroll-compensated anchor. Whenever a scroll-compensated anchor scrolls, it // effectively moves around w.r.t. its absolute containing block. This // effectively changes the size of the containing block. For example, given: // // * Absolute containing block of 50px height, // * Scroller, under the abs CB, with the scrolled content height of 100px, // * Anchor element, under the scroller, of 30px height, and // * Positioned element of 30px height, attached to anchor at the bottom. // // The positioned element would overflow the abs CB, until the scroller moves // down by 10px. We address this by defining sides of the CB that scrolls // with the anchor, so that whenever we carry out an overflow check, we move // those sides by the scroll offset, while pinning the rest of the sides to // the original containing block. SideBits mScrollCompensatedSides = SideBits::eNone; // Resolved insets for this positioned element. Modifies the adjusted & // scrolled containing block. nsMargin mInsets; StyleCascadeLevel mAnchorTreeScope = StyleCascadeLevel::Default(); private: ResolutionMap mMap; // Axes we need to compensate for scroll [1] in. // [1]: https://drafts.csswg.org/css-anchor-position-1/#compensate-for-scroll mozilla::PhysicalAxes mCompensatingForScroll; }; struct LastSuccessfulPositionData { RefPtr mStyle; uint32_t mIndex = 0; bool mTriedAllFallbacks = false; }; struct StylePositionArea; class WritingMode; struct AnchorPosDefaultAnchorCache { // Default anchor element's corresponding frame. const nsIFrame* mAnchor = nullptr; // Scroll container for the default anchor. const nsIFrame* mScrollContainer = nullptr; AnchorPosDefaultAnchorCache() = default; AnchorPosDefaultAnchorCache(const nsIFrame* aAnchor, const nsIFrame* aScrollContainer); }; // Cache data used by anchor resolution. To be populated on abspos reflow, // whenever the frame makes any anchor reference. struct AnchorPosResolutionCache { // Storage for referenced anchors. Designed to be long-lived (i.e. beyond // a reflow cycle). AnchorPosReferenceData* mReferenceData = nullptr; // Cached data for default anchor resolution. Designed to be short-lived, // so it can contain e.g. frame pointers. AnchorPosDefaultAnchorCache mDefaultAnchorCache; // Backup data for attempting a different `@position-try` style, when // the default anchor remains the same. using PositionTryBackup = AnchorPosReferenceData::PositionTryBackup; PositionTryBackup TryPositionWithSameDefaultAnchor() { return mReferenceData->TryPositionWithSameDefaultAnchor(); } void UndoTryPositionWithSameDefaultAnchor(PositionTryBackup&& aBackup) { mReferenceData->UndoTryPositionWithSameDefaultAnchor(std::move(aBackup)); } // Backup data for attempting a different `@position-try` style, when // the default anchor changes. using PositionTryFullBackup = std::pair; PositionTryFullBackup TryPositionWithDifferentDefaultAnchor() { auto referenceData = std::move(*mReferenceData); *mReferenceData = {}; return std::make_pair( std::move(referenceData), std::exchange(mDefaultAnchorCache, AnchorPosDefaultAnchorCache{})); } void UndoTryPositionWithDifferentDefaultAnchor( PositionTryFullBackup&& aBackup) { *mReferenceData = std::move(aBackup.first); std::exchange(mDefaultAnchorCache, aBackup.second); } }; enum class StylePositionTryFallbacksTryTacticKeyword : uint8_t; using StylePositionTryFallbacksTryTactic = CopyableTArray; /** * AnchorPositioningUtils is a namespace class used for various anchor * positioning helper functions that are useful in multiple places. * The goal is to avoid code duplication and to avoid having too * many helpers in nsLayoutUtils. */ struct AnchorPositioningUtils { /** * Finds the first acceptable frame from the list of possible anchor frames * following https://drafts.csswg.org/css-anchor-position-1/#target */ static nsIFrame* FindFirstAcceptableAnchor( const ScopedNameRef& aName, const nsIFrame* aPositionedFrame, const nsTArray& aPossibleAnchorFrames); static Maybe GetAnchorPosRect( const nsIFrame* aAbsoluteContainingBlock, const nsIFrame* aAnchor, bool aCBRectIsvalid); static Maybe ResolveAnchorPosRect( const nsIFrame* aPositioned, const nsIFrame* aAbsoluteContainingBlock, const ScopedNameRef& aAnchorName, bool aCBRectIsvalid, AnchorPosResolutionCache* aResolutionCache); static Maybe ResolveAnchorPosSize( const nsIFrame* aPositioned, const ScopedNameRef& aAnchorName, AnchorPosResolutionCache* aResolutionCache); /** * Adjust the containing block rect for the 'position-area' property. * https://drafts.csswg.org/css-anchor-position-1/#position-area */ static nsRect AdjustAbsoluteContainingBlockRectForPositionArea( const nsRect& aAnchorRect, const nsRect& aCBRect, WritingMode aPositionedWM, WritingMode aCBWM, const StylePositionArea& aPosArea, StylePositionArea* aOutResolvedArea); /** * Gets the used anchor name for an anchor positioned frame. * * @param aPositioned The anchor positioned frame. * @param aAnchorName The anchor name specified in the anchor function, * or nullptr if not specified. * * If `aAnchorName` is not specified, then this function will return the * default anchor name, if the `position-anchor` property specified one. * Otherwise it will return `nsGkAtoms::AnchorPosImplicitAnchor` if the * element has an implicit anchor, or a nullptr. */ static Maybe GetUsedAnchorName( const nsIFrame* aPositioned, const ScopedNameRef& aAnchorName); /** * Get the implicit anchor of the frame. * * @param aFrame The anchor positioned frame. * * For pseudo-elements, this returns the parent frame of the originating * element. For popovers, this returns the primary frame of the invoker. In * all other cases, returns null. */ enum class ImplicitAnchorKind : uint8_t { None, Popover, PseudoElement }; struct ImplicitAnchorResult { nsIFrame* mAnchorFrame = nullptr; ImplicitAnchorKind mKind = ImplicitAnchorKind::None; }; static ImplicitAnchorResult GetAnchorPosImplicitAnchor( const nsIFrame* aFrame); struct NearestScrollFrameInfo { const nsIFrame* mScrollContainer = nullptr; DistanceToNearestScrollContainer mDistance; }; static NearestScrollFrameInfo GetNearestScrollFrame(const nsIFrame* aFrame); static nsPoint GetScrollOffsetFor( PhysicalAxes aAxes, const nsIFrame* aPositioned, const AnchorPosDefaultAnchorCache& aDefaultAnchorCache); struct ContainingBlockInfo { // Provide an explicit containing block size, for during reflow when // its `mRect` has not yet been set. static ContainingBlockInfo ExplicitCBFrameSize( const nsRect& aContainingBlockRect); // Provide the positioned frame, to query its containing block rect. static ContainingBlockInfo UseCBFrameSize(const nsIFrame* aPositioned); nsRect GetContainingBlockRect() const { return mRect; } private: explicit ContainingBlockInfo(const nsRect& aRect) : mRect{aRect} {} nsRect mRect; }; static bool FitsInContainingBlock(const nsIFrame* aPositioned, const AnchorPosReferenceData&); /** * If aFrame is positioned using CSS anchor positioning, and it scrolls with * its anchor this function returns the anchor. Otherwise null. * Note that this function has different behaviour if it called during paint * (ie aBuilder not null) or not during painting (aBuilder null). */ static nsIFrame* GetAnchorThatFrameScrollsWith(nsIFrame* aFrame, nsDisplayListBuilder* aBuilder, bool aSkipAsserts = false); // Trigger a layout for positioned items that are currently overflowing their // abs-cb and that have available fallbacks to try. static bool TriggerLayoutOnOverflow(PresShell*, bool aFirstIteration); static StylePositionArea PhysicalizePositionArea(StylePositionArea aPosArea, const nsIFrame* aPositioned); /** * When an anchor is split across fragmentainers such as multiple columns or * pages, this function reconstructs what its unfragmented bounding rect would * be by walking through the containing block's continuations and stacking all * the anchor's fragment rects vertically in the containing block's block-axis * direction. The returned rect is relative to the containing block's * first-continuation. During reflow, we simply cache the unfragmented anchor * rect as the resolution cache is populated, and use it, so this isn't * required. However, we need to be able to recompute the anchor out-of-reflow * to see if we need to trigger reflow. */ static nsRect ReassembleAnchorRect(const nsIFrame* aAnchor, const nsIFrame* aContainingBlock); }; } // namespace mozilla #endif // AnchorPositioningUtils_h_