| /* |
| * Copyright (C) 2022 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND ANY |
| * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
| * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| * DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY |
| * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES |
| * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
| * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON |
| * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
| * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "config.h" |
| #include "ContainerQueryEvaluator.h" |
| |
| #include "CSSPrimitiveValue.h" |
| #include "CSSToLengthConversionData.h" |
| #include "CSSValueList.h" |
| #include "ComposedTreeAncestorIterator.h" |
| #include "Document.h" |
| #include "MediaFeatureNames.h" |
| #include "MediaList.h" |
| #include "MediaQuery.h" |
| #include "RenderView.h" |
| #include "StyleRule.h" |
| #include "StyleScope.h" |
| |
| namespace WebCore::Style { |
| |
| struct ContainerQueryEvaluator::SelectedContainer { |
| const RenderBox* renderer { nullptr }; |
| CSSToLengthConversionData conversionData; |
| }; |
| |
| ContainerQueryEvaluator::ContainerQueryEvaluator(const Element& element, SelectionMode selectionMode, ScopeOrdinal scopeOrdinal, SelectorMatchingState* selectorMatchingState) |
| : m_element(element) |
| , m_selectionMode(selectionMode) |
| , m_scopeOrdinal(scopeOrdinal) |
| , m_selectorMatchingState(selectorMatchingState) |
| { |
| } |
| |
| bool ContainerQueryEvaluator::evaluate(const FilteredContainerQuery& filteredContainerQuery) const |
| { |
| auto container = selectContainer(filteredContainerQuery); |
| if (!container) |
| return false; |
| |
| return evaluateQuery(filteredContainerQuery.query, *container) == EvaluationResult::True; |
| } |
| |
| auto ContainerQueryEvaluator::selectContainer(const FilteredContainerQuery& filteredContainerQuery) const -> std::optional<SelectedContainer> |
| { |
| // "For each element, the query container to be queried is selected from among the element’s |
| // ancestor query containers that have a valid container-type for all the container features |
| // in the <container-condition>. The optional <container-name> filters the set of query containers |
| // considered to just those with a matching query container name." |
| // https://drafts.csswg.org/css-contain-3/#container-rule |
| |
| auto makeSelectedContainer = [](const Element& element) -> SelectedContainer { |
| auto* renderer = dynamicDowncast<RenderBox>(element.renderer()); |
| if (!renderer) |
| return { }; |
| return { |
| renderer, |
| CSSToLengthConversionData { renderer->style(), element.document().documentElement()->renderStyle(), nullptr, &renderer->view() } |
| }; |
| }; |
| |
| auto* cachedQueryContainers = m_selectorMatchingState ? &m_selectorMatchingState->queryContainers : nullptr; |
| |
| auto* container = selectContainer(filteredContainerQuery.axisFilter, filteredContainerQuery.nameFilter, m_element.get(), m_selectionMode, m_scopeOrdinal, cachedQueryContainers); |
| if (!container) |
| return { }; |
| |
| return makeSelectedContainer(*container); |
| } |
| |
| const Element* ContainerQueryEvaluator::selectContainer(OptionSet<CQ::Axis> axes, const String& name, const Element& element, SelectionMode selectionMode, ScopeOrdinal scopeOrdinal, const CachedQueryContainers* cachedQueryContainers) |
| { |
| // "For each element, the query container to be queried is selected from among the element’s |
| // ancestor query containers that have a valid container-type for all the container features |
| // in the <container-condition>. The optional <container-name> filters the set of query containers |
| // considered to just those with a matching query container name." |
| // https://drafts.csswg.org/css-contain-3/#container-rule |
| |
| auto isValidContainerForRequiredAxes = [&](ContainerType containerType, const RenderElement* principalBox) { |
| switch (containerType) { |
| case ContainerType::Size: |
| return true; |
| case ContainerType::InlineSize: |
| // Without a principal box the container matches but the query against it will evaluate to Unknown. |
| if (!principalBox) |
| return true; |
| if (axes.contains(CQ::Axis::Block)) |
| return false; |
| return !axes.contains(principalBox->isHorizontalWritingMode() ? CQ::Axis::Height : CQ::Axis::Width); |
| case ContainerType::None: |
| return false; |
| } |
| RELEASE_ASSERT_NOT_REACHED(); |
| }; |
| |
| auto isContainerForQuery = [&](const Element& element) { |
| auto* style = element.existingComputedStyle(); |
| if (!style) |
| return false; |
| if (!isValidContainerForRequiredAxes(style->containerType(), element.renderer())) |
| return false; |
| if (name.isEmpty()) |
| return true; |
| return style->containerNames().contains(name); |
| }; |
| |
| auto findOriginatingElement = [&]() -> const Element* { |
| // ::part() selectors can query its originating host, but not internal query containers inside the shadow tree. |
| if (scopeOrdinal <= ScopeOrdinal::ContainingHost) |
| return hostForScopeOrdinal(element, scopeOrdinal); |
| // ::slotted() selectors can query containers inside the shadow tree, including the slot itself. |
| if (scopeOrdinal >= ScopeOrdinal::FirstSlot && scopeOrdinal <= ScopeOrdinal::SlotLimit) |
| return assignedSlotForScopeOrdinal(element, scopeOrdinal); |
| return nullptr; |
| }; |
| |
| if (auto* originatingElement = findOriginatingElement()) { |
| // For selectors with pseudo elements, query containers can be established by the shadow-including inclusive ancestors of the ultimate originating element. |
| for (auto* ancestor = originatingElement; ancestor; ancestor = ancestor->parentOrShadowHostElement()) { |
| if (isContainerForQuery(*ancestor)) |
| return ancestor; |
| } |
| return nullptr; |
| } |
| |
| if (selectionMode == SelectionMode::PseudoElement) { |
| if (isContainerForQuery(element)) |
| return &element; |
| } |
| |
| if (cachedQueryContainers) { |
| for (auto& container : makeReversedRange(*cachedQueryContainers)) { |
| if (isContainerForQuery(container)) |
| return container.ptr(); |
| } |
| return { }; |
| } |
| |
| for (auto* ancestor = element.parentOrShadowHostElement(); ancestor; ancestor = ancestor->parentOrShadowHostElement()) { |
| if (isContainerForQuery(*ancestor)) |
| return ancestor; |
| } |
| return { }; |
| } |
| |
| auto ContainerQueryEvaluator::evaluateQuery(const CQ::ContainerQuery& containerQuery, const SelectedContainer& container) const -> EvaluationResult |
| { |
| return WTF::switchOn(containerQuery, [&](const CQ::ContainerCondition& containerCondition) { |
| return evaluateCondition(containerCondition, container); |
| }, [&](const CQ::SizeFeature& sizeFeature) { |
| return evaluateSizeFeature(sizeFeature, container); |
| }, [&](const CQ::UnknownQuery&) { |
| return EvaluationResult::Unknown; |
| }); |
| } |
| |
| template<typename ConditionType> |
| auto ContainerQueryEvaluator::evaluateCondition(const ConditionType& condition, const SelectedContainer& container) const -> EvaluationResult |
| { |
| if (condition.queries.isEmpty()) |
| return EvaluationResult::Unknown; |
| |
| switch (condition.logicalOperator) { |
| case CQ::LogicalOperator::Not: |
| return !evaluateQuery(condition.queries.first(), container); |
| case CQ::LogicalOperator::And: { |
| auto result = EvaluationResult::True; |
| for (auto query : condition.queries) { |
| auto queryResult = evaluateQuery(query, container); |
| if (queryResult == EvaluationResult::False) |
| return EvaluationResult::False; |
| if (queryResult == EvaluationResult::Unknown) |
| result = EvaluationResult::Unknown; |
| } |
| return result; |
| } |
| case CQ::LogicalOperator::Or: { |
| auto result = EvaluationResult::False; |
| for (auto query : condition.queries) { |
| auto queryResult = evaluateQuery(query, container); |
| if (queryResult == EvaluationResult::True) |
| return EvaluationResult::True; |
| if (queryResult == EvaluationResult::Unknown) |
| result = EvaluationResult::Unknown; |
| } |
| return result; |
| } |
| } |
| RELEASE_ASSERT_NOT_REACHED(); |
| } |
| |
| static std::optional<LayoutUnit> computeSize(const CSSValue* value, const CSSToLengthConversionData& conversionData) |
| { |
| if (!is<CSSPrimitiveValue>(value)) |
| return { }; |
| auto& primitiveValue = downcast<CSSPrimitiveValue>(*value); |
| |
| if (primitiveValue.isNumberOrInteger()) { |
| if (primitiveValue.doubleValue()) |
| return { }; |
| return 0_lu; |
| } |
| |
| if (!primitiveValue.isLength()) |
| return { }; |
| return primitiveValue.computeLength<LayoutUnit>(conversionData); |
| } |
| |
| auto ContainerQueryEvaluator::evaluateSizeFeature(const CQ::SizeFeature& sizeFeature, const SelectedContainer& container) const -> EvaluationResult |
| { |
| // "If the query container does not have a principal box, or the principal box is not a layout containment box, |
| // or the query container does not support container size queries on the relevant axes, then the result of |
| // evaluating the size feature is unknown." |
| // https://drafts.csswg.org/css-contain-3/#size-container |
| if (!container.renderer) |
| return EvaluationResult::Unknown; |
| |
| auto& renderer = *container.renderer; |
| |
| auto hasEligibleContainment = [&] { |
| if (!renderer.shouldApplyLayoutContainment()) |
| return false; |
| switch (renderer.style().containerType()) { |
| case ContainerType::InlineSize: |
| return renderer.shouldApplyInlineSizeContainment(); |
| case ContainerType::Size: |
| return renderer.shouldApplySizeContainment(); |
| case ContainerType::None: |
| return true; |
| } |
| RELEASE_ASSERT_NOT_REACHED(); |
| }; |
| |
| if (!hasEligibleContainment()) |
| return EvaluationResult::Unknown; |
| |
| auto compare = [](CQ::ComparisonOperator op, auto left, auto right) { |
| switch (op) { |
| case CQ::ComparisonOperator::LessThan: |
| return left < right; |
| case CQ::ComparisonOperator::GreaterThan: |
| return left > right; |
| case CQ::ComparisonOperator::LessThanOrEqual: |
| return left <= right; |
| case CQ::ComparisonOperator::GreaterThanOrEqual: |
| return left >= right; |
| case CQ::ComparisonOperator::Equal: |
| return left == right; |
| } |
| RELEASE_ASSERT_NOT_REACHED(); |
| }; |
| |
| enum class Side : uint8_t { Left, Right }; |
| auto evaluateSizeComparison = [&](LayoutUnit size, const std::optional<CQ::Comparison>& comparison, Side side) { |
| if (!comparison) |
| return EvaluationResult::True; |
| auto expressionSize = computeSize(comparison->value.get(), container.conversionData); |
| if (!expressionSize) |
| return EvaluationResult::Unknown; |
| auto left = side == Side::Left ? *expressionSize : size; |
| auto right = side == Side::Left ? size : *expressionSize; |
| |
| return toEvaluationResult(compare(comparison->op, left, right)); |
| }; |
| |
| auto evaluateSize = [&](LayoutUnit size) { |
| if (!sizeFeature.leftComparison && !sizeFeature.rightComparison) |
| return toEvaluationResult(!!size); |
| |
| auto leftResult = evaluateSizeComparison(size, sizeFeature.leftComparison, Side::Left); |
| auto rightResult = evaluateSizeComparison(size, sizeFeature.rightComparison, Side::Right); |
| |
| return leftResult & rightResult; |
| }; |
| |
| auto evaluateAspectRatioComparison = [&](double aspectRatio, const std::optional<CQ::Comparison>& comparison, Side side) { |
| if (!comparison) |
| return EvaluationResult::True; |
| |
| if (!is<CSSValueList>(comparison->value)) |
| return EvaluationResult::Unknown; |
| |
| auto& ratioList = downcast<CSSValueList>(*comparison->value); |
| if (ratioList.length() != 2) |
| return EvaluationResult::Unknown; |
| |
| auto first = dynamicDowncast<CSSPrimitiveValue>(ratioList.item(0)); |
| auto second = dynamicDowncast<CSSPrimitiveValue>(ratioList.item(1)); |
| |
| if (!first || !second || !first->isNumberOrInteger() || !second->isNumberOrInteger()) |
| return EvaluationResult::Unknown; |
| |
| auto expressionRatio = first->doubleValue() / second->doubleValue(); |
| |
| auto left = side == Side::Left ? expressionRatio : aspectRatio; |
| auto right = side == Side::Left ? aspectRatio : expressionRatio; |
| |
| return toEvaluationResult(compare(comparison->op, left, right)); |
| }; |
| |
| if (sizeFeature.name == CQ::FeatureNames::width()) |
| return evaluateSize(renderer.contentWidth()); |
| |
| if (sizeFeature.name == CQ::FeatureNames::height()) |
| return evaluateSize(renderer.contentHeight()); |
| |
| if (sizeFeature.name == CQ::FeatureNames::inlineSize()) |
| return evaluateSize(renderer.contentLogicalWidth()); |
| |
| if (sizeFeature.name == CQ::FeatureNames::blockSize()) |
| return evaluateSize(renderer.contentLogicalHeight()); |
| |
| if (sizeFeature.name == CQ::FeatureNames::aspectRatio()) { |
| auto boxRatio = renderer.contentWidth().toDouble() / renderer.contentHeight().toDouble(); |
| |
| if (!sizeFeature.leftComparison && !sizeFeature.rightComparison) |
| return toEvaluationResult(!!boxRatio); |
| |
| auto leftResult = evaluateAspectRatioComparison(boxRatio, sizeFeature.leftComparison, Side::Left); |
| auto rightResult = evaluateAspectRatioComparison(boxRatio, sizeFeature.rightComparison, Side::Right); |
| |
| return leftResult & rightResult; |
| } |
| |
| if (sizeFeature.name == CQ::FeatureNames::orientation()) { |
| if (!sizeFeature.rightComparison) |
| return EvaluationResult::Unknown; |
| |
| auto& comparison = *sizeFeature.rightComparison; |
| |
| if (!is<CSSPrimitiveValue>(comparison.value) || comparison.op != CQ::ComparisonOperator::Equal) |
| return EvaluationResult::Unknown; |
| |
| auto& value = downcast<CSSPrimitiveValue>(*sizeFeature.rightComparison->value); |
| |
| bool isPortrait = renderer.contentHeight() >= renderer.contentWidth(); |
| if (value.valueID() == CSSValuePortrait) |
| return toEvaluationResult(isPortrait); |
| if (value.valueID() == CSSValueLandscape) |
| return toEvaluationResult(!isPortrait); |
| |
| return EvaluationResult::Unknown; |
| } |
| |
| return EvaluationResult::Unknown; |
| } |
| |
| } |