443 lines
17 KiB
C++
443 lines
17 KiB
C++
/* 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 T>
|
|
class nsTArray;
|
|
|
|
template <class T>
|
|
class CopyableTArray;
|
|
|
|
namespace mozilla {
|
|
|
|
namespace dom {
|
|
class ShadowRoot;
|
|
}
|
|
|
|
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<const nsAtom> 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<AnchorPosOffsetData> 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<nsScopedNameRefHashKey,
|
|
mozilla::Maybe<AnchorPosResolutionData>,
|
|
mozilla::Maybe<AnchorPosResolutionData>>;
|
|
|
|
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<AnchorPosResolutionData>;
|
|
|
|
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<const nsAtom> 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 {
|
|
// The style + index of our last reflow.
|
|
RefPtr<const ComputedStyle> mLastStyle;
|
|
Maybe<uint32_t> mLastIndex;
|
|
// The "recorded" index that we start looking fallbacks from.
|
|
// https://drafts.csswg.org/css-anchor-position/#last-successful-recording
|
|
Maybe<uint32_t> mRecordedIndex;
|
|
// Whether we tried all fallbacks or not.
|
|
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<AnchorPosReferenceData, AnchorPosDefaultAnchorCache>;
|
|
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<StylePositionTryFallbacksTryTacticKeyword>;
|
|
|
|
/**
|
|
* 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<nsIFrame*>& aPossibleAnchorFrames);
|
|
|
|
static Maybe<nsRect> GetAnchorPosRect(
|
|
const nsIFrame* aAbsoluteContainingBlock, const nsIFrame* aAnchor,
|
|
bool aCBRectIsValid);
|
|
|
|
static Maybe<AnchorPosInfo> ResolveAnchorPosRect(
|
|
const nsIFrame* aPositioned, const nsIFrame* aAbsoluteContainingBlock,
|
|
const ScopedNameRef& aAnchorName, bool aCBRectIsValid,
|
|
AnchorPosResolutionCache* aResolutionCache);
|
|
|
|
static Maybe<nsSize> 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<ScopedNameRef> 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);
|
|
|
|
// Helper to get shadow root for a property's tree scope
|
|
static const dom::ShadowRoot* GetShadowRootForTreeScope(
|
|
const dom::Element& aElement, const StyleCascadeLevel& aTreeScope);
|
|
};
|
|
|
|
} // namespace mozilla
|
|
|
|
#endif // AnchorPositioningUtils_h_
|