Serenity Operating System
at master 505 lines 18 kB view raw
1/* 2 * Copyright (c) 2021, Sam Atkins <atkinssj@serenityos.org> 3 * 4 * SPDX-License-Identifier: BSD-2-Clause 5 */ 6 7#include <LibWeb/CSS/MediaQuery.h> 8#include <LibWeb/CSS/Serialize.h> 9#include <LibWeb/DOM/Document.h> 10#include <LibWeb/HTML/Window.h> 11 12namespace Web::CSS { 13 14NonnullRefPtr<MediaQuery> MediaQuery::create_not_all() 15{ 16 auto media_query = new MediaQuery; 17 media_query->m_negated = true; 18 media_query->m_media_type = MediaType::All; 19 20 return adopt_ref(*media_query); 21} 22 23ErrorOr<String> MediaFeatureValue::to_string() const 24{ 25 return m_value.visit( 26 [](ValueID const& ident) { return String::from_utf8(string_from_value_id(ident)); }, 27 [](Length const& length) { return length.to_string(); }, 28 [](Ratio const& ratio) { return ratio.to_string(); }, 29 [](Resolution const& resolution) { return resolution.to_string(); }, 30 [](float number) { return String::number(number); }); 31} 32 33bool MediaFeatureValue::is_same_type(MediaFeatureValue const& other) const 34{ 35 return m_value.visit( 36 [&](ValueID const&) { return other.is_ident(); }, 37 [&](Length const&) { return other.is_length(); }, 38 [&](Ratio const&) { return other.is_ratio(); }, 39 [&](Resolution const&) { return other.is_resolution(); }, 40 [&](float) { return other.is_number(); }); 41} 42 43ErrorOr<String> MediaFeature::to_string() const 44{ 45 auto comparison_string = [](Comparison comparison) -> StringView { 46 switch (comparison) { 47 case Comparison::Equal: 48 return "="sv; 49 case Comparison::LessThan: 50 return "<"sv; 51 case Comparison::LessThanOrEqual: 52 return "<="sv; 53 case Comparison::GreaterThan: 54 return ">"sv; 55 case Comparison::GreaterThanOrEqual: 56 return ">="sv; 57 } 58 VERIFY_NOT_REACHED(); 59 }; 60 61 switch (m_type) { 62 case Type::IsTrue: 63 return String::from_utf8(string_from_media_feature_id(m_id)); 64 case Type::ExactValue: 65 return String::formatted("{}:{}", string_from_media_feature_id(m_id), TRY(m_value->to_string())); 66 case Type::MinValue: 67 return String::formatted("min-{}:{}", string_from_media_feature_id(m_id), TRY(m_value->to_string())); 68 case Type::MaxValue: 69 return String::formatted("max-{}:{}", string_from_media_feature_id(m_id), TRY(m_value->to_string())); 70 case Type::Range: 71 if (!m_range->right_comparison.has_value()) 72 return String::formatted("{} {} {}", TRY(m_range->left_value.to_string()), comparison_string(m_range->left_comparison), string_from_media_feature_id(m_id)); 73 74 return String::formatted("{} {} {} {} {}", TRY(m_range->left_value.to_string()), comparison_string(m_range->left_comparison), string_from_media_feature_id(m_id), comparison_string(*m_range->right_comparison), TRY(m_range->right_value->to_string())); 75 } 76 77 VERIFY_NOT_REACHED(); 78} 79 80bool MediaFeature::evaluate(HTML::Window const& window) const 81{ 82 auto maybe_queried_value = window.query_media_feature(m_id); 83 if (!maybe_queried_value.has_value()) 84 return false; 85 auto queried_value = maybe_queried_value.release_value(); 86 87 switch (m_type) { 88 case Type::IsTrue: 89 if (queried_value.is_number()) 90 return queried_value.number() != 0; 91 if (queried_value.is_length()) 92 return queried_value.length().raw_value() != 0; 93 // FIXME: I couldn't figure out from the spec how ratios should be evaluated in a boolean context. 94 if (queried_value.is_ratio()) 95 return !queried_value.ratio().is_degenerate(); 96 if (queried_value.is_resolution()) 97 return queried_value.resolution().to_dots_per_pixel() != 0; 98 if (queried_value.is_ident()) { 99 // NOTE: It is not technically correct to always treat `no-preference` as false, but every 100 // media-feature that accepts it as a value treats it as false, so good enough. :^) 101 // If other features gain this property for other identifiers in the future, we can 102 // add more robust handling for them then. 103 return queried_value.ident() != ValueID::None 104 && queried_value.ident() != ValueID::NoPreference; 105 } 106 return false; 107 108 case Type::ExactValue: 109 return compare(window, *m_value, Comparison::Equal, queried_value); 110 111 case Type::MinValue: 112 return compare(window, queried_value, Comparison::GreaterThanOrEqual, *m_value); 113 114 case Type::MaxValue: 115 return compare(window, queried_value, Comparison::LessThanOrEqual, *m_value); 116 117 case Type::Range: 118 if (!compare(window, m_range->left_value, m_range->left_comparison, queried_value)) 119 return false; 120 121 if (m_range->right_comparison.has_value()) 122 if (!compare(window, queried_value, *m_range->right_comparison, *m_range->right_value)) 123 return false; 124 125 return true; 126 } 127 128 VERIFY_NOT_REACHED(); 129} 130 131bool MediaFeature::compare(HTML::Window const& window, MediaFeatureValue left, Comparison comparison, MediaFeatureValue right) 132{ 133 if (!left.is_same_type(right)) 134 return false; 135 136 if (left.is_ident()) { 137 if (comparison == Comparison::Equal) 138 return left.ident() == right.ident(); 139 return false; 140 } 141 142 if (left.is_number()) { 143 switch (comparison) { 144 case Comparison::Equal: 145 return left.number() == right.number(); 146 case Comparison::LessThan: 147 return left.number() < right.number(); 148 case Comparison::LessThanOrEqual: 149 return left.number() <= right.number(); 150 case Comparison::GreaterThan: 151 return left.number() > right.number(); 152 case Comparison::GreaterThanOrEqual: 153 return left.number() >= right.number(); 154 } 155 VERIFY_NOT_REACHED(); 156 } 157 158 if (left.is_length()) { 159 CSSPixels left_px; 160 CSSPixels right_px; 161 // Save ourselves some work if neither side is a relative length. 162 if (left.length().is_absolute() && right.length().is_absolute()) { 163 left_px = left.length().absolute_length_to_px(); 164 right_px = right.length().absolute_length_to_px(); 165 } else { 166 auto viewport_rect = window.page()->web_exposed_screen_area(); 167 168 auto const& initial_font = window.associated_document().style_computer().initial_font(); 169 Gfx::FontPixelMetrics const& initial_font_metrics = initial_font.pixel_metrics(); 170 float initial_font_size = initial_font.presentation_size(); 171 172 left_px = left.length().to_px(viewport_rect, initial_font_metrics, initial_font_size, initial_font_size); 173 right_px = right.length().to_px(viewport_rect, initial_font_metrics, initial_font_size, initial_font_size); 174 } 175 176 switch (comparison) { 177 case Comparison::Equal: 178 return left_px == right_px; 179 case Comparison::LessThan: 180 return left_px < right_px; 181 case Comparison::LessThanOrEqual: 182 return left_px <= right_px; 183 case Comparison::GreaterThan: 184 return left_px > right_px; 185 case Comparison::GreaterThanOrEqual: 186 return left_px >= right_px; 187 } 188 189 VERIFY_NOT_REACHED(); 190 } 191 192 if (left.is_ratio()) { 193 auto left_decimal = left.ratio().value(); 194 auto right_decimal = right.ratio().value(); 195 196 switch (comparison) { 197 case Comparison::Equal: 198 return left_decimal == right_decimal; 199 case Comparison::LessThan: 200 return left_decimal < right_decimal; 201 case Comparison::LessThanOrEqual: 202 return left_decimal <= right_decimal; 203 case Comparison::GreaterThan: 204 return left_decimal > right_decimal; 205 case Comparison::GreaterThanOrEqual: 206 return left_decimal >= right_decimal; 207 } 208 VERIFY_NOT_REACHED(); 209 } 210 211 if (left.is_resolution()) { 212 auto left_dppx = left.resolution().to_dots_per_pixel(); 213 auto right_dppx = right.resolution().to_dots_per_pixel(); 214 215 switch (comparison) { 216 case Comparison::Equal: 217 return left_dppx == right_dppx; 218 case Comparison::LessThan: 219 return left_dppx < right_dppx; 220 case Comparison::LessThanOrEqual: 221 return left_dppx <= right_dppx; 222 case Comparison::GreaterThan: 223 return left_dppx > right_dppx; 224 case Comparison::GreaterThanOrEqual: 225 return left_dppx >= right_dppx; 226 } 227 VERIFY_NOT_REACHED(); 228 } 229 230 VERIFY_NOT_REACHED(); 231} 232 233NonnullOwnPtr<MediaCondition> MediaCondition::from_general_enclosed(GeneralEnclosed&& general_enclosed) 234{ 235 auto result = new MediaCondition; 236 result->type = Type::GeneralEnclosed; 237 result->general_enclosed = move(general_enclosed); 238 239 return adopt_own(*result); 240} 241 242NonnullOwnPtr<MediaCondition> MediaCondition::from_feature(MediaFeature&& feature) 243{ 244 auto result = new MediaCondition; 245 result->type = Type::Single; 246 result->feature = move(feature); 247 248 return adopt_own(*result); 249} 250 251NonnullOwnPtr<MediaCondition> MediaCondition::from_not(NonnullOwnPtr<MediaCondition>&& condition) 252{ 253 auto result = new MediaCondition; 254 result->type = Type::Not; 255 result->conditions.append(move(condition)); 256 257 return adopt_own(*result); 258} 259 260NonnullOwnPtr<MediaCondition> MediaCondition::from_and_list(Vector<NonnullOwnPtr<MediaCondition>>&& conditions) 261{ 262 auto result = new MediaCondition; 263 result->type = Type::And; 264 result->conditions = move(conditions); 265 266 return adopt_own(*result); 267} 268 269NonnullOwnPtr<MediaCondition> MediaCondition::from_or_list(Vector<NonnullOwnPtr<MediaCondition>>&& conditions) 270{ 271 auto result = new MediaCondition; 272 result->type = Type::Or; 273 result->conditions = move(conditions); 274 275 return adopt_own(*result); 276} 277 278ErrorOr<String> MediaCondition::to_string() const 279{ 280 StringBuilder builder; 281 builder.append('('); 282 switch (type) { 283 case Type::Single: 284 builder.append(TRY(feature->to_string())); 285 break; 286 case Type::Not: 287 builder.append("not "sv); 288 builder.append(TRY(conditions.first()->to_string())); 289 break; 290 case Type::And: 291 builder.join(" and "sv, conditions); 292 break; 293 case Type::Or: 294 builder.join(" or "sv, conditions); 295 break; 296 case Type::GeneralEnclosed: 297 builder.append(general_enclosed->to_string()); 298 break; 299 } 300 builder.append(')'); 301 return builder.to_string(); 302} 303 304MatchResult MediaCondition::evaluate(HTML::Window const& window) const 305{ 306 switch (type) { 307 case Type::Single: 308 return as_match_result(feature->evaluate(window)); 309 case Type::Not: 310 return negate(conditions.first()->evaluate(window)); 311 case Type::And: 312 return evaluate_and(conditions, [&](auto& child) { return child->evaluate(window); }); 313 case Type::Or: 314 return evaluate_or(conditions, [&](auto& child) { return child->evaluate(window); }); 315 case Type::GeneralEnclosed: 316 return general_enclosed->evaluate(); 317 } 318 VERIFY_NOT_REACHED(); 319} 320 321ErrorOr<String> MediaQuery::to_string() const 322{ 323 StringBuilder builder; 324 325 if (m_negated) 326 builder.append("not "sv); 327 328 if (m_negated || m_media_type != MediaType::All || !m_media_condition) { 329 builder.append(CSS::to_string(m_media_type)); 330 if (m_media_condition) 331 builder.append(" and "sv); 332 } 333 334 if (m_media_condition) { 335 builder.append(TRY(m_media_condition->to_string())); 336 } 337 338 return builder.to_string(); 339} 340 341bool MediaQuery::evaluate(HTML::Window const& window) 342{ 343 auto matches_media = [](MediaType media) -> MatchResult { 344 switch (media) { 345 case MediaType::All: 346 return MatchResult::True; 347 case MediaType::Print: 348 // FIXME: Enable for printing, when we have printing! 349 return MatchResult::False; 350 case MediaType::Screen: 351 // FIXME: Disable for printing, when we have printing! 352 return MatchResult::True; 353 case MediaType::Unknown: 354 return MatchResult::False; 355 // Deprecated, must never match: 356 case MediaType::TTY: 357 case MediaType::TV: 358 case MediaType::Projection: 359 case MediaType::Handheld: 360 case MediaType::Braille: 361 case MediaType::Embossed: 362 case MediaType::Aural: 363 case MediaType::Speech: 364 return MatchResult::False; 365 } 366 VERIFY_NOT_REACHED(); 367 }; 368 369 MatchResult result = matches_media(m_media_type); 370 371 if ((result == MatchResult::True) && m_media_condition) 372 result = m_media_condition->evaluate(window); 373 374 if (m_negated) 375 result = negate(result); 376 377 m_matches = result == MatchResult::True; 378 return m_matches; 379} 380 381// https://www.w3.org/TR/cssom-1/#serialize-a-media-query-list 382ErrorOr<String> serialize_a_media_query_list(Vector<NonnullRefPtr<MediaQuery>> const& media_queries) 383{ 384 // 1. If the media query list is empty, then return the empty string. 385 if (media_queries.is_empty()) 386 return String {}; 387 388 // 2. Serialize each media query in the list of media queries, in the same order as they 389 // appear in the media query list, and then serialize the list. 390 return String::join(", "sv, media_queries); 391} 392 393bool is_media_feature_name(StringView name) 394{ 395 // MEDIAQUERIES-4 - https://www.w3.org/TR/mediaqueries-4/#media-descriptor-table 396 if (name.equals_ignoring_ascii_case("any-hover"sv)) 397 return true; 398 if (name.equals_ignoring_ascii_case("any-pointer"sv)) 399 return true; 400 if (name.equals_ignoring_ascii_case("aspect-ratio"sv)) 401 return true; 402 if (name.equals_ignoring_ascii_case("color"sv)) 403 return true; 404 if (name.equals_ignoring_ascii_case("color-gamut"sv)) 405 return true; 406 if (name.equals_ignoring_ascii_case("color-index"sv)) 407 return true; 408 if (name.equals_ignoring_ascii_case("device-aspect-ratio"sv)) 409 return true; 410 if (name.equals_ignoring_ascii_case("device-height"sv)) 411 return true; 412 if (name.equals_ignoring_ascii_case("device-width"sv)) 413 return true; 414 if (name.equals_ignoring_ascii_case("grid"sv)) 415 return true; 416 if (name.equals_ignoring_ascii_case("height"sv)) 417 return true; 418 if (name.equals_ignoring_ascii_case("hover"sv)) 419 return true; 420 if (name.equals_ignoring_ascii_case("monochrome"sv)) 421 return true; 422 if (name.equals_ignoring_ascii_case("orientation"sv)) 423 return true; 424 if (name.equals_ignoring_ascii_case("overflow-block"sv)) 425 return true; 426 if (name.equals_ignoring_ascii_case("overflow-inline"sv)) 427 return true; 428 if (name.equals_ignoring_ascii_case("pointer"sv)) 429 return true; 430 if (name.equals_ignoring_ascii_case("resolution"sv)) 431 return true; 432 if (name.equals_ignoring_ascii_case("scan"sv)) 433 return true; 434 if (name.equals_ignoring_ascii_case("update"sv)) 435 return true; 436 if (name.equals_ignoring_ascii_case("width"sv)) 437 return true; 438 439 // MEDIAQUERIES-5 - https://www.w3.org/TR/mediaqueries-5/#media-descriptor-table 440 if (name.equals_ignoring_ascii_case("prefers-color-scheme"sv)) 441 return true; 442 // FIXME: Add other level 5 feature names 443 444 return false; 445} 446 447MediaQuery::MediaType media_type_from_string(StringView name) 448{ 449 if (name.equals_ignoring_ascii_case("all"sv)) 450 return MediaQuery::MediaType::All; 451 if (name.equals_ignoring_ascii_case("aural"sv)) 452 return MediaQuery::MediaType::Aural; 453 if (name.equals_ignoring_ascii_case("braille"sv)) 454 return MediaQuery::MediaType::Braille; 455 if (name.equals_ignoring_ascii_case("embossed"sv)) 456 return MediaQuery::MediaType::Embossed; 457 if (name.equals_ignoring_ascii_case("handheld"sv)) 458 return MediaQuery::MediaType::Handheld; 459 if (name.equals_ignoring_ascii_case("print"sv)) 460 return MediaQuery::MediaType::Print; 461 if (name.equals_ignoring_ascii_case("projection"sv)) 462 return MediaQuery::MediaType::Projection; 463 if (name.equals_ignoring_ascii_case("screen"sv)) 464 return MediaQuery::MediaType::Screen; 465 if (name.equals_ignoring_ascii_case("speech"sv)) 466 return MediaQuery::MediaType::Speech; 467 if (name.equals_ignoring_ascii_case("tty"sv)) 468 return MediaQuery::MediaType::TTY; 469 if (name.equals_ignoring_ascii_case("tv"sv)) 470 return MediaQuery::MediaType::TV; 471 return MediaQuery::MediaType::Unknown; 472} 473 474StringView to_string(MediaQuery::MediaType media_type) 475{ 476 switch (media_type) { 477 case MediaQuery::MediaType::All: 478 return "all"sv; 479 case MediaQuery::MediaType::Aural: 480 return "aural"sv; 481 case MediaQuery::MediaType::Braille: 482 return "braille"sv; 483 case MediaQuery::MediaType::Embossed: 484 return "embossed"sv; 485 case MediaQuery::MediaType::Handheld: 486 return "handheld"sv; 487 case MediaQuery::MediaType::Print: 488 return "print"sv; 489 case MediaQuery::MediaType::Projection: 490 return "projection"sv; 491 case MediaQuery::MediaType::Screen: 492 return "screen"sv; 493 case MediaQuery::MediaType::Speech: 494 return "speech"sv; 495 case MediaQuery::MediaType::TTY: 496 return "tty"sv; 497 case MediaQuery::MediaType::TV: 498 return "tv"sv; 499 case MediaQuery::MediaType::Unknown: 500 return "unknown"sv; 501 } 502 VERIFY_NOT_REACHED(); 503} 504 505}