MediaQuery.cpp 16 KB

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