| File: | Volumes/Data/worker/Apple-iOS-26-Safer-CPP-Checks-EWS/build/Source/WebCore/style/AnchorPositionEvaluator.cpp |
| Warning: | line 157, column 11 Local variable 'view' is uncounted and unsafe |
Press '?' to see keyboard shortcuts
Keyboard shortcuts:
| 1 | /* |
| 2 | * Copyright (C) 2024-2025 Apple Inc. All rights reserved. |
| 3 | * |
| 4 | * Redistribution and use in source and binary forms, with or without |
| 5 | * modification, are permitted provided that the following conditions |
| 6 | * are met: |
| 7 | * 1. Redistributions of source code must retain the above copyright |
| 8 | * notice, this list of conditions and the following disclaimer. |
| 9 | * 2. Redistributions in binary form must reproduce the above copyright |
| 10 | * notice, this list of conditions and the following disclaimer in the |
| 11 | * documentation and/or other materials provided with the distribution. |
| 12 | * |
| 13 | * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND ANY |
| 14 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
| 15 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| 16 | * DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY |
| 17 | * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES |
| 18 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
| 19 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON |
| 20 | * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| 21 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
| 22 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| 23 | */ |
| 24 | |
| 25 | #include "config.h" |
| 26 | #include "AnchorPositionEvaluator.h" |
| 27 | |
| 28 | #include "CSSPropertyNames.h" |
| 29 | #include "CSSValue.h" |
| 30 | #include "CSSValueKeywords.h" |
| 31 | #include "ContainerNodeInlines.h" |
| 32 | #include "Document.h" |
| 33 | #include "Element.h" |
| 34 | #include "Node.h" |
| 35 | #include "NodeRenderStyle.h" |
| 36 | #include "PopoverData.h" |
| 37 | #include "PositionedLayoutConstraints.h" |
| 38 | #include "RenderBoxInlines.h" |
| 39 | #include "RenderBlock.h" |
| 40 | #include "RenderBoxModelObjectInlines.h" |
| 41 | #include "RenderFragmentedFlow.h" |
| 42 | #include "RenderInline.h" |
| 43 | #include "RenderLayer.h" |
| 44 | #include "RenderLayerCompositor.h" |
| 45 | #include "RenderObjectInlines.h" |
| 46 | #include "RenderStyle.h" |
| 47 | #include "RenderStyle+GettersInlines.h" |
| 48 | #include "RenderStyle+SettersInlines.h" |
| 49 | #include "RenderView.h" |
| 50 | #include "StyleBuilderState.h" |
| 51 | #include "StyleScope.h" |
| 52 | #include "WritingMode.h" |
| 53 | #include <ranges> |
| 54 | |
| 55 | namespace WebCore { |
| 56 | |
| 57 | AnchorScrollSnapshot::AnchorScrollSnapshot(const RenderBox& scroller, LayoutPoint snapshot) |
| 58 | : m_scroller(scroller) |
| 59 | , m_scrollSnapshot(snapshot) |
| 60 | { } |
| 61 | |
| 62 | AnchorScrollSnapshot::AnchorScrollSnapshot(LayoutPoint snapshot) |
| 63 | : m_scroller(nullptr) |
| 64 | , m_scrollSnapshot(snapshot) |
| 65 | { } |
| 66 | |
| 67 | inline LayoutSize AnchorScrollSnapshot::adjustmentForCurrentScrollPosition() const |
| 68 | { |
| 69 | if (m_scroller) |
| 70 | return m_scrollSnapshot - m_scroller->scrollPosition(); |
| 71 | return { }; |
| 72 | } |
| 73 | |
| 74 | inline bool AnchorScrollAdjuster::isEmpty() const |
| 75 | { |
| 76 | return !m_scrollSnapshots.size() && !(m_hasChainedAnchor | m_hasStickyAnchor); |
| 77 | } |
| 78 | |
| 79 | AnchorScrollAdjuster::AnchorScrollAdjuster(RenderBox& anchored, const RenderBoxModelObject& defaultAnchor) |
| 80 | : m_anchored(anchored) |
| 81 | { |
| 82 | auto& style = anchored.style(); |
| 83 | |
| 84 | auto compensatedAxes = style.anchorFunctionScrollCompensatedAxes(); |
| 85 | m_needsXAdjustment = compensatedAxes.contains(BoxAxis::Horizontal); |
| 86 | m_needsYAdjustment = compensatedAxes.contains(BoxAxis::Vertical); |
| 87 | |
| 88 | auto containingWritingMode = anchored.container()->style().writingMode(); |
| 89 | if (auto positionAreaValue = style.positionArea().tryValue()) { |
| 90 | if (positionAreaValue->coordMatchedTrackForAxis(BoxAxis::Horizontal, containingWritingMode, style.writingMode()) != Style::PositionAreaTrack::SpanAll) |
| 91 | m_needsXAdjustment |= true; |
| 92 | else { |
| 93 | if (containingWritingMode.isHorizontal()) { |
| 94 | auto alignment = style.justifySelf(); |
| 95 | m_needsXAdjustment |= alignment.isAuto() || alignment.isNormal() || alignment.isAnchorCenter(); |
| 96 | } else { |
| 97 | auto alignment = style.alignSelf(); |
| 98 | m_needsXAdjustment |= alignment.isAuto() || alignment.isNormal() || alignment.isAnchorCenter(); |
| 99 | } |
| 100 | } |
| 101 | if (positionAreaValue->coordMatchedTrackForAxis(BoxAxis::Vertical, containingWritingMode, style.writingMode()) != Style::PositionAreaTrack::SpanAll) |
| 102 | m_needsYAdjustment |= true; |
| 103 | else { |
| 104 | if (containingWritingMode.isHorizontal()) { |
| 105 | auto alignment = style.alignSelf(); |
| 106 | m_needsYAdjustment |= alignment.isAuto() || alignment.isNormal() || alignment.isAnchorCenter(); |
| 107 | } else { |
| 108 | auto alignment = style.justifySelf(); |
| 109 | m_needsYAdjustment |= alignment.isAuto() || alignment.isNormal() || alignment.isAnchorCenter(); |
| 110 | } |
| 111 | } |
| 112 | } |
| 113 | |
| 114 | if (!m_needsXAdjustment) { |
| 115 | m_needsXAdjustment = containingWritingMode.isHorizontal() |
| 116 | ? style.justifySelf().isAnchorCenter() |
| 117 | : style.alignSelf().isAnchorCenter(); |
| 118 | } |
| 119 | if (!m_needsYAdjustment) { |
| 120 | m_needsYAdjustment = containingWritingMode.isHorizontal() |
| 121 | ? style.alignSelf().isAnchorCenter() |
| 122 | : style.alignSelf().isAnchorCenter(); |
| 123 | } |
| 124 | |
| 125 | m_isHidden = style.isForceHidden(); |
| 126 | |
| 127 | if ((m_hasStickyAnchor = defaultAnchor.isStickilyPositioned())) |
| 128 | m_stickySnapshot = defaultAnchor.stickyPositionOffset(); |
| 129 | |
| 130 | if (auto anchorBox = dynamicDowncast<RenderBox>(defaultAnchor)) { |
| 131 | if (CheckedPtr chainedAnchor = Style::AnchorPositionEvaluator::defaultAnchorForBox(*anchorBox)) |
| 132 | m_hasChainedAnchor = true; |
| 133 | } |
| 134 | } |
| 135 | |
| 136 | RenderBox* AnchorScrollAdjuster::anchored() const |
| 137 | { |
| 138 | return m_anchored.ptr(); |
| 139 | } |
| 140 | |
| 141 | bool AnchorScrollAdjuster::recaptureDiffers(const AnchorScrollAdjuster& other) const |
| 142 | { |
| 143 | bool same = m_anchored.ptr() == other.m_anchored.ptr() |
| 144 | && m_scrollSnapshots == other.m_scrollSnapshots |
| 145 | && m_stickySnapshot == other.m_stickySnapshot; |
| 146 | return !same; |
| 147 | } |
| 148 | |
| 149 | void AnchorScrollAdjuster::addSnapshot(const RenderBox& scroller) |
| 150 | { |
| 151 | ASSERT(scroller.hasPotentiallyScrollableOverflow() && !is<RenderView>(scroller))((void)0); |
| 152 | m_scrollSnapshots.constructAndAppend(scroller, scroller.constrainedScrollPosition()); |
| 153 | } |
| 154 | |
| 155 | void AnchorScrollAdjuster::addViewportSnapshot(const RenderView& renderView) |
| 156 | { |
| 157 | auto& view = renderView.frameView(); |
Local variable 'view' is uncounted and unsafe | |
| 158 | auto position = view.constrainedScrollPosition(ScrollPosition(view.scrollPositionRespectingCustomFixedPosition())); |
| 159 | m_scrollSnapshots.insert(0, AnchorScrollSnapshot { position }); |
| 160 | m_adjustForViewport = true; |
| 161 | } |
| 162 | |
| 163 | LayoutSize AnchorScrollAdjuster::adjustmentForViewport(const RenderView& renderView) const |
| 164 | { |
| 165 | if (m_adjustForViewport) { |
| 166 | // Viewport snapshot is stored in the first slot. |
| 167 | ASSERT(m_scrollSnapshots.size() && !m_scrollSnapshots.first().m_scroller)((void)0); |
| 168 | auto& view = renderView.frameView(); |
| 169 | return m_scrollSnapshots.first().m_scrollSnapshot |
| 170 | - view.constrainedScrollPosition(IntPoint(view.scrollPositionRespectingCustomFixedPosition())); |
| 171 | } |
| 172 | return { }; |
| 173 | } |
| 174 | |
| 175 | LayoutSize AnchorScrollAdjuster::accumulateAdjustments(const RenderView& renderView, const RenderBox& anchored) const |
| 176 | { |
| 177 | ASSERT(m_anchored.ptr() == &anchored)((void)0); |
| 178 | LayoutSize scrollAdjustment; |
| 179 | |
| 180 | if (m_hasChainedAnchor || m_hasStickyAnchor) { |
| 181 | if (CheckedPtr defaultAnchor = Style::AnchorPositionEvaluator::defaultAnchorForBox(anchored)) { |
| 182 | auto defaultAnchorBox = dynamicDowncast<RenderBox>(defaultAnchor.get()); |
| 183 | ASSERT(defaultAnchorBox)((void)0); // We shouldn't exist if there's no default anchor. |
| 184 | if (defaultAnchorBox) { |
| 185 | // The anchor may itself be scroll-compensated. Recurse if needed. |
| 186 | if (auto chainedAdjuster = renderView.layoutContext().anchorScrollAdjusterFor(*defaultAnchorBox)) |
| 187 | scrollAdjustment += chainedAdjuster->accumulateAdjustments(renderView, *defaultAnchorBox); |
| 188 | // Compensate for sticky adjustments. |
| 189 | if (m_hasStickyAnchor) |
| 190 | scrollAdjustment += defaultAnchorBox->stickyPositionOffset() - m_stickySnapshot; |
| 191 | } |
| 192 | } |
| 193 | } |
| 194 | |
| 195 | for (auto snapshot : m_scrollSnapshots) |
| 196 | scrollAdjustment += snapshot.adjustmentForCurrentScrollPosition(); |
| 197 | scrollAdjustment += adjustmentForViewport(renderView); |
| 198 | |
| 199 | if (!m_needsXAdjustment) |
| 200 | scrollAdjustment.setWidth(0); |
| 201 | if (!m_needsYAdjustment) |
| 202 | scrollAdjustment.setHeight(0); |
| 203 | return scrollAdjustment; |
| 204 | } |
| 205 | |
| 206 | void AnchorScrollAdjuster::setFallbackLimits(const RenderBox& anchored) |
| 207 | { |
| 208 | auto xConstraints = PositionedLayoutConstraints { anchored, anchored.writingMode().isHorizontal() ? LogicalBoxAxis::Inline : LogicalBoxAxis::Block }; |
| 209 | auto yConstraints = PositionedLayoutConstraints { anchored, anchored.writingMode().isHorizontal() ? LogicalBoxAxis::Block : LogicalBoxAxis::Inline }; |
| 210 | auto xLimits = xConstraints.originalContainingRange(); |
| 211 | auto yLimits = yConstraints.originalContainingRange(); |
| 212 | |
| 213 | auto marginRect = anchored.marginBoxRect(); |
| 214 | marginRect.moveBy(anchored.location()); |
| 215 | m_fallbackLimits.m_min.setWidth(xLimits.min() - marginRect.x()); |
| 216 | m_fallbackLimits.m_min.setHeight(yLimits.min() - marginRect.y()); |
| 217 | m_fallbackLimits.m_max.setWidth(xLimits.max() - marginRect.maxX()); |
| 218 | m_fallbackLimits.m_max.setHeight(yLimits.max() - marginRect.maxY()); |
| 219 | |
| 220 | m_hasFallback = true; |
| 221 | } |
| 222 | |
| 223 | bool AnchorScrollAdjuster::invalidateForScroller(const RenderBox& scroller) |
| 224 | { |
| 225 | bool anchoredNeedsInvalidation = false; |
| 226 | for (auto snapshot : m_scrollSnapshots) { |
| 227 | if (snapshot.m_scroller.get() == &scroller) { |
| 228 | anchoredNeedsInvalidation = true; |
| 229 | break; |
| 230 | } |
| 231 | } |
| 232 | if (anchoredNeedsInvalidation) { |
| 233 | m_anchored->setNeedsLayout(); |
| 234 | ASSERT(m_anchored->element())((void)0); |
| 235 | if (CheckedPtr element = m_anchored->element()) |
| 236 | element->invalidateForAnchorRectChange(); |
| 237 | } |
| 238 | return anchoredNeedsInvalidation; |
| 239 | } |
| 240 | |
| 241 | namespace Style { |
| 242 | |
| 243 | WTF_MAKE_TZONE_ALLOCATED_IMPL(AnchorPositionedState)::bmalloc::api::HeapRef AnchorPositionedState::s_heapRef; const TZoneSpecification AnchorPositionedState::s_heapSpec = { & AnchorPositionedState::s_heapRef, TZoneSpecification::encodeSize <AnchorPositionedState>(), TZoneSpecification::encodeAlignment <AnchorPositionedState>(), TZoneSpecification::encodeCategory <AnchorPositionedState>(), ::bmalloc::api::compactAllocationMode <AnchorPositionedState>(), TZoneSpecification::encodeDescriptor <AnchorPositionedState>(), }; void* AnchorPositionedState ::operatorNewSlow(size_t size) { if constexpr (::bmalloc::api ::compactAllocationMode<AnchorPositionedState>() == CompactAllocationMode ::Compact) return ::bmalloc::api::tzoneAllocateCompactSlow(size , s_heapSpec); return ::bmalloc::api::tzoneAllocateNonCompactSlow (size, s_heapSpec); } using __makeBtzoneMallocedInlineMacroSemicolonifier __attribute__((unused)) = int; |
| 244 | |
| 245 | static inline void clearAnchorScrollSnapshots(RenderBox& anchored) |
| 246 | { |
| 247 | if (!anchored.layer()->anchorScrollAdjustment()) |
| 248 | return; |
| 249 | anchored.layoutContext().unregisterAnchorScrollAdjusterFor(anchored); |
| 250 | } |
| 251 | |
| 252 | void AnchorPositionEvaluator::captureScrollSnapshots(RenderBox& anchored, bool invalidateStyleForScrollPositionChanges) |
| 253 | { |
| 254 | if (!anchored.layer()) |
| 255 | return; |
| 256 | |
| 257 | CheckedPtr defaultAnchor = AnchorPositionEvaluator::defaultAnchorForBox(anchored); |
| 258 | if (!defaultAnchor) |
| 259 | return clearAnchorScrollSnapshots(anchored); |
| 260 | |
| 261 | AnchorScrollAdjuster adjuster(anchored, *defaultAnchor); |
| 262 | if (!adjuster.mayNeedAdjustment()) // Note: We sometimes hit this path during interleaved layout because style bits aren't set yet. |
| 263 | return clearAnchorScrollSnapshots(anchored); |
| 264 | |
| 265 | CheckedPtr containingBlock = anchored.containingBlock(); |
| 266 | ASSERT(defaultAnchor->isDescendantOf(containingBlock.get()))((void)0); |
| 267 | |
| 268 | bool isFixedAnchor = defaultAnchor->layer() && defaultAnchor->layer()->behavesAsFixed(); |
| 269 | for (auto* ancestor = defaultAnchor->container(); ancestor && ancestor != containingBlock; ancestor = ancestor->container()) { |
| 270 | if (auto* box = dynamicDowncast<RenderBox>(ancestor)) { |
| 271 | if (box->hasPotentiallyScrollableOverflow()) |
| 272 | adjuster.addSnapshot(*box); |
| 273 | if (box->layer() && box->layer()->behavesAsFixed()) |
| 274 | isFixedAnchor = true; |
| 275 | } |
| 276 | } |
| 277 | |
| 278 | if (anchored.layer() && anchored.layer()->behavesAsFixed() && !isFixedAnchor) |
| 279 | adjuster.addViewportSnapshot(anchored.view()); |
| 280 | |
| 281 | if (adjuster.isEmpty()) |
| 282 | return clearAnchorScrollSnapshots(anchored); |
| 283 | |
| 284 | if (!anchored.style().positionTryFallbacks().isNone() |
| 285 | || anchored.style().positionVisibility().contains(PositionVisibilityValue::NoOverflow)) |
| 286 | adjuster.setFallbackLimits(anchored); |
| 287 | |
| 288 | auto captureDiff = anchored.layoutContext().registerAnchorScrollAdjuster(WTF::move(adjuster)); |
| 289 | if (invalidateStyleForScrollPositionChanges && AnchorScrollAdjuster::SnapshotsDiffer == captureDiff && anchored.style().usesAnchorFunctions()) { |
| 290 | // Scroll positions changed since the last capture, which means anchor() resolution needs updating. |
| 291 | if (CheckedPtr element = anchored.element()) |
| 292 | element->invalidateForAnchorRectChange(); |
| 293 | } |
| 294 | anchored.layer()->setAnchorScrollAdjustment({ }); |
| 295 | } |
| 296 | |
| 297 | void AnchorPositionEvaluator::updateScrollAdjustments(RenderView& renderView) |
| 298 | { |
| 299 | // https://drafts.csswg.org/css-anchor-position-1/#scroll |
| 300 | auto& layoutContext = renderView.layoutContext(); |
| 301 | bool needsRecompositing = false; |
| 302 | |
| 303 | for (auto adjuster : layoutContext.anchorScrollAdjusters()) { |
| 304 | CheckedPtr anchored = adjuster.anchored(); |
| 305 | if (!anchored || !anchored->layer()) { |
| 306 | ASSERT_NOT_REACHED()((void)0); // RenderBox failed to clean up. |
| 307 | continue; |
| 308 | } |
| 309 | |
| 310 | LayoutSize scrollOffset = adjuster.accumulateAdjustments(renderView, *anchored); |
| 311 | if (!anchored->layer()->setAnchorScrollAdjustment(scrollOffset)) |
| 312 | continue; |
| 313 | |
| 314 | bool shouldBeHidden = false; |
| 315 | bool needsInvalidation = false; |
| 316 | if (adjuster.hasFallbackLimits()) { |
| 317 | if (adjuster.exceedsFallbackLimits(scrollOffset)) { |
| 318 | if (!anchored->style().positionTryFallbacks().isNone()) { |
| 319 | anchored->setNeedsLayout(); |
| 320 | needsInvalidation = true; |
| 321 | } else |
| 322 | shouldBeHidden = anchored->style().positionVisibility().contains(PositionVisibilityValue::NoOverflow); |
| 323 | } |
| 324 | } |
| 325 | if (!shouldBeHidden && anchored->style().positionVisibility().contains(PositionVisibilityValue::AnchorsVisible)) |
| 326 | shouldBeHidden = AnchorPositionEvaluator::isDefaultAnchorInvisibleOrClippedByInterveningBoxes(*anchored); |
| 327 | |
| 328 | if (needsInvalidation || shouldBeHidden != adjuster.isHidden()) { |
| 329 | ASSERT(anchored->element())((void)0); |
| 330 | if (CheckedPtr element = anchored->element()) |
| 331 | element->invalidateForAnchorRectChange(); // FIXME: Optimize this. |
| 332 | } else |
| 333 | needsRecompositing = true; |
| 334 | }; |
| 335 | |
| 336 | if (needsRecompositing && !layoutContext.isInLayout()) |
| 337 | renderView.compositor().scheduleCompositingLayerUpdate(); |
| 338 | } |
| 339 | |
| 340 | |
| 341 | static const ScopedName& implicitAnchorElementName() |
| 342 | { |
| 343 | // User specified anchor names start with "--". |
| 344 | static NeverDestroyed<ScopedName> name { ScopedName { "implicit-anchor-element"_s } }; |
| 345 | return name; |
| 346 | } |
| 347 | |
| 348 | static BoxAxis NODELETE[[clang::annotate_type("webkit.nodelete")]] mapInsetPropertyToPhysicalAxis(CSSPropertyID id, const WritingMode writingMode) |
| 349 | { |
| 350 | switch (id) { |
| 351 | case CSSPropertyLeft: |
| 352 | case CSSPropertyRight: |
| 353 | return BoxAxis::Horizontal; |
| 354 | case CSSPropertyTop: |
| 355 | case CSSPropertyBottom: |
| 356 | return BoxAxis::Vertical; |
| 357 | case CSSPropertyInsetInlineStart: |
| 358 | case CSSPropertyInsetInlineEnd: |
| 359 | return mapAxisLogicalToPhysical(writingMode, LogicalBoxAxis::Inline); |
| 360 | case CSSPropertyInsetBlockStart: |
| 361 | case CSSPropertyInsetBlockEnd: |
| 362 | return mapAxisLogicalToPhysical(writingMode, LogicalBoxAxis::Block); |
| 363 | default: |
| 364 | ASSERT_NOT_REACHED()((void)0); |
| 365 | return BoxAxis::Horizontal; |
| 366 | } |
| 367 | } |
| 368 | |
| 369 | static LogicalBoxAxis NODELETE[[clang::annotate_type("webkit.nodelete")]] mapInsetPropertyToLogicalAxis(CSSPropertyID id, const WritingMode writingMode) |
| 370 | { |
| 371 | switch (id) { |
| 372 | case CSSPropertyLeft: |
| 373 | case CSSPropertyRight: |
| 374 | return writingMode.isHorizontal() ? LogicalBoxAxis::Inline : LogicalBoxAxis::Block; |
| 375 | case CSSPropertyTop: |
| 376 | case CSSPropertyBottom: |
| 377 | return writingMode.isHorizontal() ? LogicalBoxAxis::Block : LogicalBoxAxis::Inline; |
| 378 | case CSSPropertyInsetInlineStart: |
| 379 | case CSSPropertyInsetInlineEnd: |
| 380 | return LogicalBoxAxis::Inline; |
| 381 | case CSSPropertyInsetBlockStart: |
| 382 | case CSSPropertyInsetBlockEnd: |
| 383 | return LogicalBoxAxis::Block; |
| 384 | default: |
| 385 | ASSERT_NOT_REACHED()((void)0); |
| 386 | return LogicalBoxAxis::Inline; |
| 387 | } |
| 388 | } |
| 389 | |
| 390 | // Physical sides (left/right/top/bottom) can only be used in certain inset properties. "For example, |
| 391 | // left is usable in left, right, or the logical inset properties that refer to the horizontal axis." |
| 392 | // See: https://drafts.csswg.org/css-anchor-position-1/#typedef-anchor-side |
| 393 | static bool NODELETE[[clang::annotate_type("webkit.nodelete")]] anchorSideMatchesInsetProperty(CSSValueID anchorSideID, BoxAxis physicalAxis) |
| 394 | { |
| 395 | switch (anchorSideID) { |
| 396 | case CSSValueID::CSSValueInside: |
| 397 | case CSSValueID::CSSValueOutside: |
| 398 | case CSSValueID::CSSValueStart: |
| 399 | case CSSValueID::CSSValueEnd: |
| 400 | case CSSValueID::CSSValueSelfStart: |
| 401 | case CSSValueID::CSSValueSelfEnd: |
| 402 | case CSSValueID::CSSValueCenter: |
| 403 | case CSSValueID::CSSValueInvalid: // percentage |
| 404 | return true; |
| 405 | case CSSValueID::CSSValueTop: |
| 406 | case CSSValueID::CSSValueBottom: |
| 407 | return BoxAxis::Vertical == physicalAxis; |
| 408 | case CSSValueID::CSSValueLeft: |
| 409 | case CSSValueID::CSSValueRight: |
| 410 | return BoxAxis::Horizontal == physicalAxis; |
| 411 | default: |
| 412 | ASSERT_NOT_REACHED()((void)0); |
| 413 | return false; |
| 414 | } |
| 415 | } |
| 416 | |
| 417 | static LayoutSize offsetFromAncestorContainer(const RenderElement& descendantContainer, const RenderElement& ancestorContainer) |
| 418 | { |
| 419 | LayoutSize offset; |
| 420 | LayoutPoint referencePoint; |
| 421 | CheckedPtr currentContainer = &descendantContainer; |
| 422 | CheckedPtr maxContainer = &ancestorContainer; |
| 423 | if (CheckedPtr ancestorInline = dynamicDowncast<RenderInline>(&ancestorContainer)) |
| 424 | maxContainer = ancestorInline->containingBlock(); |
| 425 | do { |
| 426 | CheckedPtr nextContainer = currentContainer->container(); |
| 427 | ASSERT(nextContainer)((void)0); // This means we reached the top without finding container. |
| 428 | if (!nextContainer) |
| 429 | break; |
| 430 | LayoutSize currentOffset = currentContainer->offsetFromContainer(*nextContainer, referencePoint); |
| 431 | |
| 432 | if (CheckedPtr boxContainer = dynamicDowncast<RenderBox>(*nextContainer)) { |
| 433 | // Clamp overscroll so we don't layout into it. |
| 434 | if (boxContainer->hasPotentiallyScrollableOverflow()) |
| 435 | currentOffset += boxContainer->scrollPosition() - boxContainer->constrainedScrollPosition(); |
| 436 | } |
| 437 | |
| 438 | offset += currentOffset; |
| 439 | referencePoint.move(currentOffset); |
| 440 | currentContainer = WTF::move(nextContainer); |
| 441 | } while (currentContainer != maxContainer); |
| 442 | |
| 443 | if (CheckedPtr descendantInline = dynamicDowncast<RenderInline>(&descendantContainer)) { |
| 444 | // RenderInline objects do not automatically account for their offset above, |
| 445 | // so we incorporate this offset here. |
| 446 | offset += toLayoutSize(descendantInline->linesBoundingBox().location()); |
| 447 | } |
| 448 | if (descendantContainer.containingBlock() == ancestorContainer.containingBlock()) { |
| 449 | // Account for 'position: relative' inline containing blocks by shifting back down into them. |
| 450 | if (CheckedPtr ancestorInline = dynamicDowncast<RenderInline>(&ancestorContainer)) |
| 451 | offset -= toLayoutSize(ancestorInline->firstInlineBoxTopLeft()); // FIXME: Handle RTL. |
| 452 | } |
| 453 | |
| 454 | if (auto ancestorBox = dynamicDowncast<RenderBox>(ancestorContainer)) // Zero out containing block scroll position. |
| 455 | offset += toLayoutSize(ancestorBox->constrainedScrollPosition()); |
| 456 | |
| 457 | return offset; |
| 458 | } |
| 459 | |
| 460 | void AnchorPositionEvaluator::addAnchorFunctionScrollCompensatedAxis(RenderStyle& style, const RenderBox& anchored, const RenderBoxModelObject& anchor, BoxAxis axis) |
| 461 | { |
| 462 | // https://drafts.csswg.org/css-anchor-position-1/#scroll |
| 463 | // An absolutely positioned box abspos compensates for scroll in the horizontal or vertical axis if both of the following conditions are true: |
| 464 | // - abspos has a default anchor box. |
| 465 | auto defaultAnchor = defaultAnchorForBox(anchored); |
| 466 | if (!defaultAnchor) |
| 467 | return; |
| 468 | |
| 469 | // - at least one anchor() function on abspos’s used inset properties in the axis refers to a target anchor element |
| 470 | // with the same nearest scroll container ancestor as abspos’s default anchor box. |
| 471 | if (defaultAnchor != &anchor && defaultAnchor->enclosingScrollableContainer() != anchor.enclosingScrollableContainer()) |
| 472 | return; |
| 473 | |
| 474 | auto axes = style.anchorFunctionScrollCompensatedAxes(); |
| 475 | axes.add(axis); |
| 476 | style.setAnchorFunctionScrollCompensatedAxes(axes); |
| 477 | } |
| 478 | |
| 479 | static LayoutRect boundingRectForFragmentedAnchor(const RenderBoxModelObject& anchorBox, const RenderElement& containingBlock, const RenderFragmentedFlow& fragmentedFlow) |
| 480 | { |
| 481 | // Compute the bounding box of the fragments. |
| 482 | // Location is relative to the fragmented flow. |
| 483 | CheckedPtr anchorRenderBox = dynamicDowncast<RenderBox>(&anchorBox); |
| 484 | if (!anchorRenderBox) |
| 485 | anchorRenderBox = anchorBox.containingBlock(); |
| 486 | LayoutPoint offsetRelativeToFragmentedFlow = fragmentedFlow.mapFromLocalToFragmentedFlow(anchorRenderBox.get(), { }).location(); |
| 487 | auto unfragmentedBorderBox = anchorBox.borderBoundingBox(); |
| 488 | unfragmentedBorderBox.moveBy(offsetRelativeToFragmentedFlow); |
| 489 | fragmentedFlow.flipForWritingMode(unfragmentedBorderBox); // Convert to RenderLayer coords. |
| 490 | auto fragmentsBoundingBox = fragmentedFlow.fragmentsBoundingBox(unfragmentedBorderBox); |
| 491 | fragmentedFlow.flipForWritingMode(fragmentsBoundingBox); // Convert to RenderBox coords. |
| 492 | |
| 493 | // Now convert to physical coordinates (top/left origin) and walk up. |
| 494 | // RenderFragmentedFlow doesn't have a usable frame rect, so use its container's content rect. |
| 495 | CheckedPtr fragmentedFlowContainer = fragmentedFlow.containingBlock(); |
| 496 | if (!fragmentedFlowContainer) { |
| 497 | ASSERT_NOT_REACHED()((void)0); |
| 498 | return fragmentsBoundingBox; |
| 499 | } |
| 500 | auto fragmentedFlowRect = fragmentedFlowContainer->contentBoxRect(); |
| 501 | if (fragmentedFlow.writingMode().isBlockFlipped()) { |
| 502 | if (fragmentedFlow.writingMode().isHorizontal()) |
| 503 | fragmentsBoundingBox.setY(fragmentedFlowRect.height() - fragmentsBoundingBox.maxY()); |
| 504 | else |
| 505 | fragmentsBoundingBox.setX(fragmentedFlowRect.width() - fragmentsBoundingBox.maxX()); |
| 506 | } |
| 507 | fragmentsBoundingBox.moveBy(fragmentedFlowRect.location()); |
| 508 | |
| 509 | // Change the location to be relative to the anchor's containing block. |
| 510 | if (fragmentedFlowContainer.get() != &containingBlock) |
| 511 | fragmentsBoundingBox.move(offsetFromAncestorContainer(*fragmentedFlowContainer, containingBlock)); |
| 512 | |
| 513 | return fragmentsBoundingBox; |
| 514 | } |
| 515 | |
| 516 | // This computes the top left location, physical width, and physical height of the specified |
| 517 | // anchor element. The location is computed relative to the specified containing block. |
| 518 | LayoutRect AnchorPositionEvaluator::computeAnchorRectRelativeToContainingBlock(CheckedRef<const RenderBoxModelObject> anchorBox, const RenderElement& containingBlock, const RenderBox& anchoredBox) |
| 519 | { |
| 520 | // Fragmented flows are a little tricky to deal with. One example of a fragmented |
| 521 | // flow is a block anchor element that is "fragmented" or split across multiple columns |
| 522 | // as a result of multi-column layout. In this case, we need to compute "the axis-aligned |
| 523 | // bounding rectangle of the fragments' border boxes" and make that our anchorHeight/Width. |
| 524 | // We also need to adjust the anchor's top left location to match that of the bounding box |
| 525 | // instead of the first fragment. |
| 526 | if (CheckedPtr fragmentedFlow = anchorBox->enclosingFragmentedFlow(); |
| 527 | fragmentedFlow && fragmentedFlow->isDescendantOf(&containingBlock)) |
| 528 | return boundingRectForFragmentedAnchor(anchorBox, containingBlock, *fragmentedFlow); |
| 529 | |
| 530 | auto anchorWidth = anchorBox->offsetWidth(); |
| 531 | auto anchorHeight = anchorBox->offsetHeight(); |
| 532 | auto anchorLocation = LayoutPoint { offsetFromAncestorContainer(anchorBox, containingBlock) }; |
| 533 | |
| 534 | if (&containingBlock == &containingBlock.view() && anchoredBox.isFixedPositioned()) { |
| 535 | // Handle fixed positioning x scrolling anchor. |
| 536 | bool isFixedAnchor = false; |
| 537 | for (const RenderElement* box = anchorBox.ptr(); box && box != &containingBlock; box = box->container()) { |
| 538 | if (box->isFixedPositioned()) { |
| 539 | isFixedAnchor = true; |
| 540 | break; |
| 541 | } |
| 542 | } |
| 543 | if (!isFixedAnchor) { |
| 544 | auto& view = anchorBox->view().frameView(); |
| 545 | anchorLocation.moveBy(-view.constrainedScrollPosition(ScrollPosition(view.scrollPositionRespectingCustomFixedPosition()))); |
| 546 | } |
| 547 | } |
| 548 | |
| 549 | if (CheckedPtr containingBox = dynamicDowncast<RenderBox>(containingBlock)) { |
| 550 | if (containingBox->shouldPlaceVerticalScrollbarOnLeft()) |
| 551 | anchorLocation.move(-containingBox->verticalScrollbarWidth(), 0); |
| 552 | } |
| 553 | |
| 554 | return LayoutRect(anchorLocation, LayoutSize(anchorWidth, anchorHeight)); |
| 555 | } |
| 556 | |
| 557 | static bool inline NODELETE[[clang::annotate_type("webkit.nodelete")]] isInsetPropertyContainerStartSide(CSSPropertyID insetPropertyID, PositionedLayoutConstraints& constraints) |
| 558 | { |
| 559 | switch (insetPropertyID) { |
| 560 | case CSSPropertyLeft: |
| 561 | return constraints.containingWritingMode().isAnyLeftToRight(); |
| 562 | case CSSPropertyRight: |
| 563 | return !constraints.containingWritingMode().isAnyLeftToRight(); |
| 564 | case CSSPropertyTop: |
| 565 | return constraints.containingWritingMode().isAnyTopToBottom(); |
| 566 | case CSSPropertyBottom: |
| 567 | return !constraints.containingWritingMode().isAnyTopToBottom(); |
| 568 | case CSSPropertyInsetInlineStart: |
| 569 | return !constraints.selfWritingMode().isInlineOpposing(constraints.containingWritingMode()); |
| 570 | case CSSPropertyInsetInlineEnd: |
| 571 | return constraints.selfWritingMode().isInlineOpposing(constraints.containingWritingMode()); |
| 572 | case CSSPropertyInsetBlockStart: |
| 573 | return !constraints.selfWritingMode().isBlockOpposing(constraints.containingWritingMode()); |
| 574 | case CSSPropertyInsetBlockEnd: |
| 575 | return constraints.selfWritingMode().isBlockOpposing(constraints.containingWritingMode()); |
| 576 | default: |
| 577 | ASSERT_NOT_REACHED()((void)0); |
| 578 | return true; |
| 579 | } |
| 580 | } |
| 581 | |
| 582 | static CSSPropertyID NODELETE[[clang::annotate_type("webkit.nodelete")]] getOppositeInset(CSSPropertyID propertyID) |
| 583 | { |
| 584 | switch (propertyID) { |
| 585 | case CSSPropertyLeft: |
| 586 | return CSSPropertyRight; |
| 587 | case CSSPropertyRight: |
| 588 | return CSSPropertyLeft; |
| 589 | case CSSPropertyTop: |
| 590 | return CSSPropertyBottom; |
| 591 | case CSSPropertyBottom: |
| 592 | return CSSPropertyTop; |
| 593 | |
| 594 | case CSSPropertyInsetInlineStart: |
| 595 | return CSSPropertyInsetInlineEnd; |
| 596 | case CSSPropertyInsetInlineEnd: |
| 597 | return CSSPropertyInsetInlineStart; |
| 598 | case CSSPropertyInsetBlockStart: |
| 599 | return CSSPropertyInsetBlockEnd; |
| 600 | case CSSPropertyInsetBlockEnd: |
| 601 | return CSSPropertyInsetBlockStart; |
| 602 | |
| 603 | default: |
| 604 | ASSERT_NOT_REACHED()((void)0); |
| 605 | return CSSPropertyInsetBlockStart; |
| 606 | } |
| 607 | } |
| 608 | |
| 609 | static std::pair<CSSPropertyID, bool> applyTryTacticsToInset(CSSPropertyID propertyID, WritingMode writingMode, const BuilderPositionTryFallback& fallback) |
| 610 | { |
| 611 | bool isFlipped = false; |
| 612 | for (auto tactic : fallback.tactics) { |
| 613 | switch (tactic) { |
| 614 | case PositionTryFallback::Tactic::FlipInline: |
| 615 | if (LogicalBoxAxis::Inline == mapInsetPropertyToLogicalAxis(propertyID, writingMode)) { |
| 616 | propertyID = getOppositeInset(propertyID); |
| 617 | isFlipped = true; |
| 618 | } |
| 619 | break; |
| 620 | case PositionTryFallback::Tactic::FlipBlock: |
| 621 | if (LogicalBoxAxis::Block == mapInsetPropertyToLogicalAxis(propertyID, writingMode)) { |
| 622 | propertyID = getOppositeInset(propertyID); |
| 623 | isFlipped = true; |
| 624 | } |
| 625 | break; |
| 626 | case PositionTryFallback::Tactic::FlipX: |
| 627 | if (BoxAxis::Horizontal == mapInsetPropertyToPhysicalAxis(propertyID, writingMode)) { |
| 628 | propertyID = getOppositeInset(propertyID); |
| 629 | isFlipped = true; |
| 630 | } |
| 631 | break; |
| 632 | case PositionTryFallback::Tactic::FlipY: |
| 633 | if (BoxAxis::Vertical == mapInsetPropertyToPhysicalAxis(propertyID, writingMode)) { |
| 634 | propertyID = getOppositeInset(propertyID); |
| 635 | isFlipped = true; |
| 636 | } |
| 637 | break; |
| 638 | case PositionTryFallback::Tactic::FlipStart: |
| 639 | propertyID = [&] { |
| 640 | switch (propertyID) { |
| 641 | case CSSPropertyInsetInlineStart: |
| 642 | return CSSPropertyInsetBlockStart; |
| 643 | case CSSPropertyInsetInlineEnd: |
| 644 | return CSSPropertyInsetBlockEnd; |
| 645 | case CSSPropertyInsetBlockStart: |
| 646 | return CSSPropertyInsetInlineStart; |
| 647 | case CSSPropertyInsetBlockEnd: |
| 648 | return CSSPropertyInsetInlineEnd; |
| 649 | |
| 650 | case CSSPropertyLeft: |
| 651 | return writingMode.isAnyLeftToRight() == writingMode.isAnyTopToBottom() |
| 652 | ? CSSPropertyTop : CSSPropertyBottom; |
| 653 | case CSSPropertyRight: |
| 654 | return writingMode.isAnyLeftToRight() == writingMode.isAnyTopToBottom() |
| 655 | ? CSSPropertyBottom : CSSPropertyTop; |
| 656 | case CSSPropertyTop: |
| 657 | return writingMode.isAnyLeftToRight() == writingMode.isAnyTopToBottom() |
| 658 | ? CSSPropertyLeft : CSSPropertyRight; |
| 659 | case CSSPropertyBottom: |
| 660 | return writingMode.isAnyLeftToRight() == writingMode.isAnyTopToBottom() |
| 661 | ? CSSPropertyRight : CSSPropertyLeft; |
| 662 | |
| 663 | default: |
| 664 | ASSERT_NOT_REACHED()((void)0); |
| 665 | return CSSPropertyInsetBlockStart; |
| 666 | } |
| 667 | }(); |
| 668 | break; |
| 669 | } |
| 670 | } |
| 671 | return { propertyID, isFlipped }; |
| 672 | } |
| 673 | |
| 674 | // "An anchor() function representing a valid anchor function resolves...to the <length> that would |
| 675 | // align the edge of the positioned elements' inset-modified containing block corresponding to the |
| 676 | // property the function appears in with the specified border edge of the target anchor element..." |
| 677 | // See: https://drafts.csswg.org/css-anchor-position-1/#anchor-pos |
| 678 | static LayoutUnit computeInsetValue(CSSPropertyID insetPropertyID, CheckedRef<const RenderBoxModelObject> anchorBox, CheckedRef<const RenderBox> anchorPositionedRenderer, AnchorPositionEvaluator::Side anchorSide, const std::optional<BuilderPositionTryFallback>& positionTryFallback) |
| 679 | { |
| 680 | auto writingMode = anchorPositionedRenderer->writingMode(); |
| 681 | bool isFlipped = false; |
| 682 | if (positionTryFallback) |
| 683 | std::tie(insetPropertyID, isFlipped) = applyTryTacticsToInset(insetPropertyID, writingMode, *positionTryFallback); |
| 684 | |
| 685 | auto selfLogicalAxis = mapInsetPropertyToLogicalAxis(insetPropertyID, writingMode); |
| 686 | PositionedLayoutConstraints constraints(anchorPositionedRenderer, selfLogicalAxis); |
| 687 | |
| 688 | auto anchorPercentage = [&]() -> double { |
| 689 | if (std::holds_alternative<CSSValueID>(anchorSide)) { |
| 690 | auto anchorSideID = std::get<CSSValueID>(anchorSide); |
| 691 | switch (anchorSideID) { |
| 692 | case CSSValueCenter: |
| 693 | return 0.5; |
| 694 | case CSSValueStart: |
| 695 | return 0; |
| 696 | case CSSValueEnd: |
| 697 | return 1; |
| 698 | case CSSValueSelfStart: |
| 699 | return constraints.isOpposing() ? 1 : 0; |
| 700 | case CSSValueSelfEnd: |
| 701 | return constraints.isOpposing() ? 0 : 1; |
| 702 | |
| 703 | case CSSValueTop: |
| 704 | return constraints.containingWritingMode().isAnyTopToBottom() ? 0 : 1; |
| 705 | case CSSValueBottom: |
| 706 | return constraints.containingWritingMode().isAnyTopToBottom() ? 1 : 0; |
| 707 | case CSSValueLeft: |
| 708 | return constraints.containingWritingMode().isAnyLeftToRight() ? 0 : 1; |
| 709 | case CSSValueRight: |
| 710 | return constraints.containingWritingMode().isAnyLeftToRight() ? 1 : 0; |
| 711 | |
| 712 | case CSSValueInside: |
| 713 | return isInsetPropertyContainerStartSide(insetPropertyID, constraints) != isFlipped ? 0 : 1; |
| 714 | case CSSValueOutside: |
| 715 | return isInsetPropertyContainerStartSide(insetPropertyID, constraints) != isFlipped ? 1 : 0; |
| 716 | |
| 717 | default: |
| 718 | ASSERT_NOT_REACHED()((void)0); |
| 719 | return 0; |
| 720 | } |
| 721 | } else |
| 722 | return std::get<double>(anchorSide); |
| 723 | }(); |
| 724 | if (constraints.startIsBefore() == isFlipped) |
| 725 | anchorPercentage = 1 - anchorPercentage; |
| 726 | |
| 727 | CheckedPtr containingBlock = anchorPositionedRenderer->container(); |
| 728 | ASSERT(containingBlock)((void)0); |
| 729 | auto anchorRect = AnchorPositionEvaluator::computeAnchorRectRelativeToContainingBlock(anchorBox, *containingBlock, anchorPositionedRenderer.get()); |
| 730 | auto anchorRange = constraints.extractRange(anchorRect); |
| 731 | |
| 732 | auto anchorPosition = anchorRange.min() + LayoutUnit(anchorRange.size() * anchorPercentage); |
| 733 | auto insetValue = isInsetPropertyContainerStartSide(insetPropertyID, constraints) == constraints.startIsBefore() |
| 734 | ? anchorPosition - constraints.containingRange().min() |
| 735 | : constraints.containingRange().max() - anchorPosition; |
| 736 | return insetValue; |
| 737 | } |
| 738 | |
| 739 | CheckedPtr<RenderBoxModelObject> AnchorPositionEvaluator::findAnchorForAnchorFunctionAndAttemptResolution(BuilderState& builderState, std::optional<ScopedName> anchorNameArgument) |
| 740 | { |
| 741 | auto& style = builderState.renderStyle(); |
| 742 | style.setUsesAnchorFunctions(); |
| 743 | |
| 744 | if (!builderState.anchorPositionedStates()) |
| 745 | return { }; |
| 746 | |
| 747 | auto isValid = [&] { |
| 748 | if (!builderState.element()) |
| 749 | return false; |
| 750 | |
| 751 | // FIXME: Support remaining box generating pseudo-elements (like ::marker). |
| 752 | auto pseudoElement = style.pseudoElementType(); |
| 753 | if (pseudoElement && pseudoElement != PseudoElementType::Before && pseudoElement != PseudoElementType::After) |
| 754 | return false; |
| 755 | |
| 756 | return true; |
| 757 | }; |
| 758 | |
| 759 | if (!isValid()) |
| 760 | return { }; |
| 761 | |
| 762 | Ref elementOrHost = *builderState.element(); |
| 763 | |
| 764 | // PseudoElement nodes are created on-demand by render tree builder so dont' work as keys here. |
| 765 | auto& anchorPositionedStates = *builderState.anchorPositionedStates(); |
| 766 | auto& anchorPositionedState = *anchorPositionedStates.ensure({ elementOrHost.ptr(), style.pseudoElementIdentifier() }, [&] { |
| 767 | return WTF::makeUnique<AnchorPositionedState>(); |
| 768 | }).iterator->value.get(); |
| 769 | |
| 770 | auto scopedAnchorName = [&] { |
| 771 | if (anchorNameArgument) |
| 772 | return *anchorNameArgument; |
| 773 | return defaultAnchorName(style); |
| 774 | }; |
| 775 | |
| 776 | auto resolvedAnchorName = ResolvedScopedName::createFromScopedName(elementOrHost, scopedAnchorName()); |
| 777 | |
| 778 | // Collect anchor names that this element refers to in anchor() or anchor-size() |
| 779 | bool isNewAnchorName = anchorPositionedState.anchorNames.add(resolvedAnchorName).isNewEntry; |
| 780 | |
| 781 | // If anchor resolution has progressed past FindAnchors, and we pick up a new anchor name, set the |
| 782 | // stage back to FindAnchors. This restarts the resolution process to resolve newly added names. |
| 783 | if (isNewAnchorName) |
| 784 | anchorPositionedState.stage = AnchorPositionResolutionStage::FindAnchors; |
| 785 | |
| 786 | // An anchor() instance will be ready to be resolved when all referenced anchor-names |
| 787 | // have been mapped to an actual anchor element in the DOM tree. At that point, we |
| 788 | // should also have layout information for the anchor-positioned element alongside |
| 789 | // the anchors referenced by the anchor-positioned element. Until then, we cannot |
| 790 | // resolve this anchor() instance. |
| 791 | if (anchorPositionedState.stage <= AnchorPositionResolutionStage::FindAnchors) |
| 792 | return { }; |
| 793 | |
| 794 | auto anchorPositionedElement = anchorPositionedElementOrPseudoElement(builderState); |
| 795 | |
| 796 | CheckedPtr anchorPositionedRenderer = anchorPositionedElement ? anchorPositionedElement->renderer() : nullptr; |
| 797 | if (!anchorPositionedRenderer) { |
| 798 | // If no render tree information is present, the procedure is finished. |
| 799 | anchorPositionedState.stage = AnchorPositionResolutionStage::Resolved; |
| 800 | return { }; |
| 801 | } |
| 802 | |
| 803 | // Anchor value may now be resolved using layout information |
| 804 | |
| 805 | RefPtr anchorElement = anchorPositionedState.anchorElements.get(resolvedAnchorName); |
| 806 | if (!anchorElement) { |
| 807 | // See: https://drafts.csswg.org/css-anchor-position-1/#valid-anchor-function |
| 808 | anchorPositionedState.stage = AnchorPositionResolutionStage::Resolved; |
| 809 | |
| 810 | return { }; |
| 811 | } |
| 812 | |
| 813 | if (auto* state = anchorPositionedStates.get(keyForElementOrPseudoElement(*anchorElement))) { |
| 814 | // Check if the anchor is itself anchor-positioned but hasn't been positioned yet. |
| 815 | if (state->stage < AnchorPositionResolutionStage::Positioned) |
| 816 | return { }; |
| 817 | } |
| 818 | |
| 819 | anchorPositionedState.stage = AnchorPositionResolutionStage::Resolved; |
| 820 | |
| 821 | return dynamicDowncast<RenderBoxModelObject>(anchorElement->renderer()); |
| 822 | } |
| 823 | |
| 824 | bool AnchorPositionEvaluator::propertyAllowsAnchorFunction(CSSPropertyID propertyID) |
| 825 | { |
| 826 | return CSSProperty::isInsetProperty(propertyID); |
| 827 | } |
| 828 | |
| 829 | std::optional<double> AnchorPositionEvaluator::evaluate(BuilderState& builderState, std::optional<ScopedName> elementName, Side side) |
| 830 | { |
| 831 | auto& style = builderState.renderStyle(); |
| 832 | |
| 833 | auto propertyID = builderState.cssPropertyID(); |
| 834 | auto physicalAxis = mapInsetPropertyToPhysicalAxis(propertyID, style.writingMode()); |
| 835 | |
| 836 | // https://drafts.csswg.org/css-anchor-position-1/#anchor-valid |
| 837 | auto isValidAnchor = [&] { |
| 838 | // It’s being used in an inset property... |
| 839 | if (!propertyAllowsAnchorFunction(propertyID)) |
| 840 | return false; |
| 841 | |
| 842 | // ...on an absolutely-positioned element. |
| 843 | if (!style.hasOutOfFlowPosition()) |
| 844 | return false; |
| 845 | |
| 846 | // If its <anchor-side> specifies a physical keyword, it’s being used in an inset property in that axis. |
| 847 | // (For example, left can only be used in left, right, or a logical inset property in the horizontal axis.) |
| 848 | if (auto* sideID = std::get_if<CSSValueID>(&side); sideID && !anchorSideMatchesInsetProperty(*sideID, physicalAxis)) |
| 849 | return false; |
| 850 | |
| 851 | return true; |
| 852 | }; |
| 853 | |
| 854 | if (!isValidAnchor()) |
| 855 | return { }; |
| 856 | |
| 857 | auto anchorRenderer = findAnchorForAnchorFunctionAndAttemptResolution(builderState, elementName); |
| 858 | if (!anchorRenderer) |
| 859 | return { }; |
| 860 | |
| 861 | RefPtr anchorPositionedElement = anchorPositionedElementOrPseudoElement(builderState); |
| 862 | if (!anchorPositionedElement) |
| 863 | return { }; |
| 864 | |
| 865 | CheckedPtr anchorPositionedRenderer = dynamicDowncast<RenderBox>(anchorPositionedElement->renderer()); |
| 866 | if (!anchorPositionedRenderer) |
| 867 | return { }; |
| 868 | |
| 869 | addAnchorFunctionScrollCompensatedAxis(style, *anchorPositionedRenderer, *anchorRenderer, physicalAxis); |
| 870 | |
| 871 | // Proceed with computing the inset value for the specified inset property. |
| 872 | double insetValue = computeInsetValue(propertyID, *anchorRenderer, *anchorPositionedRenderer, side, builderState.positionTryFallback()); |
| 873 | |
| 874 | // Adjust for CSS `zoom` property and page zoom. |
| 875 | return insetValue / style.usedZoom(); |
| 876 | } |
| 877 | |
| 878 | // Returns the default anchor size dimension to use when it is not specified in |
| 879 | // anchor-size(). This matches the axis of the property that anchor-size() is used in. |
| 880 | static AnchorSizeDimension NODELETE[[clang::annotate_type("webkit.nodelete")]] defaultDimensionForPropertyID(CSSPropertyID propertyID) |
| 881 | { |
| 882 | switch (propertyID) { |
| 883 | case CSSPropertyWidth: |
| 884 | case CSSPropertyMinWidth: |
| 885 | case CSSPropertyMaxWidth: |
| 886 | case CSSPropertyLeft: |
| 887 | case CSSPropertyRight: |
| 888 | case CSSPropertyMarginLeft: |
| 889 | case CSSPropertyMarginRight: |
| 890 | return AnchorSizeDimension::Width; |
| 891 | |
| 892 | case CSSPropertyHeight: |
| 893 | case CSSPropertyMinHeight: |
| 894 | case CSSPropertyMaxHeight: |
| 895 | case CSSPropertyTop: |
| 896 | case CSSPropertyBottom: |
| 897 | case CSSPropertyMarginTop: |
| 898 | case CSSPropertyMarginBottom: |
| 899 | return AnchorSizeDimension::Height; |
| 900 | |
| 901 | case CSSPropertyBlockSize: |
| 902 | case CSSPropertyMinBlockSize: |
| 903 | case CSSPropertyMaxBlockSize: |
| 904 | case CSSPropertyInsetBlockStart: |
| 905 | case CSSPropertyInsetBlockEnd: |
| 906 | case CSSPropertyMarginBlockStart: |
| 907 | case CSSPropertyMarginBlockEnd: |
| 908 | return AnchorSizeDimension::Block; |
| 909 | |
| 910 | case CSSPropertyInlineSize: |
| 911 | case CSSPropertyMinInlineSize: |
| 912 | case CSSPropertyMaxInlineSize: |
| 913 | case CSSPropertyInsetInlineStart: |
| 914 | case CSSPropertyInsetInlineEnd: |
| 915 | case CSSPropertyMarginInlineStart: |
| 916 | case CSSPropertyMarginInlineEnd: |
| 917 | return AnchorSizeDimension::Inline; |
| 918 | |
| 919 | default: |
| 920 | ASSERT_NOT_REACHED("anchor-size() being used in disallowed CSS property, which should not happen")((void)0); |
| 921 | return AnchorSizeDimension::Width; |
| 922 | } |
| 923 | } |
| 924 | |
| 925 | // Convert anchor size dimension to the physical dimension (width or height). |
| 926 | static BoxAxis NODELETE[[clang::annotate_type("webkit.nodelete")]] anchorSizeDimensionToPhysicalDimension(AnchorSizeDimension dimension, const RenderStyle& style, const RenderStyle& containerStyle) |
| 927 | { |
| 928 | switch (dimension) { |
| 929 | case AnchorSizeDimension::Width: |
| 930 | return BoxAxis::Horizontal; |
| 931 | case AnchorSizeDimension::Height: |
| 932 | return BoxAxis::Vertical; |
| 933 | case AnchorSizeDimension::Block: |
| 934 | return mapAxisLogicalToPhysical(containerStyle.writingMode(), LogicalBoxAxis::Block); |
| 935 | case AnchorSizeDimension::Inline: |
| 936 | return mapAxisLogicalToPhysical(containerStyle.writingMode(), LogicalBoxAxis::Inline); |
| 937 | case AnchorSizeDimension::SelfBlock: |
| 938 | return mapAxisLogicalToPhysical(style.writingMode(), LogicalBoxAxis::Block); |
| 939 | case AnchorSizeDimension::SelfInline: |
| 940 | return mapAxisLogicalToPhysical(style.writingMode(), LogicalBoxAxis::Inline); |
| 941 | } |
| 942 | |
| 943 | ASSERT_NOT_REACHED()((void)0); |
| 944 | return BoxAxis::Horizontal; |
| 945 | } |
| 946 | |
| 947 | bool AnchorPositionEvaluator::propertyAllowsAnchorSizeFunction(CSSPropertyID propertyID) |
| 948 | { |
| 949 | return CSSProperty::isSizingProperty(propertyID) || CSSProperty::isInsetProperty(propertyID) || CSSProperty::isMarginProperty(propertyID); |
| 950 | } |
| 951 | |
| 952 | std::optional<double> AnchorPositionEvaluator::evaluateSize(BuilderState& builderState, std::optional<ScopedName> elementName, std::optional<AnchorSizeDimension> dimension) |
| 953 | { |
| 954 | auto propertyID = builderState.cssPropertyID(); |
| 955 | const auto& style = builderState.renderStyle(); |
| 956 | |
| 957 | auto isValidAnchorSize = [&] { |
| 958 | // It’s being used in a sizing property, an inset property, or a margin property... |
| 959 | if (!propertyAllowsAnchorSizeFunction(propertyID)) |
| 960 | return false; |
| 961 | |
| 962 | // ...on an absolutely-positioned element. |
| 963 | if (!style.hasOutOfFlowPosition()) |
| 964 | return false; |
| 965 | |
| 966 | return true; |
| 967 | }; |
| 968 | |
| 969 | if (!isValidAnchorSize()) |
| 970 | return { }; |
| 971 | |
| 972 | auto anchorRenderer = findAnchorForAnchorFunctionAndAttemptResolution(builderState, elementName); |
| 973 | if (!anchorRenderer) |
| 974 | return { }; |
| 975 | |
| 976 | // Resolve the dimension (width or height) to return from the anchor positioned element. |
| 977 | RefPtr anchorPositionedElement = anchorPositionedElementOrPseudoElement(builderState); |
| 978 | if (!anchorPositionedElement) |
| 979 | return { }; |
| 980 | |
| 981 | CheckedPtr anchorPositionedRenderer = anchorPositionedElement->renderer(); |
| 982 | ASSERT(anchorPositionedRenderer)((void)0); |
| 983 | |
| 984 | CheckedPtr anchorPositionedContainerRenderer = anchorPositionedRenderer->container(); |
| 985 | ASSERT(anchorPositionedContainerRenderer)((void)0); |
| 986 | |
| 987 | auto resolvedDimension = dimension.value_or(defaultDimensionForPropertyID(propertyID)); |
| 988 | auto physicalDimension = anchorSizeDimensionToPhysicalDimension(resolvedDimension, anchorPositionedRenderer->style(), anchorPositionedContainerRenderer->style()); |
| 989 | |
| 990 | if (builderState.positionTryFallback()) { |
| 991 | // "For sizing properties, change the specified axis in anchor-size() functions to maintain the same relative relationship to the new direction that they had to the old." |
| 992 | if (CSSProperty::isSizingProperty(propertyID)) { |
| 993 | auto swapDimensions = builderState.positionTryFallback()->tactics.contains(PositionTryFallback::Tactic::FlipStart); |
| 994 | if (swapDimensions) |
| 995 | physicalDimension = oppositeAxis(physicalDimension); |
| 996 | } |
| 997 | } |
| 998 | |
| 999 | auto anchorBorderBoundingBox = anchorRenderer->borderBoundingBox(); |
| 1000 | if (CheckedPtr fragmentedFlow = anchorRenderer->enclosingFragmentedFlow()) { |
| 1001 | CheckedPtr containingBlock = anchorPositionedRenderer->containingBlock(); |
| 1002 | if (fragmentedFlow && containingBlock |
| 1003 | && fragmentedFlow->isDescendantOf(containingBlock.get())) |
| 1004 | anchorBorderBoundingBox = boundingRectForFragmentedAnchor(*anchorRenderer, *containingBlock, *fragmentedFlow); |
| 1005 | } |
| 1006 | |
| 1007 | // Adjust for CSS `zoom` property and page zoom. |
| 1008 | |
| 1009 | switch (physicalDimension) { |
| 1010 | case BoxAxis::Horizontal: |
| 1011 | return anchorBorderBoundingBox.width() / style.usedZoom(); |
| 1012 | case BoxAxis::Vertical: |
| 1013 | return anchorBorderBoundingBox.height() / style.usedZoom(); |
| 1014 | } |
| 1015 | |
| 1016 | ASSERT_NOT_REACHED()((void)0); |
| 1017 | return { }; |
| 1018 | } |
| 1019 | |
| 1020 | static const RenderElement* penultimateContainingBlockChainElement(const RenderElement& descendant, const RenderElement* ancestor) |
| 1021 | { |
| 1022 | auto* currentElement = &descendant; |
| 1023 | for (auto* nextElement = currentElement->container(); nextElement; nextElement = nextElement->container()) { |
| 1024 | if (nextElement == ancestor) |
| 1025 | return currentElement; |
| 1026 | currentElement = nextElement; |
| 1027 | } |
| 1028 | return nullptr; |
| 1029 | } |
| 1030 | |
| 1031 | static bool firstChildPrecedesSecondChild(const RenderObject* firstChild, const RenderObject* secondChild, const RenderObject* containingBlock) |
| 1032 | { |
| 1033 | HashSet<CheckedRef<const RenderObject>> firstAncestorChain; |
| 1034 | |
| 1035 | for (auto* first = firstChild; first; first = first->parent()) { |
| 1036 | firstAncestorChain.add(*first); |
| 1037 | if (first == containingBlock) |
| 1038 | break; |
| 1039 | } |
| 1040 | |
| 1041 | auto* second = secondChild; |
| 1042 | for (; second != containingBlock; second = second->parent()) { |
| 1043 | if (firstAncestorChain.contains(second->parent())) { |
| 1044 | for (auto* sibling = second->previousSibling(); sibling; sibling = sibling->previousSibling()) { |
| 1045 | if (firstAncestorChain.contains(sibling)) |
| 1046 | return true; |
| 1047 | } |
| 1048 | return false; |
| 1049 | } |
| 1050 | } |
| 1051 | return false; |
| 1052 | } |
| 1053 | |
| 1054 | // Given an element and its anchor name, locate the closest ancestor (*) element |
| 1055 | // that establishes an anchor scope affecting this anchor name, and return the pointer |
| 1056 | // to such element. If no ancestor establishes an anchor scope affecting this name, |
| 1057 | // returns nullptr. |
| 1058 | // (*): an anchor element can also establish an anchor scope containing itself. In this |
| 1059 | // case, the return value is itself. |
| 1060 | static CheckedPtr<const Element> anchorScopeForAnchorName(const RenderBoxModelObject& renderer, const ResolvedScopedName anchorName) |
| 1061 | { |
| 1062 | // Traverse up the composed tree through itself and each ancestor. |
| 1063 | CheckedPtr<const Element> anchorElement = renderer.element(); |
| 1064 | ASSERT(anchorElement)((void)0); |
| 1065 | for (CheckedPtr<const Element> currentAncestor = anchorElement; currentAncestor; currentAncestor = currentAncestor->parentElementInComposedTree()) { |
| 1066 | CheckedPtr currentAncestorStyle = currentAncestor->renderStyle(); |
| 1067 | if (!currentAncestorStyle) |
| 1068 | continue; |
| 1069 | const auto& currentAncestorAnchorScope = currentAncestorStyle->anchorScope(); |
| 1070 | |
| 1071 | if (Style::NameScope::Type::None == currentAncestorAnchorScope.type) |
| 1072 | continue; |
| 1073 | |
| 1074 | auto styleScope = Scope::forOrdinal(*currentAncestor, currentAncestorAnchorScope.scopeOrdinal); |
| 1075 | ASSERT(styleScope)((void)0); |
| 1076 | if (anchorName.scopeIdentifier() != styleScope->identifier()) |
| 1077 | continue; |
| 1078 | |
| 1079 | if (Style::NameScope::Type::All == currentAncestorAnchorScope.type |
| 1080 | || currentAncestorAnchorScope.names.contains(CustomIdentifier { anchorName.name() })) |
| 1081 | return currentAncestor; |
| 1082 | } |
| 1083 | |
| 1084 | return nullptr; |
| 1085 | } |
| 1086 | |
| 1087 | enum class TopLayerStatus : uint8_t { Same, Lower, Higher }; |
| 1088 | static TopLayerStatus computeTopLayerStatus(const RenderBox& anchored, const RenderBoxModelObject& anchor) |
| 1089 | { |
| 1090 | // Two elements are in the same top layer if they have the same top layer root (including if both are none). |
| 1091 | // An element A is in a higher top layer than an element B if A has a top layer root, and either B has a top |
| 1092 | // layer root earlier in the top layer than A’s, or B doesn’t have a top layer root at all. |
| 1093 | // https://drafts.csswg.org/css-position-4/#top-layer |
| 1094 | |
| 1095 | if (!anchored.document().hasTopLayerElement()) |
| 1096 | return TopLayerStatus::Same; |
| 1097 | |
| 1098 | auto topLayerRoot = [&](auto& renderer) -> const RenderLayerModelObject* { |
| 1099 | for (auto* layer = renderer.enclosingLayer(); layer; layer = layer->parent()) { |
| 1100 | if (layer->establishesTopLayer()) |
| 1101 | return &layer->renderer(); |
| 1102 | } |
| 1103 | return nullptr; |
| 1104 | }; |
| 1105 | |
| 1106 | auto* anchoredRoot = topLayerRoot(anchored); |
| 1107 | auto* anchorRoot = topLayerRoot(anchor); |
| 1108 | if (anchoredRoot == anchorRoot) |
| 1109 | return TopLayerStatus::Same; |
| 1110 | if (!anchoredRoot) |
| 1111 | return TopLayerStatus::Lower; |
| 1112 | if (!anchorRoot) |
| 1113 | return TopLayerStatus::Higher; |
| 1114 | |
| 1115 | auto& topLayerElements = anchored.document().topLayerElements(); |
| 1116 | for (auto& topLayerElement : topLayerElements) { |
| 1117 | if (topLayerElement.ptr() == anchoredRoot->element()) |
| 1118 | return TopLayerStatus::Lower; |
| 1119 | if (topLayerElement.ptr() == anchorRoot->element()) |
| 1120 | return TopLayerStatus::Higher; |
| 1121 | } |
| 1122 | return TopLayerStatus::Lower; |
| 1123 | } |
| 1124 | |
| 1125 | // See: https://drafts.csswg.org/css-anchor-position-1/#acceptable-anchor-element |
| 1126 | static bool isAcceptableAnchorElement(const RenderBoxModelObject& anchorRenderer, Ref<const Element> anchorPositionedElement, const std::optional<ResolvedScopedName> anchorName = { }) |
| 1127 | { |
| 1128 | // "Possible anchor is either an element or a fully styleable tree-abiding pseudo-element." |
| 1129 | // This always have an associated Element (for ::before/::after it is PseudoElement). |
| 1130 | if (!anchorRenderer.element()) |
| 1131 | return false; |
| 1132 | |
| 1133 | CheckedPtr anchorPositionedRenderer = dynamicDowncast<RenderBox>(anchorPositionedElement->renderer()); |
| 1134 | ASSERT(anchorPositionedRenderer)((void)0); |
| 1135 | |
| 1136 | if (anchorName) { |
| 1137 | // Check that anchorRenderer has the specified name. |
| 1138 | ASSERT(anchorRenderer.style().anchorNames().containsIf(((void)0) |
| 1139 | [anchorName](auto& scopedName) { return scopedName.name == anchorName->name(); }((void)0) |
| 1140 | ))((void)0); |
| 1141 | |
| 1142 | // The anchor and anchor-positioned element must be in the same scope. |
| 1143 | auto anchorScopeElement = anchorScopeForAnchorName(anchorRenderer, *anchorName); |
| 1144 | auto anchorPositionedScopeElement = anchorScopeForAnchorName(*anchorPositionedRenderer, *anchorName); |
| 1145 | if (anchorScopeElement != anchorPositionedScopeElement) |
| 1146 | return false; |
| 1147 | } |
| 1148 | |
| 1149 | CheckedPtr containingBlock = anchorPositionedRenderer->container(); |
| 1150 | ASSERT(containingBlock)((void)0); |
| 1151 | |
| 1152 | // "possible anchor is laid out strictly before positioned el, aka one of the following is true:" |
| 1153 | auto topLayerStatus = computeTopLayerStatus(*anchorPositionedRenderer, anchorRenderer); |
| 1154 | switch (topLayerStatus) { |
| 1155 | case TopLayerStatus::Higher: |
| 1156 | // "- positioned el is in a higher top layer than possible anchor" |
| 1157 | return true; |
| 1158 | case TopLayerStatus::Same: { |
| 1159 | // "- Both elements are in the same top layer..." |
| 1160 | auto* penultimateElement = penultimateContainingBlockChainElement(anchorRenderer, containingBlock.get()); |
| 1161 | if (!penultimateElement) |
| 1162 | return false; |
| 1163 | |
| 1164 | if (!penultimateElement->isOutOfFlowPositioned()) |
| 1165 | return true; |
| 1166 | |
| 1167 | return firstChildPrecedesSecondChild(penultimateElement, anchorPositionedRenderer.get(), containingBlock.get()); |
| 1168 | } |
| 1169 | case TopLayerStatus::Lower: |
| 1170 | return false; |
| 1171 | } |
| 1172 | ASSERT_NOT_REACHED()((void)0); |
| 1173 | return false; |
| 1174 | } |
| 1175 | |
| 1176 | static RefPtr<Element> findImplicitAnchor(const Element& anchorPositionedElement) |
| 1177 | { |
| 1178 | auto find = [&]() -> RefPtr<Element> { |
| 1179 | // "The implicit anchor element of a pseudo-element is its originating element, unless otherwise specified." |
| 1180 | // https://drafts.csswg.org/css-anchor-position-1/#implicit |
| 1181 | if (auto pseudoElement = dynamicDowncast<PseudoElement>(anchorPositionedElement)) |
| 1182 | return pseudoElement->hostElement(); |
| 1183 | |
| 1184 | // https://html.spec.whatwg.org/multipage/popover.html#the-popover-attribute |
| 1185 | // 24. Set element's implicit anchor element to invoker. |
| 1186 | if (auto popoverData = anchorPositionedElement.popoverData()) |
| 1187 | return popoverData->invoker(); |
| 1188 | |
| 1189 | return nullptr; |
| 1190 | }; |
| 1191 | |
| 1192 | if (auto implicitAnchorElement = find()) { |
| 1193 | // "If [a spec] defines is an implicit anchor element for query el which is an acceptable anchor element for query el, return that element." |
| 1194 | // https://drafts.csswg.org/css-anchor-position-1/#target |
| 1195 | CheckedPtr anchor = dynamicDowncast<RenderBoxModelObject>(implicitAnchorElement->renderer()); |
| 1196 | if (anchor && isAcceptableAnchorElement(*anchor, anchorPositionedElement)) |
| 1197 | return implicitAnchorElement; |
| 1198 | } |
| 1199 | |
| 1200 | return nullptr; |
| 1201 | } |
| 1202 | |
| 1203 | static RefPtr<Element> findLastAcceptableAnchorWithName(ResolvedScopedName anchorName, const Element& anchorPositionedElement, const AnchorsForAnchorName& anchorsForAnchorName) |
| 1204 | { |
| 1205 | if (anchorName.name() == implicitAnchorElementName().name) |
| 1206 | return findImplicitAnchor(anchorPositionedElement); |
| 1207 | |
| 1208 | const auto& anchors = anchorsForAnchorName.get(anchorName); |
| 1209 | |
| 1210 | for (auto& anchor : anchors | std::views::reverse) { |
| 1211 | if (isAcceptableAnchorElement(anchor.get(), anchorPositionedElement, anchorName)) |
| 1212 | return anchor->element(); |
| 1213 | } |
| 1214 | |
| 1215 | return { }; |
| 1216 | } |
| 1217 | |
| 1218 | static AnchorsForAnchorName collectAnchorsForAnchorName(const Document& document) |
| 1219 | { |
| 1220 | if (!document.renderView()) |
| 1221 | return { }; |
| 1222 | |
| 1223 | AnchorsForAnchorName anchorsForAnchorName; |
| 1224 | |
| 1225 | auto& anchors = document.renderView()->anchors(); |
| 1226 | for (auto& anchorRenderer : anchors) { |
| 1227 | CheckedPtr anchorElement = anchorRenderer.element(); |
| 1228 | ASSERT(anchorElement)((void)0); |
| 1229 | |
| 1230 | for (auto& scopedName : anchorRenderer.style().anchorNames()) { |
| 1231 | auto resolvedScopedName = ResolvedScopedName::createFromScopedName(*anchorElement, scopedName); |
| 1232 | |
| 1233 | anchorsForAnchorName.ensure(resolvedScopedName, [&] { |
| 1234 | return AnchorsForAnchorName::MappedType { }; |
| 1235 | }).iterator->value.append(anchorRenderer); |
| 1236 | } |
| 1237 | } |
| 1238 | |
| 1239 | // Sort them in tree order. |
| 1240 | for (auto& anchors : anchorsForAnchorName.values()) { |
| 1241 | std::ranges::sort(anchors, [](auto& a, auto& b) { |
| 1242 | // FIXME: Figure out anonymous pseudo-elements. |
| 1243 | if (!a->element() || !b->element()) |
| 1244 | return !!b->element(); |
| 1245 | return is_lt(treeOrder<ComposedTree>(*a->element(), *b->element())); |
| 1246 | }); |
| 1247 | } |
| 1248 | |
| 1249 | return anchorsForAnchorName; |
| 1250 | } |
| 1251 | |
| 1252 | AnchorElements AnchorPositionEvaluator::findAnchorsForAnchorPositionedElement(const Element& anchorPositionedElement, const HashSet<ResolvedScopedName>& anchorNames, const AnchorsForAnchorName& anchorsForAnchorName) |
| 1253 | { |
| 1254 | AnchorElements anchorElements; |
| 1255 | |
| 1256 | for (auto& anchorName : anchorNames) { |
| 1257 | auto anchor = findLastAcceptableAnchorWithName(anchorName, anchorPositionedElement, anchorsForAnchorName); |
| 1258 | anchorElements.add(anchorName, anchor); |
| 1259 | } |
| 1260 | |
| 1261 | return anchorElements; |
| 1262 | } |
| 1263 | |
| 1264 | void AnchorPositionEvaluator::updateAnchorPositioningStatesAfterInterleavedLayout(Document& document, AnchorPositionedStates& anchorPositionedStates) |
| 1265 | { |
| 1266 | if (anchorPositionedStates.isEmpty()) |
| 1267 | return; |
| 1268 | |
| 1269 | // FIXME: Make the code below oeprate on renderers (boxes) rather than elements. |
| 1270 | auto anchorsForAnchorName = collectAnchorsForAnchorName(document); |
| 1271 | |
| 1272 | for (auto& elementAndState : anchorPositionedStates) { |
| 1273 | auto& state = *elementAndState.value; |
| 1274 | |
| 1275 | switch (state.stage) { |
| 1276 | case AnchorPositionResolutionStage::FindAnchors: { |
| 1277 | RefPtr element = elementAndState.key.first; |
| 1278 | if (elementAndState.key.second) |
| 1279 | element = element->pseudoElementIfExists(*elementAndState.key.second); |
| 1280 | |
| 1281 | CheckedPtr renderer = element ? element->renderer() : nullptr; |
| 1282 | if (renderer) { |
| 1283 | // FIXME: Remove the redundant anchorElements member. The mappings are available in anchorPositionedToAnchorMap. |
| 1284 | state.anchorElements = findAnchorsForAnchorPositionedElement(*element, state.anchorNames, anchorsForAnchorName); |
| 1285 | if (isLayoutTimeAnchorPositioned(renderer->style())) |
| 1286 | renderer->setNeedsLayout(); |
| 1287 | |
| 1288 | Vector<ResolvedAnchor> anchors; |
| 1289 | for (auto& anchorNameAndElement : state.anchorElements) { |
| 1290 | CheckedPtr anchorElement = anchorNameAndElement.value.get(); |
| 1291 | anchors.append(ResolvedAnchor { |
| 1292 | .renderer = anchorElement ? dynamicDowncast<RenderBoxModelObject>(anchorElement->renderer()) : nullptr, |
| 1293 | .name = anchorNameAndElement.key |
| 1294 | }); |
| 1295 | } |
| 1296 | document.styleScope().anchorPositionedToAnchorMap().set(*element, AnchorPositionedToAnchorEntry { |
| 1297 | .pseudoElementIdentifier = elementAndState.key.second, |
| 1298 | .anchors = WTF::move(anchors) |
| 1299 | }); |
| 1300 | } |
| 1301 | state.stage = renderer && renderer->style().usesAnchorFunctions() ? AnchorPositionResolutionStage::ResolveAnchorFunctions : AnchorPositionResolutionStage::Resolved; |
| 1302 | break; |
| 1303 | } |
| 1304 | |
| 1305 | case AnchorPositionResolutionStage::ResolveAnchorFunctions: |
| 1306 | case AnchorPositionResolutionStage::Resolved: |
| 1307 | if (CheckedPtr anchored = elementAndState.key.first->renderer()) { |
| 1308 | if (auto anchoredBox = dynamicDowncast<RenderBox>(anchored.get())) |
| 1309 | AnchorPositionEvaluator::captureScrollSnapshots(*anchoredBox, false); |
| 1310 | } |
| 1311 | state.stage = AnchorPositionResolutionStage::Positioned; |
| 1312 | break; |
| 1313 | |
| 1314 | case AnchorPositionResolutionStage::Positioned: |
| 1315 | break; |
| 1316 | } |
| 1317 | } |
| 1318 | } |
| 1319 | |
| 1320 | void AnchorPositionEvaluator::updateAnchorPositionedStateForDefaultAnchorAndPositionVisibility(Element& element, const RenderStyle& style, AnchorPositionedStates& states) |
| 1321 | { |
| 1322 | auto shouldResolveDefaultAnchor = isAnchorPositioned(style); |
| 1323 | |
| 1324 | // `position-visibility: no-overflow` should also work for non-anchor positioned out-of-flow boxes. |
| 1325 | // Create an empty anchor positioning state for it so we perform the required layout interleaving. |
| 1326 | auto hasPositionVisibilityNoOverflow = style.display().doesGenerateBox() |
| 1327 | && style.hasOutOfFlowPosition() |
| 1328 | && style.positionVisibility().contains(PositionVisibilityValue::NoOverflow); |
| 1329 | |
| 1330 | if (!shouldResolveDefaultAnchor && !hasPositionVisibilityNoOverflow) |
| 1331 | return; |
| 1332 | |
| 1333 | auto* state = states.ensure({ &element, style.pseudoElementIdentifier() }, [&] { |
| 1334 | return makeUnique<AnchorPositionedState>(); |
| 1335 | }).iterator->value.get(); |
| 1336 | |
| 1337 | if (shouldResolveDefaultAnchor) { |
| 1338 | // Always resolve the default anchor. Even if nothing is anchored to it we need it to compute the scroll compensation. |
| 1339 | auto resolvedDefaultAnchor = ResolvedScopedName::createFromScopedName(element, defaultAnchorName(style)); |
| 1340 | if (state->anchorNames.add(resolvedDefaultAnchor).isNewEntry) { |
| 1341 | // If anchor resolution has progressed past FindAnchors, and we pick up a new anchor name, set the |
| 1342 | // stage back to FindAnchors. This restarts the resolution process to resolve newly added names. |
| 1343 | state->stage = AnchorPositionResolutionStage::FindAnchors; |
| 1344 | } |
| 1345 | } |
| 1346 | } |
| 1347 | |
| 1348 | auto AnchorPositionEvaluator::makeAnchorPositionedForAnchorMap(AnchorPositionedToAnchorMap& toAnchorMap) -> AnchorToAnchorPositionedMap |
| 1349 | { |
| 1350 | AnchorToAnchorPositionedMap map; |
| 1351 | |
| 1352 | for (auto elementAndAnchors : toAnchorMap) { |
| 1353 | CheckedRef anchorPositionedElement = elementAndAnchors.key; |
| 1354 | for (auto& anchor : elementAndAnchors.value.anchors) { |
| 1355 | if (!anchor.renderer) |
| 1356 | continue; |
| 1357 | map.ensure(*anchor.renderer, [&] { |
| 1358 | return Vector<Ref<Element>> { }; |
| 1359 | }).iterator->value.append(anchorPositionedElement); |
| 1360 | } |
| 1361 | } |
| 1362 | return map; |
| 1363 | } |
| 1364 | |
| 1365 | bool AnchorPositionEvaluator::isAnchorPositioned(const RenderStyle& style) |
| 1366 | { |
| 1367 | return isStyleTimeAnchorPositioned(style) || isLayoutTimeAnchorPositioned(style); |
| 1368 | } |
| 1369 | |
| 1370 | bool AnchorPositionEvaluator::isStyleTimeAnchorPositioned(const RenderStyle& style) |
| 1371 | { |
| 1372 | if (!style.display().doesGenerateBox() || !style.hasOutOfFlowPosition()) |
| 1373 | return false; |
| 1374 | |
| 1375 | return style.usesAnchorFunctions(); |
| 1376 | } |
| 1377 | |
| 1378 | bool AnchorPositionEvaluator::isLayoutTimeAnchorPositioned(const RenderStyle& style) |
| 1379 | { |
| 1380 | if (!style.display().doesGenerateBox() || !style.hasOutOfFlowPosition()) |
| 1381 | return false; |
| 1382 | |
| 1383 | if (!style.positionArea().isNone()) |
| 1384 | return true; |
| 1385 | |
| 1386 | return style.justifySelf().isAnchorCenter() || style.alignSelf().isAnchorCenter(); |
| 1387 | } |
| 1388 | |
| 1389 | static CSSPropertyID NODELETE[[clang::annotate_type("webkit.nodelete")]] flipHorizontal(CSSPropertyID propertyID) |
| 1390 | { |
| 1391 | switch (propertyID) { |
| 1392 | case CSSPropertyLeft: |
| 1393 | return CSSPropertyRight; |
| 1394 | case CSSPropertyRight: |
| 1395 | return CSSPropertyLeft; |
| 1396 | case CSSPropertyMarginLeft: |
| 1397 | return CSSPropertyMarginRight; |
| 1398 | case CSSPropertyMarginRight: |
| 1399 | return CSSPropertyMarginLeft; |
| 1400 | default: |
| 1401 | return propertyID; |
| 1402 | } |
| 1403 | } |
| 1404 | |
| 1405 | static CSSPropertyID NODELETE[[clang::annotate_type("webkit.nodelete")]] flipVertical(CSSPropertyID propertyID) |
| 1406 | { |
| 1407 | switch (propertyID) { |
| 1408 | case CSSPropertyTop: |
| 1409 | return CSSPropertyBottom; |
| 1410 | case CSSPropertyBottom: |
| 1411 | return CSSPropertyTop; |
| 1412 | case CSSPropertyMarginTop: |
| 1413 | return CSSPropertyMarginBottom; |
| 1414 | case CSSPropertyMarginBottom: |
| 1415 | return CSSPropertyMarginTop; |
| 1416 | default: |
| 1417 | return propertyID; |
| 1418 | } |
| 1419 | } |
| 1420 | |
| 1421 | static CSSPropertyID flipStart(CSSPropertyID propertyID, WritingMode writingMode) |
| 1422 | { |
| 1423 | auto logicalProperty = CSSProperty::unresolvePhysicalProperty(propertyID, writingMode); |
| 1424 | |
| 1425 | auto flippedLogical = [&] { |
| 1426 | switch (logicalProperty) { |
| 1427 | case CSSPropertyInsetBlockStart: |
| 1428 | return CSSPropertyInsetInlineStart; |
| 1429 | case CSSPropertyInsetBlockEnd: |
| 1430 | return CSSPropertyInsetInlineEnd; |
| 1431 | case CSSPropertyBlockSize: |
| 1432 | return CSSPropertyInlineSize; |
| 1433 | case CSSPropertyMinBlockSize: |
| 1434 | return CSSPropertyMinInlineSize; |
| 1435 | case CSSPropertyMaxBlockSize: |
| 1436 | return CSSPropertyMaxInlineSize; |
| 1437 | case CSSPropertyInsetInlineStart: |
| 1438 | return CSSPropertyInsetBlockStart; |
| 1439 | case CSSPropertyInsetInlineEnd: |
| 1440 | return CSSPropertyInsetBlockEnd; |
| 1441 | case CSSPropertyInlineSize: |
| 1442 | return CSSPropertyBlockSize; |
| 1443 | case CSSPropertyMinInlineSize: |
| 1444 | return CSSPropertyMinBlockSize; |
| 1445 | case CSSPropertyMaxInlineSize: |
| 1446 | return CSSPropertyMaxBlockSize; |
| 1447 | case CSSPropertyMarginBlockStart: |
| 1448 | return CSSPropertyMarginInlineStart; |
| 1449 | case CSSPropertyMarginBlockEnd: |
| 1450 | return CSSPropertyMarginInlineEnd; |
| 1451 | case CSSPropertyMarginInlineStart: |
| 1452 | return CSSPropertyMarginBlockStart; |
| 1453 | case CSSPropertyMarginInlineEnd: |
| 1454 | return CSSPropertyMarginBlockEnd; |
| 1455 | case CSSPropertyAlignSelf: |
| 1456 | return CSSPropertyJustifySelf; |
| 1457 | case CSSPropertyJustifySelf: |
| 1458 | return CSSPropertyAlignSelf; |
| 1459 | default: |
| 1460 | return propertyID; |
| 1461 | } |
| 1462 | }; |
| 1463 | return CSSProperty::resolveDirectionAwareProperty(flippedLogical(), writingMode); |
| 1464 | } |
| 1465 | |
| 1466 | CSSPropertyID AnchorPositionEvaluator::resolvePositionTryFallbackProperty(CSSPropertyID propertyID, WritingMode writingMode, const BuilderPositionTryFallback& fallback) |
| 1467 | { |
| 1468 | ASSERT(!CSSProperty::isDirectionAwareProperty(propertyID))((void)0); |
| 1469 | |
| 1470 | for (auto tactic : fallback.tactics) { |
| 1471 | switch (tactic) { |
| 1472 | case PositionTryFallback::Tactic::FlipInline: |
| 1473 | propertyID = writingMode.isHorizontal() ? flipHorizontal(propertyID) : flipVertical(propertyID); |
| 1474 | break; |
| 1475 | case PositionTryFallback::Tactic::FlipBlock: |
| 1476 | propertyID = writingMode.isHorizontal() ? flipVertical(propertyID) : flipHorizontal(propertyID); |
| 1477 | break; |
| 1478 | case PositionTryFallback::Tactic::FlipX: |
| 1479 | propertyID = flipHorizontal(propertyID); |
| 1480 | break; |
| 1481 | case PositionTryFallback::Tactic::FlipY: |
| 1482 | propertyID = flipVertical(propertyID); |
| 1483 | break; |
| 1484 | case PositionTryFallback::Tactic::FlipStart: |
| 1485 | propertyID = flipStart(propertyID, writingMode); |
| 1486 | break; |
| 1487 | } |
| 1488 | } |
| 1489 | return propertyID; |
| 1490 | } |
| 1491 | |
| 1492 | CSSValueID AnchorPositionEvaluator::resolvePositionTryFallbackValueForSelfPosition(CSSPropertyID propertyID, CSSValueID position, WritingMode writingMode, const BuilderPositionTryFallback& fallback) |
| 1493 | { |
| 1494 | // Implements the bullet starting "For the self-alignment properties" from step 4 of https://drafts.csswg.org/css-anchor-position-1/#swap-due-to-a-try-tactic. |
| 1495 | |
| 1496 | ASSERT(propertyID == CSSPropertyAlignSelf || propertyID == CSSPropertyJustifySelf)((void)0); |
| 1497 | |
| 1498 | auto flipSidedPosition = [](auto position) -> CSSValueID { |
| 1499 | // Swap to the "opposite" position if the current position is "sided". |
| 1500 | // If there is no opposite value, nothing is changed. |
| 1501 | switch (position) { |
| 1502 | case CSSValueStart: |
| 1503 | return CSSValueEnd; |
| 1504 | case CSSValueEnd: |
| 1505 | return CSSValueStart; |
| 1506 | case CSSValueSelfStart: |
| 1507 | return CSSValueSelfEnd; |
| 1508 | case CSSValueSelfEnd: |
| 1509 | return CSSValueSelfStart; |
| 1510 | case CSSValueFlexStart: |
| 1511 | return CSSValueFlexEnd; |
| 1512 | case CSSValueFlexEnd: |
| 1513 | return CSSValueFlexStart; |
| 1514 | case CSSValueLeft: |
| 1515 | return CSSValueRight; |
| 1516 | case CSSValueRight: |
| 1517 | return CSSValueLeft; |
| 1518 | default: |
| 1519 | return position; |
| 1520 | } |
| 1521 | }; |
| 1522 | |
| 1523 | auto flipStart = [](auto writingMode, auto position) -> CSSValueID { |
| 1524 | // `justify-self` additionally takes `left`/`right`, `align-self` doesn't. When |
| 1525 | // applying `flip-start`, `justify-self` gets swapped with `align-self` (see |
| 1526 | // call to `flipStart` in `AnchorPositionEvaluator::resolvePositionTryFallbackProperty`). |
| 1527 | // So if we're resolving `justify-self` (which later gets swapped with `align-self`), |
| 1528 | // and the position is `left`/`right`, resolve it to `self-start`/`self-end`. |
| 1529 | switch (position) { |
| 1530 | case CSSValueLeft: |
| 1531 | return writingMode.bidiDirection() == TextDirection::LTR ? CSSValueSelfStart : CSSValueSelfEnd; |
| 1532 | case CSSValueRight: |
| 1533 | return writingMode.bidiDirection() == TextDirection::LTR ? CSSValueSelfEnd : CSSValueSelfStart; |
| 1534 | default: |
| 1535 | return position; |
| 1536 | } |
| 1537 | }; |
| 1538 | |
| 1539 | for (auto tactic : fallback.tactics) { |
| 1540 | switch (tactic) { |
| 1541 | case PositionTryFallback::Tactic::FlipBlock: |
| 1542 | if (propertyID == CSSPropertyAlignSelf) |
| 1543 | position = flipSidedPosition(position); |
| 1544 | break; |
| 1545 | case PositionTryFallback::Tactic::FlipInline: |
| 1546 | if (propertyID == CSSPropertyJustifySelf) |
| 1547 | position = flipSidedPosition(position); |
| 1548 | break; |
| 1549 | case PositionTryFallback::Tactic::FlipX: |
| 1550 | if (propertyID == (writingMode.isHorizontal() ? CSSPropertyJustifySelf : CSSPropertyAlignSelf)) |
| 1551 | position = flipSidedPosition(position); |
| 1552 | break; |
| 1553 | case PositionTryFallback::Tactic::FlipY: |
| 1554 | if (propertyID == (writingMode.isHorizontal() ? CSSPropertyAlignSelf : CSSPropertyJustifySelf)) |
| 1555 | position = flipSidedPosition(position); |
| 1556 | break; |
| 1557 | case PositionTryFallback::Tactic::FlipStart: |
| 1558 | if (propertyID == CSSPropertyJustifySelf) |
| 1559 | position = flipStart(writingMode, position); |
| 1560 | break; |
| 1561 | } |
| 1562 | } |
| 1563 | |
| 1564 | return position; |
| 1565 | } |
| 1566 | |
| 1567 | bool AnchorPositionEvaluator::overflowsInsetModifiedContainingBlock(const RenderBox& anchoredBox) |
| 1568 | { |
| 1569 | if (!anchoredBox.isOutOfFlowPositioned()) |
| 1570 | return false; |
| 1571 | |
| 1572 | auto inlineConstraints = PositionedLayoutConstraints { anchoredBox, LogicalBoxAxis::Inline }; |
| 1573 | auto blockConstraints = PositionedLayoutConstraints { anchoredBox, LogicalBoxAxis::Block }; |
| 1574 | inlineConstraints.computeInsets(); |
| 1575 | blockConstraints.computeInsets(); |
| 1576 | |
| 1577 | auto anchorInlineSize = anchoredBox.logicalWidth() + anchoredBox.marginStart() + anchoredBox.marginEnd(); |
| 1578 | auto anchorBlockSize = anchoredBox.logicalHeight() + anchoredBox.marginBefore() + anchoredBox.marginAfter(); |
| 1579 | |
| 1580 | return inlineConstraints.insetModifiedContainingSize() < anchorInlineSize |
| 1581 | || blockConstraints.insetModifiedContainingSize() < anchorBlockSize; |
| 1582 | } |
| 1583 | |
| 1584 | bool AnchorPositionEvaluator::isDefaultAnchorInvisibleOrClippedByInterveningBoxes(const RenderBox& anchoredBox) |
| 1585 | { |
| 1586 | CheckedPtr defaultAnchor = defaultAnchorForBox(anchoredBox); |
| 1587 | if (!defaultAnchor) |
| 1588 | return false; |
| 1589 | |
| 1590 | if (defaultAnchor->style().usedVisibility() == Visibility::Hidden) |
| 1591 | return true; |
| 1592 | |
| 1593 | CheckedPtr anchorBox = dynamicDowncast<RenderBox>(*defaultAnchor); |
| 1594 | |
| 1595 | // https://drafts.csswg.org/css-anchor-position-1/#position-visibility |
| 1596 | // "An anchor box anchor is clipped by intervening boxes relative to a positioned box abspos relying on it if anchor’s ink overflow |
| 1597 | // rectangle is fully clipped by a box which is an ancestor of anchor but a descendant of abspos’s containing block." |
| 1598 | |
| 1599 | auto localAnchorRect = [&] { |
| 1600 | if (anchorBox) |
| 1601 | return anchorBox->visualOverflowRect(); |
| 1602 | return downcast<RenderInline>(*defaultAnchor).linesVisualOverflowBoundingBox(); |
| 1603 | }(); |
| 1604 | auto* anchoredContainingBlock = anchoredBox.container(); |
| 1605 | |
| 1606 | auto anchorRect = defaultAnchor->localToAbsoluteQuad(FloatQuad { localAnchorRect }).boundingBox(); |
| 1607 | |
| 1608 | for (auto* anchorAncestor = defaultAnchor->container(); anchorAncestor && anchorAncestor != anchoredContainingBlock; anchorAncestor = anchorAncestor->container()) { |
| 1609 | if (!anchorAncestor->hasNonVisibleOverflow()) |
| 1610 | continue; |
| 1611 | auto* clipAncestor = dynamicDowncast<RenderBox>(*anchorAncestor); |
| 1612 | if (!clipAncestor) |
| 1613 | continue; |
| 1614 | auto localClipRect = clipAncestor->overflowClipRect({ }); |
| 1615 | auto clipRect = clipAncestor->localToAbsoluteQuad(FloatQuad { localClipRect }).boundingBox(); |
| 1616 | if (!clipRect.intersects(anchorRect)) |
| 1617 | return true; |
| 1618 | } |
| 1619 | |
| 1620 | if (anchorBox) { |
| 1621 | // Test for chained anchors. |
| 1622 | if (isDefaultAnchorInvisibleOrClippedByInterveningBoxes(*anchorBox)) |
| 1623 | return true; |
| 1624 | } |
| 1625 | |
| 1626 | return false; |
| 1627 | } |
| 1628 | |
| 1629 | // FIXME: The code should operate fully on host/pseudoElementIdentifier pairs and not use PseudoElements to |
| 1630 | // support pseudo-elements other than ::before/::after. |
| 1631 | RefPtr<const Element> AnchorPositionEvaluator::anchorPositionedElementOrPseudoElement(BuilderState& builderState) |
| 1632 | { |
| 1633 | RefPtr element = builderState.element(); |
| 1634 | if (auto identifier = builderState.style().pseudoElementIdentifier()) |
| 1635 | return element->pseudoElementIfExists(*identifier); |
| 1636 | return element; |
| 1637 | } |
| 1638 | |
| 1639 | AnchorPositionedKey AnchorPositionEvaluator::keyForElementOrPseudoElement(const Element& element) |
| 1640 | { |
| 1641 | if (auto* pseudoElement = dynamicDowncast<PseudoElement>(element)) |
| 1642 | return { pseudoElement->hostElement(), PseudoElementIdentifier { pseudoElement->pseudoElementType() } }; |
| 1643 | return { &element, { } }; |
| 1644 | } |
| 1645 | |
| 1646 | bool AnchorPositionEvaluator::isAnchor(const RenderStyle& style) |
| 1647 | { |
| 1648 | if (!style.anchorNames().isNone()) |
| 1649 | return true; |
| 1650 | |
| 1651 | return isImplicitAnchor(style); |
| 1652 | } |
| 1653 | |
| 1654 | bool AnchorPositionEvaluator::isImplicitAnchor(const RenderStyle& style) |
| 1655 | { |
| 1656 | // The invoker is an implicit anchor for the popover. |
| 1657 | // https://drafts.csswg.org/css-anchor-position-1/#implicit |
| 1658 | if (style.isPopoverInvoker()) |
| 1659 | return true; |
| 1660 | |
| 1661 | // "The implicit anchor element of a pseudo-element is its originating element, unless otherwise specified." |
| 1662 | // https://drafts.csswg.org/css-anchor-position-1/#implicit |
| 1663 | auto isImplicitAnchorForPseudoElement = [&](PseudoElementType pseudoElementType) { |
| 1664 | const RenderStyle* pseudoElementStyle = style.getCachedPseudoStyle({ pseudoElementType }); |
| 1665 | if (!pseudoElementStyle) |
| 1666 | return false; |
| 1667 | // If we have an explicit anchor name then there is no need for an implicit anchor. |
| 1668 | if (!pseudoElementStyle->positionAnchor().isAuto()) |
| 1669 | return false; |
| 1670 | |
| 1671 | return pseudoElementStyle->usesAnchorFunctions() || isLayoutTimeAnchorPositioned(*pseudoElementStyle); |
| 1672 | }; |
| 1673 | return isImplicitAnchorForPseudoElement(PseudoElementType::Before) || isImplicitAnchorForPseudoElement(PseudoElementType::After); |
| 1674 | } |
| 1675 | |
| 1676 | ScopedName AnchorPositionEvaluator::defaultAnchorName(const RenderStyle& style) |
| 1677 | { |
| 1678 | if (auto name = style.positionAnchor().tryName()) |
| 1679 | return *name; |
| 1680 | return implicitAnchorElementName(); |
| 1681 | } |
| 1682 | |
| 1683 | CheckedPtr<RenderBoxModelObject> AnchorPositionEvaluator::defaultAnchorForBox(const RenderBox& box) |
| 1684 | { |
| 1685 | if (!box.element()) |
| 1686 | return nullptr; |
| 1687 | |
| 1688 | CheckedRef element = *box.element(); |
| 1689 | |
| 1690 | auto& anchorPositionedMap = box.document().styleScope().anchorPositionedToAnchorMap(); |
| 1691 | auto it = anchorPositionedMap.find(element); |
| 1692 | if (it == anchorPositionedMap.end()) |
| 1693 | return nullptr; |
| 1694 | |
| 1695 | auto anchorName = ResolvedScopedName::createFromScopedName(element, defaultAnchorName(box.style())); |
| 1696 | |
| 1697 | for (auto& anchor : it->value.anchors) { |
| 1698 | if (anchorName == anchor.name) |
| 1699 | return anchor.renderer.get(); |
| 1700 | } |
| 1701 | return nullptr; |
| 1702 | } |
| 1703 | |
| 1704 | HashMap<AnchorPositionedKey, size_t> AnchorPositionEvaluator::recordLastSuccessfulPositionOptions(const SingleThreadWeakHashSet<const RenderBox>& positionTryBoxes) |
| 1705 | { |
| 1706 | HashMap<Style::AnchorPositionedKey, size_t> lastSuccessfulPositionOptionMap; |
| 1707 | |
| 1708 | for (const auto& positionTryBox : positionTryBoxes) { |
| 1709 | auto styleable = Styleable::fromRenderer(positionTryBox); |
| 1710 | ASSERT(styleable)((void)0); |
| 1711 | |
| 1712 | if (auto usedPositionOptionIndex = positionTryBox.style().usedPositionOptionIndex()) |
| 1713 | lastSuccessfulPositionOptionMap.add({ styleable->element, styleable->pseudoElementIdentifier }, *usedPositionOptionIndex); |
| 1714 | } |
| 1715 | |
| 1716 | return lastSuccessfulPositionOptionMap; |
| 1717 | } |
| 1718 | |
| 1719 | } // namespace Style |
| 1720 | |
| 1721 | } // namespace WebCore |