Source code

Revision control

Copy as Markdown

Other Tools

/* -*- 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 T>
class nsTArray;
template <class T>
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?
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]
//
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;
nsPoint mDefaultScrollShift;
// Rect of the original containg block.
nsRect mOriginalContainingBlockRect;
// Adjusted containing block, by position-area or grid, as per
// TODO(dshin, bug 2004596): "or" should be "and/or."
nsRect mAdjustedContainingBlock;
// TODO(dshin, bug 1987962): 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.
mozilla::PhysicalAxes mCompensatingForScroll;
};
struct LastSuccessfulPositionData {
RefPtr<const ComputedStyle> 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<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
*/
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.
*/
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);
};
} // namespace mozilla
#endif // AnchorPositioningUtils_h_