blob: 9c6de688756476e6d114835d0b7302a8f7918592 [file] [log] [blame]
/*
* 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;
}
}