From 9e3c08db40b8916968b9f30096c7be3f00ce9647 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 21 Apr 2024 13:44:51 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- servo/components/style/Cargo.toml | 90 + servo/components/style/README.md | 6 + servo/components/style/animation.rs | 1411 +++++++ servo/components/style/applicable_declarations.rs | 210 + servo/components/style/attr.rs | 599 +++ servo/components/style/author_styles.rs | 70 + servo/components/style/bezier.rs | 176 + servo/components/style/bloom.rs | 400 ++ servo/components/style/build.rs | 91 + servo/components/style/build_gecko.rs | 400 ++ servo/components/style/color/convert.rs | 888 ++++ servo/components/style/color/mix.rs | 475 +++ servo/components/style/color/mod.rs | 465 +++ servo/components/style/context.rs | 698 ++++ servo/components/style/counter_style/mod.rs | 697 ++++ servo/components/style/counter_style/predefined.rs | 61 + .../style/counter_style/update_predefined.py | 35 + servo/components/style/custom_properties.rs | 1185 ++++++ servo/components/style/data.rs | 545 +++ servo/components/style/dom.rs | 947 +++++ servo/components/style/dom_apis.rs | 723 ++++ servo/components/style/driver.rs | 195 + servo/components/style/encoding_support.rs | 105 + servo/components/style/error_reporting.rs | 283 ++ servo/components/style/font_face.rs | 818 ++++ servo/components/style/font_metrics.rs | 58 + servo/components/style/gecko/arc_types.rs | 171 + servo/components/style/gecko/conversions.rs | 59 + servo/components/style/gecko/data.rs | 198 + servo/components/style/gecko/media_features.rs | 1028 +++++ servo/components/style/gecko/media_queries.rs | 567 +++ servo/components/style/gecko/mod.rs | 23 + .../style/gecko/non_ts_pseudo_class_list.rs | 100 + servo/components/style/gecko/pseudo_element.rs | 237 ++ .../style/gecko/pseudo_element_definition.mako.rs | 276 ++ servo/components/style/gecko/regen_atoms.py | 218 + servo/components/style/gecko/restyle_damage.rs | 121 + servo/components/style/gecko/selector_parser.rs | 498 +++ servo/components/style/gecko/snapshot.rs | 238 ++ servo/components/style/gecko/snapshot_helpers.rs | 196 + servo/components/style/gecko/traversal.rs | 53 + servo/components/style/gecko/url.rs | 383 ++ servo/components/style/gecko/values.rs | 72 + servo/components/style/gecko/wrapper.rs | 2199 ++++++++++ servo/components/style/gecko_bindings/mod.rs | 28 + servo/components/style/gecko_bindings/sugar/mod.rs | 13 + .../style/gecko_bindings/sugar/ns_com_ptr.rs | 15 + .../style/gecko_bindings/sugar/ns_compatibility.rs | 19 + .../gecko_bindings/sugar/ns_style_auto_array.rs | 111 + .../style/gecko_bindings/sugar/ns_t_array.rs | 144 + .../style/gecko_bindings/sugar/origin_flags.rs | 31 + .../style/gecko_bindings/sugar/ownership.rs | 61 + .../style/gecko_bindings/sugar/refptr.rs | 289 ++ servo/components/style/gecko_string_cache/mod.rs | 532 +++ .../style/gecko_string_cache/namespace.rs | 105 + servo/components/style/global_style_data.rs | 171 + .../style/invalidation/element/document_state.rs | 142 + .../style/invalidation/element/element_wrapper.rs | 391 ++ .../style/invalidation/element/invalidation_map.rs | 545 +++ .../style/invalidation/element/invalidator.rs | 1017 +++++ servo/components/style/invalidation/element/mod.rs | 12 + .../style/invalidation/element/restyle_hints.rs | 191 + .../invalidation/element/state_and_attributes.rs | 552 +++ .../components/style/invalidation/media_queries.rs | 130 + servo/components/style/invalidation/mod.rs | 10 + servo/components/style/invalidation/stylesheets.rs | 656 +++ .../style/invalidation/viewport_units.rs | 71 + servo/components/style/lib.rs | 330 ++ servo/components/style/logical_geometry.rs | 1472 +++++++ servo/components/style/macros.rs | 98 + servo/components/style/matching.rs | 1091 +++++ servo/components/style/media_queries/media_list.rs | 150 + .../components/style/media_queries/media_query.rs | 193 + servo/components/style/media_queries/mod.rs | 18 + servo/components/style/parallel.rs | 287 ++ servo/components/style/parser.rs | 210 + servo/components/style/piecewise_linear.rs | 293 ++ .../properties/Mako-1.1.2-py2.py3-none-any.whl | Bin 0 -> 75521 bytes servo/components/style/properties/build.py | 176 + servo/components/style/properties/cascade.rs | 1253 ++++++ .../style/properties/computed_value_flags.rs | 180 + .../style/properties/counted_unknown_properties.py | 120 + servo/components/style/properties/data.py | 936 +++++ .../style/properties/declaration_block.rs | 1613 ++++++++ servo/components/style/properties/gecko.mako.rs | 1971 +++++++++ servo/components/style/properties/helpers.mako.rs | 1031 +++++ .../properties/helpers/animated_properties.mako.rs | 728 ++++ .../style/properties/longhands/background.mako.rs | 116 + .../style/properties/longhands/border.mako.rs | 159 + .../style/properties/longhands/box.mako.rs | 587 +++ .../style/properties/longhands/column.mako.rs | 83 + .../style/properties/longhands/counters.mako.rs | 48 + .../style/properties/longhands/effects.mako.rs | 86 + .../style/properties/longhands/font.mako.rs | 488 +++ .../properties/longhands/inherited_box.mako.rs | 97 + .../properties/longhands/inherited_svg.mako.rs | 217 + .../properties/longhands/inherited_table.mako.rs | 49 + .../properties/longhands/inherited_text.mako.rs | 408 ++ .../properties/longhands/inherited_ui.mako.rs | 118 + .../style/properties/longhands/list.mako.rs | 75 + .../style/properties/longhands/margin.mako.rs | 52 + .../style/properties/longhands/outline.mako.rs | 53 + .../style/properties/longhands/padding.mako.rs | 41 + .../style/properties/longhands/page.mako.rs | 42 + .../style/properties/longhands/position.mako.rs | 448 ++ .../style/properties/longhands/svg.mako.rs | 258 ++ .../style/properties/longhands/table.mako.rs | 28 + .../style/properties/longhands/text.mako.rs | 81 + .../style/properties/longhands/ui.mako.rs | 395 ++ .../style/properties/longhands/xul.mako.rs | 79 + servo/components/style/properties/mod.rs | 27 + .../style/properties/properties.html.mako | 31 + .../components/style/properties/properties.mako.rs | 4273 ++++++++++++++++++++ .../style/properties/shorthands/background.mako.rs | 289 ++ .../style/properties/shorthands/border.mako.rs | 479 +++ .../style/properties/shorthands/box.mako.rs | 311 ++ .../style/properties/shorthands/column.mako.rs | 115 + .../style/properties/shorthands/font.mako.rs | 542 +++ .../properties/shorthands/inherited_svg.mako.rs | 38 + .../properties/shorthands/inherited_text.mako.rs | 91 + .../style/properties/shorthands/list.mako.rs | 137 + .../style/properties/shorthands/margin.mako.rs | 60 + .../style/properties/shorthands/outline.mako.rs | 80 + .../style/properties/shorthands/padding.mako.rs | 58 + .../style/properties/shorthands/position.mako.rs | 879 ++++ .../style/properties/shorthands/svg.mako.rs | 258 ++ .../style/properties/shorthands/text.mako.rs | 120 + .../style/properties/shorthands/ui.mako.rs | 427 ++ .../components/style/properties_and_values/mod.rs | 10 + .../components/style/properties_and_values/rule.rs | 245 ++ .../style/properties_and_values/syntax/ascii.rs | 60 + .../properties_and_values/syntax/data_type.rs | 91 + .../style/properties_and_values/syntax/mod.rs | 328 ++ servo/components/style/queries/condition.rs | 366 ++ servo/components/style/queries/feature.rs | 195 + .../components/style/queries/feature_expression.rs | 754 ++++ servo/components/style/queries/mod.rs | 19 + servo/components/style/queries/values.rs | 36 + servo/components/style/rule_cache.rs | 187 + servo/components/style/rule_collector.rs | 505 +++ servo/components/style/rule_tree/core.rs | 772 ++++ servo/components/style/rule_tree/level.rs | 249 ++ servo/components/style/rule_tree/map.rs | 201 + servo/components/style/rule_tree/mod.rs | 426 ++ servo/components/style/rule_tree/source.rs | 75 + servo/components/style/rule_tree/unsafe_box.rs | 74 + servo/components/style/scoped_tls.rs | 78 + servo/components/style/selector_map.rs | 868 ++++ servo/components/style/selector_parser.rs | 240 ++ servo/components/style/servo/media_queries.rs | 232 ++ servo/components/style/servo/mod.rs | 12 + servo/components/style/servo/restyle_damage.rs | 268 ++ servo/components/style/servo/selector_parser.rs | 806 ++++ servo/components/style/servo/url.rs | 238 ++ servo/components/style/shared_lock.rs | 374 ++ servo/components/style/sharing/checks.rs | 182 + servo/components/style/sharing/mod.rs | 901 +++++ servo/components/style/str.rs | 181 + servo/components/style/style_adjuster.rs | 979 +++++ servo/components/style/style_resolver.rs | 589 +++ servo/components/style/stylesheet_set.rs | 705 ++++ .../style/stylesheets/cascading_at_rule.rs | 70 + .../components/style/stylesheets/container_rule.rs | 632 +++ .../style/stylesheets/counter_style_rule.rs | 7 + .../components/style/stylesheets/document_rule.rs | 305 ++ .../components/style/stylesheets/font_face_rule.rs | 7 + .../style/stylesheets/font_feature_values_rule.rs | 490 +++ .../style/stylesheets/font_palette_values_rule.rs | 264 ++ servo/components/style/stylesheets/import_rule.rs | 301 ++ .../components/style/stylesheets/keyframes_rule.rs | 691 ++++ servo/components/style/stylesheets/layer_rule.rs | 228 ++ servo/components/style/stylesheets/loader.rs | 31 + servo/components/style/stylesheets/media_rule.rs | 71 + servo/components/style/stylesheets/mod.rs | 565 +++ .../components/style/stylesheets/namespace_rule.rs | 43 + servo/components/style/stylesheets/origin.rs | 247 ++ servo/components/style/stylesheets/page_rule.rs | 145 + .../components/style/stylesheets/property_rule.rs | 5 + servo/components/style/stylesheets/rule_list.rs | 198 + servo/components/style/stylesheets/rule_parser.rs | 879 ++++ .../components/style/stylesheets/rules_iterator.rs | 327 ++ servo/components/style/stylesheets/style_rule.rs | 104 + servo/components/style/stylesheets/stylesheet.rs | 595 +++ .../components/style/stylesheets/supports_rule.rs | 436 ++ .../components/style/stylesheets/viewport_rule.rs | 810 ++++ servo/components/style/stylist.rs | 3347 +++++++++++++++ servo/components/style/thread_state.rs | 98 + servo/components/style/traversal.rs | 841 ++++ servo/components/style/traversal_flags.rs | 68 + servo/components/style/use_counters/mod.rs | 96 + servo/components/style/values/animated/color.rs | 88 + servo/components/style/values/animated/effects.rs | 27 + servo/components/style/values/animated/font.rs | 37 + servo/components/style/values/animated/grid.rs | 157 + servo/components/style/values/animated/lists.rs | 141 + servo/components/style/values/animated/mod.rs | 488 +++ servo/components/style/values/animated/svg.rs | 46 + .../components/style/values/animated/transform.rs | 1473 +++++++ servo/components/style/values/computed/align.rs | 91 + servo/components/style/values/computed/angle.rs | 101 + .../components/style/values/computed/animation.rs | 69 + .../components/style/values/computed/background.rs | 13 + .../style/values/computed/basic_shape.rs | 42 + servo/components/style/values/computed/border.rs | 84 + servo/components/style/values/computed/box.rs | 254 ++ servo/components/style/values/computed/color.rs | 104 + servo/components/style/values/computed/column.rs | 11 + servo/components/style/values/computed/counters.rs | 26 + servo/components/style/values/computed/easing.rs | 109 + servo/components/style/values/computed/effects.rs | 44 + servo/components/style/values/computed/flex.rs | 19 + servo/components/style/values/computed/font.rs | 1239 ++++++ servo/components/style/values/computed/image.rs | 209 + servo/components/style/values/computed/length.rs | 515 +++ .../style/values/computed/length_percentage.rs | 899 ++++ servo/components/style/values/computed/list.rs | 17 + servo/components/style/values/computed/mod.rs | 999 +++++ servo/components/style/values/computed/motion.rs | 60 + servo/components/style/values/computed/outline.rs | 7 + servo/components/style/values/computed/page.rs | 75 + .../components/style/values/computed/percentage.rs | 136 + servo/components/style/values/computed/position.rs | 74 + servo/components/style/values/computed/ratio.rs | 115 + servo/components/style/values/computed/rect.rs | 11 + .../components/style/values/computed/resolution.rs | 56 + servo/components/style/values/computed/svg.rs | 68 + servo/components/style/values/computed/table.rs | 7 + servo/components/style/values/computed/text.rs | 254 ++ servo/components/style/values/computed/time.rs | 45 + .../components/style/values/computed/transform.rs | 558 +++ servo/components/style/values/computed/ui.rs | 22 + servo/components/style/values/computed/url.rs | 15 + servo/components/style/values/distance.rs | 138 + .../components/style/values/generics/animation.rs | 140 + .../components/style/values/generics/background.rs | 54 + .../style/values/generics/basic_shape.rs | 513 +++ servo/components/style/values/generics/border.rs | 257 ++ servo/components/style/values/generics/box.rs | 208 + servo/components/style/values/generics/calc.rs | 1343 ++++++ servo/components/style/values/generics/color.rs | 196 + servo/components/style/values/generics/column.rs | 45 + servo/components/style/values/generics/counters.rs | 295 ++ servo/components/style/values/generics/easing.rs | 137 + servo/components/style/values/generics/effects.rs | 121 + servo/components/style/values/generics/flex.rs | 33 + servo/components/style/values/generics/font.rs | 271 ++ servo/components/style/values/generics/grid.rs | 829 ++++ servo/components/style/values/generics/image.rs | 614 +++ servo/components/style/values/generics/length.rs | 304 ++ servo/components/style/values/generics/mod.rs | 386 ++ servo/components/style/values/generics/motion.rs | 174 + servo/components/style/values/generics/page.rs | 162 + servo/components/style/values/generics/position.rs | 231 ++ servo/components/style/values/generics/ratio.rs | 50 + servo/components/style/values/generics/rect.rs | 126 + servo/components/style/values/generics/size.rs | 99 + servo/components/style/values/generics/svg.rs | 221 + servo/components/style/values/generics/text.rs | 156 + .../components/style/values/generics/transform.rs | 879 ++++ servo/components/style/values/generics/ui.rs | 129 + servo/components/style/values/generics/url.rs | 47 + servo/components/style/values/mod.rs | 762 ++++ servo/components/style/values/resolved/color.rs | 48 + servo/components/style/values/resolved/counters.rs | 51 + servo/components/style/values/resolved/mod.rs | 274 ++ servo/components/style/values/specified/align.rs | 817 ++++ servo/components/style/values/specified/angle.rs | 276 ++ .../components/style/values/specified/animation.rs | 420 ++ .../style/values/specified/background.rs | 143 + .../style/values/specified/basic_shape.rs | 321 ++ servo/components/style/values/specified/border.rs | 368 ++ servo/components/style/values/specified/box.rs | 1905 +++++++++ servo/components/style/values/specified/calc.rs | 1043 +++++ servo/components/style/values/specified/color.rs | 1181 ++++++ servo/components/style/values/specified/column.rs | 11 + .../components/style/values/specified/counters.rs | 286 ++ servo/components/style/values/specified/easing.rs | 252 ++ servo/components/style/values/specified/effects.rs | 362 ++ servo/components/style/values/specified/flex.rs | 25 + servo/components/style/values/specified/font.rs | 2100 ++++++++++ servo/components/style/values/specified/gecko.rs | 82 + servo/components/style/values/specified/grid.rs | 349 ++ servo/components/style/values/specified/image.rs | 1296 ++++++ servo/components/style/values/specified/length.rs | 1849 +++++++++ servo/components/style/values/specified/list.rs | 202 + servo/components/style/values/specified/mod.rs | 954 +++++ servo/components/style/values/specified/motion.rs | 222 + servo/components/style/values/specified/outline.rs | 71 + servo/components/style/values/specified/page.rs | 99 + .../style/values/specified/percentage.rs | 225 ++ .../components/style/values/specified/position.rs | 905 +++++ servo/components/style/values/specified/ratio.rs | 32 + servo/components/style/values/specified/rect.rs | 11 + .../style/values/specified/resolution.rs | 141 + .../style/values/specified/source_size_list.rs | 136 + servo/components/style/values/specified/svg.rs | 391 ++ .../components/style/values/specified/svg_path.rs | 1029 +++++ servo/components/style/values/specified/table.rs | 36 + servo/components/style/values/specified/text.rs | 1129 ++++++ servo/components/style/values/specified/time.rs | 174 + .../components/style/values/specified/transform.rs | 487 +++ servo/components/style/values/specified/ui.rs | 232 ++ servo/components/style/values/specified/url.rs | 15 + 303 files changed, 108699 insertions(+) create mode 100644 servo/components/style/Cargo.toml create mode 100644 servo/components/style/README.md create mode 100644 servo/components/style/animation.rs create mode 100644 servo/components/style/applicable_declarations.rs create mode 100644 servo/components/style/attr.rs create mode 100644 servo/components/style/author_styles.rs create mode 100644 servo/components/style/bezier.rs create mode 100644 servo/components/style/bloom.rs create mode 100644 servo/components/style/build.rs create mode 100644 servo/components/style/build_gecko.rs create mode 100644 servo/components/style/color/convert.rs create mode 100644 servo/components/style/color/mix.rs create mode 100644 servo/components/style/color/mod.rs create mode 100644 servo/components/style/context.rs create mode 100644 servo/components/style/counter_style/mod.rs create mode 100644 servo/components/style/counter_style/predefined.rs create mode 100755 servo/components/style/counter_style/update_predefined.py create mode 100644 servo/components/style/custom_properties.rs create mode 100644 servo/components/style/data.rs create mode 100644 servo/components/style/dom.rs create mode 100644 servo/components/style/dom_apis.rs create mode 100644 servo/components/style/driver.rs create mode 100644 servo/components/style/encoding_support.rs create mode 100644 servo/components/style/error_reporting.rs create mode 100644 servo/components/style/font_face.rs create mode 100644 servo/components/style/font_metrics.rs create mode 100644 servo/components/style/gecko/arc_types.rs create mode 100644 servo/components/style/gecko/conversions.rs create mode 100644 servo/components/style/gecko/data.rs create mode 100644 servo/components/style/gecko/media_features.rs create mode 100644 servo/components/style/gecko/media_queries.rs create mode 100644 servo/components/style/gecko/mod.rs create mode 100644 servo/components/style/gecko/non_ts_pseudo_class_list.rs create mode 100644 servo/components/style/gecko/pseudo_element.rs create mode 100644 servo/components/style/gecko/pseudo_element_definition.mako.rs create mode 100755 servo/components/style/gecko/regen_atoms.py create mode 100644 servo/components/style/gecko/restyle_damage.rs create mode 100644 servo/components/style/gecko/selector_parser.rs create mode 100644 servo/components/style/gecko/snapshot.rs create mode 100644 servo/components/style/gecko/snapshot_helpers.rs create mode 100644 servo/components/style/gecko/traversal.rs create mode 100644 servo/components/style/gecko/url.rs create mode 100644 servo/components/style/gecko/values.rs create mode 100644 servo/components/style/gecko/wrapper.rs create mode 100644 servo/components/style/gecko_bindings/mod.rs create mode 100644 servo/components/style/gecko_bindings/sugar/mod.rs create mode 100644 servo/components/style/gecko_bindings/sugar/ns_com_ptr.rs create mode 100644 servo/components/style/gecko_bindings/sugar/ns_compatibility.rs create mode 100644 servo/components/style/gecko_bindings/sugar/ns_style_auto_array.rs create mode 100644 servo/components/style/gecko_bindings/sugar/ns_t_array.rs create mode 100644 servo/components/style/gecko_bindings/sugar/origin_flags.rs create mode 100644 servo/components/style/gecko_bindings/sugar/ownership.rs create mode 100644 servo/components/style/gecko_bindings/sugar/refptr.rs create mode 100644 servo/components/style/gecko_string_cache/mod.rs create mode 100644 servo/components/style/gecko_string_cache/namespace.rs create mode 100644 servo/components/style/global_style_data.rs create mode 100644 servo/components/style/invalidation/element/document_state.rs create mode 100644 servo/components/style/invalidation/element/element_wrapper.rs create mode 100644 servo/components/style/invalidation/element/invalidation_map.rs create mode 100644 servo/components/style/invalidation/element/invalidator.rs create mode 100644 servo/components/style/invalidation/element/mod.rs create mode 100644 servo/components/style/invalidation/element/restyle_hints.rs create mode 100644 servo/components/style/invalidation/element/state_and_attributes.rs create mode 100644 servo/components/style/invalidation/media_queries.rs create mode 100644 servo/components/style/invalidation/mod.rs create mode 100644 servo/components/style/invalidation/stylesheets.rs create mode 100644 servo/components/style/invalidation/viewport_units.rs create mode 100644 servo/components/style/lib.rs create mode 100644 servo/components/style/logical_geometry.rs create mode 100644 servo/components/style/macros.rs create mode 100644 servo/components/style/matching.rs create mode 100644 servo/components/style/media_queries/media_list.rs create mode 100644 servo/components/style/media_queries/media_query.rs create mode 100644 servo/components/style/media_queries/mod.rs create mode 100644 servo/components/style/parallel.rs create mode 100644 servo/components/style/parser.rs create mode 100644 servo/components/style/piecewise_linear.rs create mode 100644 servo/components/style/properties/Mako-1.1.2-py2.py3-none-any.whl create mode 100644 servo/components/style/properties/build.py create mode 100644 servo/components/style/properties/cascade.rs create mode 100644 servo/components/style/properties/computed_value_flags.rs create mode 100644 servo/components/style/properties/counted_unknown_properties.py create mode 100644 servo/components/style/properties/data.py create mode 100644 servo/components/style/properties/declaration_block.rs create mode 100644 servo/components/style/properties/gecko.mako.rs create mode 100644 servo/components/style/properties/helpers.mako.rs create mode 100644 servo/components/style/properties/helpers/animated_properties.mako.rs create mode 100644 servo/components/style/properties/longhands/background.mako.rs create mode 100644 servo/components/style/properties/longhands/border.mako.rs create mode 100644 servo/components/style/properties/longhands/box.mako.rs create mode 100644 servo/components/style/properties/longhands/column.mako.rs create mode 100644 servo/components/style/properties/longhands/counters.mako.rs create mode 100644 servo/components/style/properties/longhands/effects.mako.rs create mode 100644 servo/components/style/properties/longhands/font.mako.rs create mode 100644 servo/components/style/properties/longhands/inherited_box.mako.rs create mode 100644 servo/components/style/properties/longhands/inherited_svg.mako.rs create mode 100644 servo/components/style/properties/longhands/inherited_table.mako.rs create mode 100644 servo/components/style/properties/longhands/inherited_text.mako.rs create mode 100644 servo/components/style/properties/longhands/inherited_ui.mako.rs create mode 100644 servo/components/style/properties/longhands/list.mako.rs create mode 100644 servo/components/style/properties/longhands/margin.mako.rs create mode 100644 servo/components/style/properties/longhands/outline.mako.rs create mode 100644 servo/components/style/properties/longhands/padding.mako.rs create mode 100644 servo/components/style/properties/longhands/page.mako.rs create mode 100644 servo/components/style/properties/longhands/position.mako.rs create mode 100644 servo/components/style/properties/longhands/svg.mako.rs create mode 100644 servo/components/style/properties/longhands/table.mako.rs create mode 100644 servo/components/style/properties/longhands/text.mako.rs create mode 100644 servo/components/style/properties/longhands/ui.mako.rs create mode 100644 servo/components/style/properties/longhands/xul.mako.rs create mode 100644 servo/components/style/properties/mod.rs create mode 100644 servo/components/style/properties/properties.html.mako create mode 100644 servo/components/style/properties/properties.mako.rs create mode 100644 servo/components/style/properties/shorthands/background.mako.rs create mode 100644 servo/components/style/properties/shorthands/border.mako.rs create mode 100644 servo/components/style/properties/shorthands/box.mako.rs create mode 100644 servo/components/style/properties/shorthands/column.mako.rs create mode 100644 servo/components/style/properties/shorthands/font.mako.rs create mode 100644 servo/components/style/properties/shorthands/inherited_svg.mako.rs create mode 100644 servo/components/style/properties/shorthands/inherited_text.mako.rs create mode 100644 servo/components/style/properties/shorthands/list.mako.rs create mode 100644 servo/components/style/properties/shorthands/margin.mako.rs create mode 100644 servo/components/style/properties/shorthands/outline.mako.rs create mode 100644 servo/components/style/properties/shorthands/padding.mako.rs create mode 100644 servo/components/style/properties/shorthands/position.mako.rs create mode 100644 servo/components/style/properties/shorthands/svg.mako.rs create mode 100644 servo/components/style/properties/shorthands/text.mako.rs create mode 100644 servo/components/style/properties/shorthands/ui.mako.rs create mode 100644 servo/components/style/properties_and_values/mod.rs create mode 100644 servo/components/style/properties_and_values/rule.rs create mode 100644 servo/components/style/properties_and_values/syntax/ascii.rs create mode 100644 servo/components/style/properties_and_values/syntax/data_type.rs create mode 100644 servo/components/style/properties_and_values/syntax/mod.rs create mode 100644 servo/components/style/queries/condition.rs create mode 100644 servo/components/style/queries/feature.rs create mode 100644 servo/components/style/queries/feature_expression.rs create mode 100644 servo/components/style/queries/mod.rs create mode 100644 servo/components/style/queries/values.rs create mode 100644 servo/components/style/rule_cache.rs create mode 100644 servo/components/style/rule_collector.rs create mode 100644 servo/components/style/rule_tree/core.rs create mode 100644 servo/components/style/rule_tree/level.rs create mode 100644 servo/components/style/rule_tree/map.rs create mode 100644 servo/components/style/rule_tree/mod.rs create mode 100644 servo/components/style/rule_tree/source.rs create mode 100644 servo/components/style/rule_tree/unsafe_box.rs create mode 100644 servo/components/style/scoped_tls.rs create mode 100644 servo/components/style/selector_map.rs create mode 100644 servo/components/style/selector_parser.rs create mode 100644 servo/components/style/servo/media_queries.rs create mode 100644 servo/components/style/servo/mod.rs create mode 100644 servo/components/style/servo/restyle_damage.rs create mode 100644 servo/components/style/servo/selector_parser.rs create mode 100644 servo/components/style/servo/url.rs create mode 100644 servo/components/style/shared_lock.rs create mode 100644 servo/components/style/sharing/checks.rs create mode 100644 servo/components/style/sharing/mod.rs create mode 100644 servo/components/style/str.rs create mode 100644 servo/components/style/style_adjuster.rs create mode 100644 servo/components/style/style_resolver.rs create mode 100644 servo/components/style/stylesheet_set.rs create mode 100644 servo/components/style/stylesheets/cascading_at_rule.rs create mode 100644 servo/components/style/stylesheets/container_rule.rs create mode 100644 servo/components/style/stylesheets/counter_style_rule.rs create mode 100644 servo/components/style/stylesheets/document_rule.rs create mode 100644 servo/components/style/stylesheets/font_face_rule.rs create mode 100644 servo/components/style/stylesheets/font_feature_values_rule.rs create mode 100644 servo/components/style/stylesheets/font_palette_values_rule.rs create mode 100644 servo/components/style/stylesheets/import_rule.rs create mode 100644 servo/components/style/stylesheets/keyframes_rule.rs create mode 100644 servo/components/style/stylesheets/layer_rule.rs create mode 100644 servo/components/style/stylesheets/loader.rs create mode 100644 servo/components/style/stylesheets/media_rule.rs create mode 100644 servo/components/style/stylesheets/mod.rs create mode 100644 servo/components/style/stylesheets/namespace_rule.rs create mode 100644 servo/components/style/stylesheets/origin.rs create mode 100644 servo/components/style/stylesheets/page_rule.rs create mode 100644 servo/components/style/stylesheets/property_rule.rs create mode 100644 servo/components/style/stylesheets/rule_list.rs create mode 100644 servo/components/style/stylesheets/rule_parser.rs create mode 100644 servo/components/style/stylesheets/rules_iterator.rs create mode 100644 servo/components/style/stylesheets/style_rule.rs create mode 100644 servo/components/style/stylesheets/stylesheet.rs create mode 100644 servo/components/style/stylesheets/supports_rule.rs create mode 100644 servo/components/style/stylesheets/viewport_rule.rs create mode 100644 servo/components/style/stylist.rs create mode 100644 servo/components/style/thread_state.rs create mode 100644 servo/components/style/traversal.rs create mode 100644 servo/components/style/traversal_flags.rs create mode 100644 servo/components/style/use_counters/mod.rs create mode 100644 servo/components/style/values/animated/color.rs create mode 100644 servo/components/style/values/animated/effects.rs create mode 100644 servo/components/style/values/animated/font.rs create mode 100644 servo/components/style/values/animated/grid.rs create mode 100644 servo/components/style/values/animated/lists.rs create mode 100644 servo/components/style/values/animated/mod.rs create mode 100644 servo/components/style/values/animated/svg.rs create mode 100644 servo/components/style/values/animated/transform.rs create mode 100644 servo/components/style/values/computed/align.rs create mode 100644 servo/components/style/values/computed/angle.rs create mode 100644 servo/components/style/values/computed/animation.rs create mode 100644 servo/components/style/values/computed/background.rs create mode 100644 servo/components/style/values/computed/basic_shape.rs create mode 100644 servo/components/style/values/computed/border.rs create mode 100644 servo/components/style/values/computed/box.rs create mode 100644 servo/components/style/values/computed/color.rs create mode 100644 servo/components/style/values/computed/column.rs create mode 100644 servo/components/style/values/computed/counters.rs create mode 100644 servo/components/style/values/computed/easing.rs create mode 100644 servo/components/style/values/computed/effects.rs create mode 100644 servo/components/style/values/computed/flex.rs create mode 100644 servo/components/style/values/computed/font.rs create mode 100644 servo/components/style/values/computed/image.rs create mode 100644 servo/components/style/values/computed/length.rs create mode 100644 servo/components/style/values/computed/length_percentage.rs create mode 100644 servo/components/style/values/computed/list.rs create mode 100644 servo/components/style/values/computed/mod.rs create mode 100644 servo/components/style/values/computed/motion.rs create mode 100644 servo/components/style/values/computed/outline.rs create mode 100644 servo/components/style/values/computed/page.rs create mode 100644 servo/components/style/values/computed/percentage.rs create mode 100644 servo/components/style/values/computed/position.rs create mode 100644 servo/components/style/values/computed/ratio.rs create mode 100644 servo/components/style/values/computed/rect.rs create mode 100644 servo/components/style/values/computed/resolution.rs create mode 100644 servo/components/style/values/computed/svg.rs create mode 100644 servo/components/style/values/computed/table.rs create mode 100644 servo/components/style/values/computed/text.rs create mode 100644 servo/components/style/values/computed/time.rs create mode 100644 servo/components/style/values/computed/transform.rs create mode 100644 servo/components/style/values/computed/ui.rs create mode 100644 servo/components/style/values/computed/url.rs create mode 100644 servo/components/style/values/distance.rs create mode 100644 servo/components/style/values/generics/animation.rs create mode 100644 servo/components/style/values/generics/background.rs create mode 100644 servo/components/style/values/generics/basic_shape.rs create mode 100644 servo/components/style/values/generics/border.rs create mode 100644 servo/components/style/values/generics/box.rs create mode 100644 servo/components/style/values/generics/calc.rs create mode 100644 servo/components/style/values/generics/color.rs create mode 100644 servo/components/style/values/generics/column.rs create mode 100644 servo/components/style/values/generics/counters.rs create mode 100644 servo/components/style/values/generics/easing.rs create mode 100644 servo/components/style/values/generics/effects.rs create mode 100644 servo/components/style/values/generics/flex.rs create mode 100644 servo/components/style/values/generics/font.rs create mode 100644 servo/components/style/values/generics/grid.rs create mode 100644 servo/components/style/values/generics/image.rs create mode 100644 servo/components/style/values/generics/length.rs create mode 100644 servo/components/style/values/generics/mod.rs create mode 100644 servo/components/style/values/generics/motion.rs create mode 100644 servo/components/style/values/generics/page.rs create mode 100644 servo/components/style/values/generics/position.rs create mode 100644 servo/components/style/values/generics/ratio.rs create mode 100644 servo/components/style/values/generics/rect.rs create mode 100644 servo/components/style/values/generics/size.rs create mode 100644 servo/components/style/values/generics/svg.rs create mode 100644 servo/components/style/values/generics/text.rs create mode 100644 servo/components/style/values/generics/transform.rs create mode 100644 servo/components/style/values/generics/ui.rs create mode 100644 servo/components/style/values/generics/url.rs create mode 100644 servo/components/style/values/mod.rs create mode 100644 servo/components/style/values/resolved/color.rs create mode 100644 servo/components/style/values/resolved/counters.rs create mode 100644 servo/components/style/values/resolved/mod.rs create mode 100644 servo/components/style/values/specified/align.rs create mode 100644 servo/components/style/values/specified/angle.rs create mode 100644 servo/components/style/values/specified/animation.rs create mode 100644 servo/components/style/values/specified/background.rs create mode 100644 servo/components/style/values/specified/basic_shape.rs create mode 100644 servo/components/style/values/specified/border.rs create mode 100644 servo/components/style/values/specified/box.rs create mode 100644 servo/components/style/values/specified/calc.rs create mode 100644 servo/components/style/values/specified/color.rs create mode 100644 servo/components/style/values/specified/column.rs create mode 100644 servo/components/style/values/specified/counters.rs create mode 100644 servo/components/style/values/specified/easing.rs create mode 100644 servo/components/style/values/specified/effects.rs create mode 100644 servo/components/style/values/specified/flex.rs create mode 100644 servo/components/style/values/specified/font.rs create mode 100644 servo/components/style/values/specified/gecko.rs create mode 100644 servo/components/style/values/specified/grid.rs create mode 100644 servo/components/style/values/specified/image.rs create mode 100644 servo/components/style/values/specified/length.rs create mode 100644 servo/components/style/values/specified/list.rs create mode 100644 servo/components/style/values/specified/mod.rs create mode 100644 servo/components/style/values/specified/motion.rs create mode 100644 servo/components/style/values/specified/outline.rs create mode 100644 servo/components/style/values/specified/page.rs create mode 100644 servo/components/style/values/specified/percentage.rs create mode 100644 servo/components/style/values/specified/position.rs create mode 100644 servo/components/style/values/specified/ratio.rs create mode 100644 servo/components/style/values/specified/rect.rs create mode 100644 servo/components/style/values/specified/resolution.rs create mode 100644 servo/components/style/values/specified/source_size_list.rs create mode 100644 servo/components/style/values/specified/svg.rs create mode 100644 servo/components/style/values/specified/svg_path.rs create mode 100644 servo/components/style/values/specified/table.rs create mode 100644 servo/components/style/values/specified/text.rs create mode 100644 servo/components/style/values/specified/time.rs create mode 100644 servo/components/style/values/specified/transform.rs create mode 100644 servo/components/style/values/specified/ui.rs create mode 100644 servo/components/style/values/specified/url.rs (limited to 'servo/components/style') diff --git a/servo/components/style/Cargo.toml b/servo/components/style/Cargo.toml new file mode 100644 index 0000000000..30ad42f6c4 --- /dev/null +++ b/servo/components/style/Cargo.toml @@ -0,0 +1,90 @@ +[package] +name = "style" +version = "0.0.1" +authors = ["The Servo Project Developers"] +license = "MPL-2.0" +publish = false + +build = "build.rs" +edition = "2018" + +# https://github.com/rust-lang/cargo/issues/3544 +links = "servo_style_crate" + +[lib] +name = "style" +path = "lib.rs" +doctest = false + +[features] +gecko = ["nsstring", "serde", "style_traits/gecko", "bindgen", "regex", "toml", "mozbuild"] +servo = ["serde", "style_traits/servo", "servo_atoms", "servo_config", "html5ever", + "cssparser/serde", "encoding_rs", "malloc_size_of/servo", "arrayvec/use_union", + "servo_url", "string_cache", "to_shmem/servo", "servo_arc/servo"] +servo-layout-2013 = [] +servo-layout-2020 = [] +gecko_debug = [] +gecko_refcount_logging = [] + +[dependencies] +app_units = "0.7" +arrayvec = "0.7" +atomic_refcell = "0.1" +bitflags = "2" +byteorder = "1.0" +cssparser = "0.31" +derive_more = { version = "0.99", default-features = false, features = ["add", "add_assign", "deref", "from"] } +dom = { path = "../../../dom/base/rust" } +new_debug_unreachable = "1.0" +encoding_rs = {version = "0.8", optional = true} +euclid = "0.22" +fxhash = "0.2" +html5ever = {version = "0.24", optional = true} +indexmap = {version = "1.0", features = ["std"]} +itertools = "0.10" +itoa = "1.0" +lazy_static = "1" +log = "0.4" +malloc_size_of = { path = "../malloc_size_of" } +malloc_size_of_derive = "0.1" +matches = "0.1" +nsstring = {path = "../../../xpcom/rust/nsstring/", optional = true} +num_cpus = {version = "1.1.0"} +num-integer = "0.1" +num-traits = "0.2" +num-derive = "0.3" +owning_ref = "0.4" +parking_lot = "0.11" +precomputed-hash = "0.1.1" +rayon = "1" +selectors = { path = "../selectors" } +serde = {version = "1.0", optional = true, features = ["derive"]} +servo_arc = { path = "../servo_arc" } +servo_atoms = {path = "../atoms", optional = true} +servo_config = {path = "../config", optional = true} +smallbitvec = "2.3.0" +smallvec = "1.0" +static_assertions = "1.1" +static_prefs = { path = "../../../modules/libpref/init/static_prefs" } +string_cache = { version = "0.8", optional = true } +style_derive = {path = "../style_derive"} +style_traits = {path = "../style_traits"} +servo_url = {path = "../url", optional = true} +to_shmem = {path = "../to_shmem"} +to_shmem_derive = {path = "../to_shmem_derive"} +time = "0.1" +thin-vec = { version = "0.2.1", features = ["gecko-ffi"] } +uluru = "3.0" +unicode-bidi = "0.3" +unicode-segmentation = "1.0" +void = "1.0.2" +gecko-profiler = { path = "../../../tools/profiler/rust-api" } + +[build-dependencies] +lazy_static = "1" +log = { version = "0.4", features = ["std"] } +bindgen = {version = "0.64", optional = true, default-features = false} +regex = {version = "1.0", optional = true, default-features = false, features = ["perf", "std"]} +walkdir = "2.1.4" +toml = {version = "0.5", optional = true, default-features = false} +mozbuild = {version = "0.1", optional = true} diff --git a/servo/components/style/README.md b/servo/components/style/README.md new file mode 100644 index 0000000000..96457e1b30 --- /dev/null +++ b/servo/components/style/README.md @@ -0,0 +1,6 @@ +servo-style +=========== + +Style system for Servo, using [rust-cssparser](https://github.com/servo/rust-cssparser) for parsing. + + * [Documentation](https://github.com/servo/servo/blob/master/docs/components/style.md). diff --git a/servo/components/style/animation.rs b/servo/components/style/animation.rs new file mode 100644 index 0000000000..a0d3bfce58 --- /dev/null +++ b/servo/components/style/animation.rs @@ -0,0 +1,1411 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! CSS transitions and animations. + +// NOTE(emilio): This code isn't really executed in Gecko, but we don't want to +// compile it out so that people remember it exists. + +use crate::context::{CascadeInputs, SharedStyleContext}; +use crate::dom::{OpaqueNode, TDocument, TElement, TNode}; +use crate::properties::animated_properties::{AnimationValue, AnimationValueMap}; +use crate::properties::longhands::animation_direction::computed_value::single_value::T as AnimationDirection; +use crate::properties::longhands::animation_fill_mode::computed_value::single_value::T as AnimationFillMode; +use crate::properties::longhands::animation_play_state::computed_value::single_value::T as AnimationPlayState; +use crate::properties::AnimationDeclarations; +use crate::properties::{ + ComputedValues, Importance, LonghandId, LonghandIdSet, PropertyDeclarationBlock, + PropertyDeclarationId, +}; +use crate::rule_tree::CascadeLevel; +use crate::selector_parser::PseudoElement; +use crate::shared_lock::{Locked, SharedRwLock}; +use crate::style_resolver::StyleResolverForElement; +use crate::stylesheets::keyframes_rule::{KeyframesAnimation, KeyframesStep, KeyframesStepValue}; +use crate::stylesheets::layer_rule::LayerOrder; +use crate::values::animated::{Animate, Procedure}; +use crate::values::computed::{Time, TimingFunction}; +use crate::values::generics::easing::BeforeFlag; +use crate::Atom; +use fxhash::FxHashMap; +use parking_lot::RwLock; +use servo_arc::Arc; +use std::fmt; + +/// Represents an animation for a given property. +#[derive(Clone, Debug, MallocSizeOf)] +pub struct PropertyAnimation { + /// The value we are animating from. + from: AnimationValue, + + /// The value we are animating to. + to: AnimationValue, + + /// The timing function of this `PropertyAnimation`. + timing_function: TimingFunction, + + /// The duration of this `PropertyAnimation` in seconds. + pub duration: f64, +} + +impl PropertyAnimation { + /// Returns the given property longhand id. + pub fn property_id(&self) -> LonghandId { + debug_assert_eq!(self.from.id(), self.to.id()); + self.from.id() + } + + fn from_longhand( + longhand: LonghandId, + timing_function: TimingFunction, + duration: Time, + old_style: &ComputedValues, + new_style: &ComputedValues, + ) -> Option { + // FIXME(emilio): Handle the case where old_style and new_style's writing mode differ. + let longhand = longhand.to_physical(new_style.writing_mode); + let from = AnimationValue::from_computed_values(longhand, old_style)?; + let to = AnimationValue::from_computed_values(longhand, new_style)?; + let duration = duration.seconds() as f64; + + if from == to || duration == 0.0 { + return None; + } + + Some(PropertyAnimation { + from, + to, + timing_function, + duration, + }) + } + + /// The output of the timing function given the progress ration of this animation. + fn timing_function_output(&self, progress: f64) -> f64 { + let epsilon = 1. / (200. * self.duration); + // FIXME: Need to set the before flag correctly. + // In order to get the before flag, we have to know the current animation phase + // and whether the iteration is reversed. For now, we skip this calculation + // by treating as if the flag is unset at all times. + // https://drafts.csswg.org/css-easing/#step-timing-function-algo + self.timing_function + .calculate_output(progress, BeforeFlag::Unset, epsilon) + } + + /// Update the given animation at a given point of progress. + fn calculate_value(&self, progress: f64) -> Result { + let procedure = Procedure::Interpolate { + progress: self.timing_function_output(progress), + }; + self.from.animate(&self.to, procedure) + } +} + +/// This structure represents the state of an animation. +#[derive(Clone, Debug, MallocSizeOf, PartialEq)] +pub enum AnimationState { + /// The animation has been created, but is not running yet. This state + /// is also used when an animation is still in the first delay phase. + Pending, + /// This animation is currently running. + Running, + /// This animation is paused. The inner field is the percentage of progress + /// when it was paused, from 0 to 1. + Paused(f64), + /// This animation has finished. + Finished, + /// This animation has been canceled. + Canceled, +} + +impl AnimationState { + /// Whether or not this state requires its owning animation to be ticked. + fn needs_to_be_ticked(&self) -> bool { + *self == AnimationState::Running || *self == AnimationState::Pending + } +} + +/// This structure represents a keyframes animation current iteration state. +/// +/// If the iteration count is infinite, there's no other state, otherwise we +/// have to keep track the current iteration and the max iteration count. +#[derive(Clone, Debug, MallocSizeOf)] +pub enum KeyframesIterationState { + /// Infinite iterations with the current iteration count. + Infinite(f64), + /// Current and max iterations. + Finite(f64, f64), +} + +/// A temporary data structure used when calculating ComputedKeyframes for an +/// animation. This data structure is used to collapse information for steps +/// which may be spread across multiple keyframe declarations into a single +/// instance per `start_percentage`. +struct IntermediateComputedKeyframe { + declarations: PropertyDeclarationBlock, + timing_function: Option, + start_percentage: f32, +} + +impl IntermediateComputedKeyframe { + fn new(start_percentage: f32) -> Self { + IntermediateComputedKeyframe { + declarations: PropertyDeclarationBlock::new(), + timing_function: None, + start_percentage, + } + } + + /// Walk through all keyframe declarations and combine all declarations with the + /// same `start_percentage` into individual `IntermediateComputedKeyframe`s. + fn generate_for_keyframes( + animation: &KeyframesAnimation, + context: &SharedStyleContext, + base_style: &ComputedValues, + ) -> Vec { + let mut intermediate_steps: Vec = Vec::with_capacity(animation.steps.len()); + let mut current_step = IntermediateComputedKeyframe::new(0.); + for step in animation.steps.iter() { + let start_percentage = step.start_percentage.0; + if start_percentage != current_step.start_percentage { + let new_step = IntermediateComputedKeyframe::new(start_percentage); + intermediate_steps.push(std::mem::replace(&mut current_step, new_step)); + } + + current_step.update_from_step(step, context, base_style); + } + intermediate_steps.push(current_step); + + // We should always have a first and a last step, even if these are just + // generated by KeyframesStepValue::ComputedValues. + debug_assert!(intermediate_steps.first().unwrap().start_percentage == 0.); + debug_assert!(intermediate_steps.last().unwrap().start_percentage == 1.); + + intermediate_steps + } + + fn update_from_step( + &mut self, + step: &KeyframesStep, + context: &SharedStyleContext, + base_style: &ComputedValues, + ) { + // Each keyframe declaration may optionally specify a timing function, falling + // back to the one defined global for the animation. + let guard = &context.guards.author; + if let Some(timing_function) = step.get_animation_timing_function(&guard) { + self.timing_function = Some(timing_function.to_computed_value_without_context()); + } + + let block = match step.value { + KeyframesStepValue::ComputedValues => return, + KeyframesStepValue::Declarations { ref block } => block, + }; + + // Filter out !important, non-animatable properties, and the + // 'display' property (which is only animatable from SMIL). + let guard = block.read_with(&guard); + for declaration in guard.normal_declaration_iter() { + if let PropertyDeclarationId::Longhand(id) = declaration.id() { + if id == LonghandId::Display { + continue; + } + + if !id.is_animatable() { + continue; + } + } + + self.declarations.push( + declaration.to_physical(base_style.writing_mode), + Importance::Normal, + ); + } + } + + fn resolve_style( + self, + element: E, + context: &SharedStyleContext, + base_style: &Arc, + resolver: &mut StyleResolverForElement, + ) -> Arc + where + E: TElement, + { + if !self.declarations.any_normal() { + return base_style.clone(); + } + + let document = element.as_node().owner_doc(); + let locked_block = Arc::new(document.shared_lock().wrap(self.declarations)); + let mut important_rules_changed = false; + let rule_node = base_style.rules().clone(); + let new_node = context.stylist.rule_tree().update_rule_at_level( + CascadeLevel::Animations, + LayerOrder::root(), + Some(locked_block.borrow_arc()), + &rule_node, + &context.guards, + &mut important_rules_changed, + ); + + if new_node.is_none() { + return base_style.clone(); + } + + let inputs = CascadeInputs { + rules: new_node, + visited_rules: base_style.visited_rules().cloned(), + flags: base_style.flags.for_cascade_inputs(), + }; + resolver + .cascade_style_and_visited_with_default_parents(inputs) + .0 + } +} + +/// A single computed keyframe for a CSS Animation. +#[derive(Clone, MallocSizeOf)] +struct ComputedKeyframe { + /// The timing function to use for transitions between this step + /// and the next one. + timing_function: TimingFunction, + + /// The starting percentage (a number between 0 and 1) which represents + /// at what point in an animation iteration this step is. + start_percentage: f32, + + /// The animation values to transition to and from when processing this + /// keyframe animation step. + values: Vec, +} + +impl ComputedKeyframe { + fn generate_for_keyframes( + element: E, + animation: &KeyframesAnimation, + context: &SharedStyleContext, + base_style: &Arc, + default_timing_function: TimingFunction, + resolver: &mut StyleResolverForElement, + ) -> Vec + where + E: TElement, + { + let mut animating_properties = LonghandIdSet::new(); + for property in animation.properties_changed.iter() { + debug_assert!(property.is_animatable()); + animating_properties.insert(property.to_physical(base_style.writing_mode)); + } + + let animation_values_from_style: Vec = animating_properties + .iter() + .map(|property| { + AnimationValue::from_computed_values(property, &**base_style) + .expect("Unexpected non-animatable property.") + }) + .collect(); + + let intermediate_steps = + IntermediateComputedKeyframe::generate_for_keyframes(animation, context, base_style); + + let mut computed_steps: Vec = Vec::with_capacity(intermediate_steps.len()); + for (step_index, step) in intermediate_steps.into_iter().enumerate() { + let start_percentage = step.start_percentage; + let properties_changed_in_step = step.declarations.longhands().clone(); + let step_timing_function = step.timing_function.clone(); + let step_style = step.resolve_style(element, context, base_style, resolver); + let timing_function = + step_timing_function.unwrap_or_else(|| default_timing_function.clone()); + + let values = { + // If a value is not set in a property declaration we use the value from + // the style for the first and last keyframe. For intermediate ones, we + // use the value from the previous keyframe. + // + // TODO(mrobinson): According to the spec, we should use an interpolated + // value for properties missing from keyframe declarations. + let default_values = if start_percentage == 0. || start_percentage == 1.0 { + &animation_values_from_style + } else { + debug_assert!(step_index != 0); + &computed_steps[step_index - 1].values + }; + + // For each property that is animating, pull the value from the resolved + // style for this step if it's in one of the declarations. Otherwise, we + // use the default value from the set we calculated above. + animating_properties + .iter() + .zip(default_values.iter()) + .map(|(longhand, default_value)| { + if properties_changed_in_step.contains(longhand) { + AnimationValue::from_computed_values(longhand, &step_style) + .unwrap_or_else(|| default_value.clone()) + } else { + default_value.clone() + } + }) + .collect() + }; + + computed_steps.push(ComputedKeyframe { + timing_function, + start_percentage, + values, + }); + } + computed_steps + } +} + +/// A CSS Animation +#[derive(Clone, MallocSizeOf)] +pub struct Animation { + /// The name of this animation as defined by the style. + pub name: Atom, + + /// The properties that change in this animation. + properties_changed: LonghandIdSet, + + /// The computed style for each keyframe of this animation. + computed_steps: Vec, + + /// The time this animation started at, which is the current value of the animation + /// timeline when this animation was created plus any animation delay. + pub started_at: f64, + + /// The duration of this animation. + pub duration: f64, + + /// The delay of the animation. + pub delay: f64, + + /// The `animation-fill-mode` property of this animation. + pub fill_mode: AnimationFillMode, + + /// The current iteration state for the animation. + pub iteration_state: KeyframesIterationState, + + /// Whether this animation is paused. + pub state: AnimationState, + + /// The declared animation direction of this animation. + pub direction: AnimationDirection, + + /// The current animation direction. This can only be `normal` or `reverse`. + pub current_direction: AnimationDirection, + + /// The original cascade style, needed to compute the generated keyframes of + /// the animation. + #[ignore_malloc_size_of = "ComputedValues"] + pub cascade_style: Arc, + + /// Whether or not this animation is new and or has already been tracked + /// by the script thread. + pub is_new: bool, +} + +impl Animation { + /// Whether or not this animation is cancelled by changes from a new style. + fn is_cancelled_in_new_style(&self, new_style: &Arc) -> bool { + let new_ui = new_style.get_ui(); + let index = new_ui + .animation_name_iter() + .position(|animation_name| Some(&self.name) == animation_name.as_atom()); + let index = match index { + Some(index) => index, + None => return true, + }; + + new_ui.animation_duration_mod(index).seconds() == 0. + } + + /// Given the current time, advances this animation to the next iteration, + /// updates times, and then toggles the direction if appropriate. Otherwise + /// does nothing. Returns true if this animation has iterated. + pub fn iterate_if_necessary(&mut self, time: f64) -> bool { + if !self.iteration_over(time) { + return false; + } + + // Only iterate animations that are currently running. + if self.state != AnimationState::Running { + return false; + } + + if self.on_last_iteration() { + return false; + } + + self.iterate(); + true + } + + fn iterate(&mut self) { + debug_assert!(!self.on_last_iteration()); + + if let KeyframesIterationState::Finite(ref mut current, max) = self.iteration_state { + *current = (*current + 1.).min(max); + } + + if let AnimationState::Paused(ref mut progress) = self.state { + debug_assert!(*progress > 1.); + *progress -= 1.; + } + + // Update the next iteration direction if applicable. + self.started_at += self.duration; + match self.direction { + AnimationDirection::Alternate | AnimationDirection::AlternateReverse => { + self.current_direction = match self.current_direction { + AnimationDirection::Normal => AnimationDirection::Reverse, + AnimationDirection::Reverse => AnimationDirection::Normal, + _ => unreachable!(), + }; + }, + _ => {}, + } + } + + /// A number (> 0 and <= 1) which represents the fraction of a full iteration + /// that the current iteration of the animation lasts. This will be less than 1 + /// if the current iteration is the fractional remainder of a non-integral + /// iteration count. + pub fn current_iteration_end_progress(&self) -> f64 { + match self.iteration_state { + KeyframesIterationState::Finite(current, max) => (max - current).min(1.), + KeyframesIterationState::Infinite(_) => 1., + } + } + + /// The duration of the current iteration of this animation which may be less + /// than the animation duration if it has a non-integral iteration count. + pub fn current_iteration_duration(&self) -> f64 { + self.current_iteration_end_progress() * self.duration + } + + /// Whether or not the current iteration is over. Note that this method assumes that + /// the animation is still running. + fn iteration_over(&self, time: f64) -> bool { + time > (self.started_at + self.current_iteration_duration()) + } + + /// Assuming this animation is running, whether or not it is on the last iteration. + fn on_last_iteration(&self) -> bool { + match self.iteration_state { + KeyframesIterationState::Finite(current, max) => current >= (max - 1.), + KeyframesIterationState::Infinite(_) => false, + } + } + + /// Whether or not this animation has finished at the provided time. This does + /// not take into account canceling i.e. when an animation or transition is + /// canceled due to changes in the style. + pub fn has_ended(&self, time: f64) -> bool { + if !self.on_last_iteration() { + return false; + } + + let progress = match self.state { + AnimationState::Finished => return true, + AnimationState::Paused(progress) => progress, + AnimationState::Running => (time - self.started_at) / self.duration, + AnimationState::Pending | AnimationState::Canceled => return false, + }; + + progress >= self.current_iteration_end_progress() + } + + /// Updates the appropiate state from other animation. + /// + /// This happens when an animation is re-submitted to layout, presumably + /// because of an state change. + /// + /// There are some bits of state we can't just replace, over all taking in + /// account times, so here's that logic. + pub fn update_from_other(&mut self, other: &Self, now: f64) { + use self::AnimationState::*; + + debug!( + "KeyframesAnimationState::update_from_other({:?}, {:?})", + self, other + ); + + // NB: We shall not touch the started_at field, since we don't want to + // restart the animation. + let old_started_at = self.started_at; + let old_duration = self.duration; + let old_direction = self.current_direction; + let old_state = self.state.clone(); + let old_iteration_state = self.iteration_state.clone(); + + *self = other.clone(); + + self.started_at = old_started_at; + self.current_direction = old_direction; + + // Don't update the iteration count, just the iteration limit. + // TODO: see how changing the limit affects rendering in other browsers. + // We might need to keep the iteration count even when it's infinite. + match (&mut self.iteration_state, old_iteration_state) { + ( + &mut KeyframesIterationState::Finite(ref mut iters, _), + KeyframesIterationState::Finite(old_iters, _), + ) => *iters = old_iters, + _ => {}, + } + + // Don't pause or restart animations that should remain finished. + // We call mem::replace because `has_ended(...)` looks at `Animation::state`. + let new_state = std::mem::replace(&mut self.state, Running); + if old_state == Finished && self.has_ended(now) { + self.state = Finished; + } else { + self.state = new_state; + } + + // If we're unpausing the animation, fake the start time so we seem to + // restore it. + // + // If the animation keeps paused, keep the old value. + // + // If we're pausing the animation, compute the progress value. + match (&mut self.state, &old_state) { + (&mut Pending, &Paused(progress)) => { + self.started_at = now - (self.duration * progress); + }, + (&mut Paused(ref mut new), &Paused(old)) => *new = old, + (&mut Paused(ref mut progress), &Running) => { + *progress = (now - old_started_at) / old_duration + }, + _ => {}, + } + + // Try to detect when we should skip straight to the running phase to + // avoid sending multiple animationstart events. + if self.state == Pending && self.started_at <= now && old_state != Pending { + self.state = Running; + } + } + + /// Fill in an `AnimationValueMap` with values calculated from this animation at + /// the given time value. + fn get_property_declaration_at_time(&self, now: f64, map: &mut AnimationValueMap) { + debug_assert!(!self.computed_steps.is_empty()); + + let total_progress = match self.state { + AnimationState::Running | AnimationState::Pending | AnimationState::Finished => { + (now - self.started_at) / self.duration + }, + AnimationState::Paused(progress) => progress, + AnimationState::Canceled => return, + }; + + if total_progress < 0. && + self.fill_mode != AnimationFillMode::Backwards && + self.fill_mode != AnimationFillMode::Both + { + return; + } + if self.has_ended(now) && + self.fill_mode != AnimationFillMode::Forwards && + self.fill_mode != AnimationFillMode::Both + { + return; + } + let total_progress = total_progress + .min(self.current_iteration_end_progress()) + .max(0.0); + + // Get the indices of the previous (from) keyframe and the next (to) keyframe. + let next_keyframe_index; + let prev_keyframe_index; + let num_steps = self.computed_steps.len(); + match self.current_direction { + AnimationDirection::Normal => { + next_keyframe_index = self + .computed_steps + .iter() + .position(|step| total_progress as f32 <= step.start_percentage); + prev_keyframe_index = next_keyframe_index + .and_then(|pos| if pos != 0 { Some(pos - 1) } else { None }) + .unwrap_or(0); + }, + AnimationDirection::Reverse => { + next_keyframe_index = self + .computed_steps + .iter() + .rev() + .position(|step| total_progress as f32 <= 1. - step.start_percentage) + .map(|pos| num_steps - pos - 1); + prev_keyframe_index = next_keyframe_index + .and_then(|pos| { + if pos != num_steps - 1 { + Some(pos + 1) + } else { + None + } + }) + .unwrap_or(num_steps - 1) + }, + _ => unreachable!(), + } + + debug!( + "Animation::get_property_declaration_at_time: keyframe from {:?} to {:?}", + prev_keyframe_index, next_keyframe_index + ); + + let prev_keyframe = &self.computed_steps[prev_keyframe_index]; + let next_keyframe = match next_keyframe_index { + Some(index) => &self.computed_steps[index], + None => return, + }; + + // If we only need to take into account one keyframe, then exit early + // in order to avoid doing more work. + let mut add_declarations_to_map = |keyframe: &ComputedKeyframe| { + for value in keyframe.values.iter() { + map.insert(value.id(), value.clone()); + } + }; + if total_progress <= 0.0 { + add_declarations_to_map(&prev_keyframe); + return; + } + if total_progress >= 1.0 { + add_declarations_to_map(&next_keyframe); + return; + } + + let percentage_between_keyframes = + (next_keyframe.start_percentage - prev_keyframe.start_percentage).abs() as f64; + let duration_between_keyframes = percentage_between_keyframes * self.duration; + let direction_aware_prev_keyframe_start_percentage = match self.current_direction { + AnimationDirection::Normal => prev_keyframe.start_percentage as f64, + AnimationDirection::Reverse => 1. - prev_keyframe.start_percentage as f64, + _ => unreachable!(), + }; + let progress_between_keyframes = (total_progress - + direction_aware_prev_keyframe_start_percentage) / + percentage_between_keyframes; + + for (from, to) in prev_keyframe.values.iter().zip(next_keyframe.values.iter()) { + let animation = PropertyAnimation { + from: from.clone(), + to: to.clone(), + timing_function: prev_keyframe.timing_function.clone(), + duration: duration_between_keyframes as f64, + }; + + if let Ok(value) = animation.calculate_value(progress_between_keyframes) { + map.insert(value.id(), value); + } + } + } +} + +impl fmt::Debug for Animation { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("Animation") + .field("name", &self.name) + .field("started_at", &self.started_at) + .field("duration", &self.duration) + .field("delay", &self.delay) + .field("iteration_state", &self.iteration_state) + .field("state", &self.state) + .field("direction", &self.direction) + .field("current_direction", &self.current_direction) + .field("cascade_style", &()) + .finish() + } +} + +/// A CSS Transition +#[derive(Clone, Debug, MallocSizeOf)] +pub struct Transition { + /// The start time of this transition, which is the current value of the animation + /// timeline when this transition was created plus any animation delay. + pub start_time: f64, + + /// The delay used for this transition. + pub delay: f64, + + /// The internal style `PropertyAnimation` for this transition. + pub property_animation: PropertyAnimation, + + /// The state of this transition. + pub state: AnimationState, + + /// Whether or not this transition is new and or has already been tracked + /// by the script thread. + pub is_new: bool, + + /// If this `Transition` has been replaced by a new one this field is + /// used to help produce better reversed transitions. + pub reversing_adjusted_start_value: AnimationValue, + + /// If this `Transition` has been replaced by a new one this field is + /// used to help produce better reversed transitions. + pub reversing_shortening_factor: f64, +} + +impl Transition { + fn update_for_possibly_reversed_transition( + &mut self, + replaced_transition: &Transition, + delay: f64, + now: f64, + ) { + // If we reach here, we need to calculate a reversed transition according to + // https://drafts.csswg.org/css-transitions/#starting + // + // "...if the reversing-adjusted start value of the running transition + // is the same as the value of the property in the after-change style (see + // the section on reversing of transitions for why these case exists), + // implementations must cancel the running transition and start + // a new transition..." + if replaced_transition.reversing_adjusted_start_value != self.property_animation.to { + return; + } + + // "* reversing-adjusted start value is the end value of the running transition" + let replaced_animation = &replaced_transition.property_animation; + self.reversing_adjusted_start_value = replaced_animation.to.clone(); + + // "* reversing shortening factor is the absolute value, clamped to the + // range [0, 1], of the sum of: + // 1. the output of the timing function of the old transition at the + // time of the style change event, times the reversing shortening + // factor of the old transition + // 2. 1 minus the reversing shortening factor of the old transition." + let transition_progress = ((now - replaced_transition.start_time) / + (replaced_transition.property_animation.duration)) + .min(1.0) + .max(0.0); + let timing_function_output = replaced_animation.timing_function_output(transition_progress); + let old_reversing_shortening_factor = replaced_transition.reversing_shortening_factor; + self.reversing_shortening_factor = ((timing_function_output * + old_reversing_shortening_factor) + + (1.0 - old_reversing_shortening_factor)) + .abs() + .min(1.0) + .max(0.0); + + // "* start time is the time of the style change event plus: + // 1. if the matching transition delay is nonnegative, the matching + // transition delay, or. + // 2. if the matching transition delay is negative, the product of the new + // transition’s reversing shortening factor and the matching transition delay," + self.start_time = if delay >= 0. { + now + delay + } else { + now + (self.reversing_shortening_factor * delay) + }; + + // "* end time is the start time plus the product of the matching transition + // duration and the new transition’s reversing shortening factor," + self.property_animation.duration *= self.reversing_shortening_factor; + + // "* start value is the current value of the property in the running transition, + // * end value is the value of the property in the after-change style," + let procedure = Procedure::Interpolate { + progress: timing_function_output, + }; + match replaced_animation + .from + .animate(&replaced_animation.to, procedure) + { + Ok(new_start) => self.property_animation.from = new_start, + Err(..) => {}, + } + } + + /// Whether or not this animation has ended at the provided time. This does + /// not take into account canceling i.e. when an animation or transition is + /// canceled due to changes in the style. + pub fn has_ended(&self, time: f64) -> bool { + time >= self.start_time + (self.property_animation.duration) + } + + /// Update the given animation at a given point of progress. + pub fn calculate_value(&self, time: f64) -> Option { + let progress = (time - self.start_time) / (self.property_animation.duration); + if progress < 0.0 { + return None; + } + + self.property_animation + .calculate_value(progress.min(1.0)) + .ok() + } +} + +/// Holds the animation state for a particular element. +#[derive(Debug, Default, MallocSizeOf)] +pub struct ElementAnimationSet { + /// The animations for this element. + pub animations: Vec, + + /// The transitions for this element. + pub transitions: Vec, + + /// Whether or not this ElementAnimationSet has had animations or transitions + /// which have been added, removed, or had their state changed. + pub dirty: bool, +} + +impl ElementAnimationSet { + /// Cancel all animations in this `ElementAnimationSet`. This is typically called + /// when the element has been removed from the DOM. + pub fn cancel_all_animations(&mut self) { + self.dirty = !self.animations.is_empty(); + for animation in self.animations.iter_mut() { + animation.state = AnimationState::Canceled; + } + self.cancel_active_transitions(); + } + + fn cancel_active_transitions(&mut self) { + for transition in self.transitions.iter_mut() { + if transition.state != AnimationState::Finished { + self.dirty = true; + transition.state = AnimationState::Canceled; + } + } + } + + /// Apply all active animations. + pub fn apply_active_animations( + &self, + context: &SharedStyleContext, + style: &mut Arc, + ) { + let now = context.current_time_for_animations; + let mutable_style = Arc::make_mut(style); + if let Some(map) = self.get_value_map_for_active_animations(now) { + for value in map.values() { + value.set_in_style_for_servo(mutable_style); + } + } + + if let Some(map) = self.get_value_map_for_active_transitions(now) { + for value in map.values() { + value.set_in_style_for_servo(mutable_style); + } + } + } + + /// Clear all canceled animations and transitions from this `ElementAnimationSet`. + pub fn clear_canceled_animations(&mut self) { + self.animations + .retain(|animation| animation.state != AnimationState::Canceled); + self.transitions + .retain(|animation| animation.state != AnimationState::Canceled); + } + + /// Whether this `ElementAnimationSet` is empty, which means it doesn't + /// hold any animations in any state. + pub fn is_empty(&self) -> bool { + self.animations.is_empty() && self.transitions.is_empty() + } + + /// Whether or not this state needs animation ticks for its transitions + /// or animations. + pub fn needs_animation_ticks(&self) -> bool { + self.animations + .iter() + .any(|animation| animation.state.needs_to_be_ticked()) || + self.transitions + .iter() + .any(|transition| transition.state.needs_to_be_ticked()) + } + + /// The number of running animations and transitions for this `ElementAnimationSet`. + pub fn running_animation_and_transition_count(&self) -> usize { + self.animations + .iter() + .filter(|animation| animation.state.needs_to_be_ticked()) + .count() + + self.transitions + .iter() + .filter(|transition| transition.state.needs_to_be_ticked()) + .count() + } + + /// If this `ElementAnimationSet` has any any active animations. + pub fn has_active_animation(&self) -> bool { + self.animations + .iter() + .any(|animation| animation.state != AnimationState::Canceled) + } + + /// If this `ElementAnimationSet` has any any active transitions. + pub fn has_active_transition(&self) -> bool { + self.transitions + .iter() + .any(|transition| transition.state != AnimationState::Canceled) + } + + /// Update our animations given a new style, canceling or starting new animations + /// when appropriate. + pub fn update_animations_for_new_style( + &mut self, + element: E, + context: &SharedStyleContext, + new_style: &Arc, + resolver: &mut StyleResolverForElement, + ) where + E: TElement, + { + for animation in self.animations.iter_mut() { + if animation.is_cancelled_in_new_style(new_style) { + animation.state = AnimationState::Canceled; + } + } + + maybe_start_animations(element, &context, &new_style, self, resolver); + } + + /// Update our transitions given a new style, canceling or starting new animations + /// when appropriate. + pub fn update_transitions_for_new_style( + &mut self, + might_need_transitions_update: bool, + context: &SharedStyleContext, + old_style: Option<&Arc>, + after_change_style: &Arc, + ) { + // If this is the first style, we don't trigger any transitions and we assume + // there were no previously triggered transitions. + let mut before_change_style = match old_style { + Some(old_style) => Arc::clone(old_style), + None => return, + }; + + // If the style of this element is display:none, then cancel all active transitions. + if after_change_style.get_box().clone_display().is_none() { + self.cancel_active_transitions(); + return; + } + + if !might_need_transitions_update { + return; + } + + // We convert old values into `before-change-style` here. + if self.has_active_transition() || self.has_active_animation() { + self.apply_active_animations(context, &mut before_change_style); + } + + let transitioning_properties = start_transitions_if_applicable( + context, + &before_change_style, + after_change_style, + self, + ); + + // Cancel any non-finished transitions that have properties which no longer transition. + for transition in self.transitions.iter_mut() { + if transition.state == AnimationState::Finished { + continue; + } + if transitioning_properties.contains(transition.property_animation.property_id()) { + continue; + } + transition.state = AnimationState::Canceled; + self.dirty = true; + } + } + + fn start_transition_if_applicable( + &mut self, + context: &SharedStyleContext, + longhand_id: LonghandId, + index: usize, + old_style: &ComputedValues, + new_style: &Arc, + ) { + let style = new_style.get_ui(); + let timing_function = style.transition_timing_function_mod(index); + let duration = style.transition_duration_mod(index); + let delay = style.transition_delay_mod(index).seconds() as f64; + let now = context.current_time_for_animations; + + // Only start a new transition if the style actually changes between + // the old style and the new style. + let property_animation = match PropertyAnimation::from_longhand( + longhand_id, + timing_function, + duration, + old_style, + new_style, + ) { + Some(property_animation) => property_animation, + None => return, + }; + + // Per [1], don't trigger a new transition if the end state for that + // transition is the same as that of a transition that's running or + // completed. We don't take into account any canceled animations. + // [1]: https://drafts.csswg.org/css-transitions/#starting + if self + .transitions + .iter() + .filter(|transition| transition.state != AnimationState::Canceled) + .any(|transition| transition.property_animation.to == property_animation.to) + { + return; + } + + // We are going to start a new transition, but we might have to update + // it if we are replacing a reversed transition. + let reversing_adjusted_start_value = property_animation.from.clone(); + let mut new_transition = Transition { + start_time: now + delay, + delay, + property_animation, + state: AnimationState::Pending, + is_new: true, + reversing_adjusted_start_value, + reversing_shortening_factor: 1.0, + }; + + if let Some(old_transition) = self + .transitions + .iter_mut() + .filter(|transition| transition.state == AnimationState::Running) + .find(|transition| transition.property_animation.property_id() == longhand_id) + { + // We always cancel any running transitions for the same property. + old_transition.state = AnimationState::Canceled; + new_transition.update_for_possibly_reversed_transition(old_transition, delay, now); + } + + self.transitions.push(new_transition); + self.dirty = true; + } + + /// Generate a `AnimationValueMap` for this `ElementAnimationSet`'s + /// active transitions at the given time value. + pub fn get_value_map_for_active_transitions(&self, now: f64) -> Option { + if !self.has_active_transition() { + return None; + } + + let mut map = + AnimationValueMap::with_capacity_and_hasher(self.transitions.len(), Default::default()); + for transition in &self.transitions { + if transition.state == AnimationState::Canceled { + continue; + } + let value = match transition.calculate_value(now) { + Some(value) => value, + None => continue, + }; + map.insert(value.id(), value); + } + + Some(map) + } + + /// Generate a `AnimationValueMap` for this `ElementAnimationSet`'s + /// active animations at the given time value. + pub fn get_value_map_for_active_animations(&self, now: f64) -> Option { + if !self.has_active_animation() { + return None; + } + + let mut map = Default::default(); + for animation in &self.animations { + animation.get_property_declaration_at_time(now, &mut map); + } + + Some(map) + } +} + +#[derive(Clone, Debug, Eq, Hash, MallocSizeOf, PartialEq)] +/// A key that is used to identify nodes in the `DocumentAnimationSet`. +pub struct AnimationSetKey { + /// The node for this `AnimationSetKey`. + pub node: OpaqueNode, + /// The pseudo element for this `AnimationSetKey`. If `None` this key will + /// refer to the main content for its node. + pub pseudo_element: Option, +} + +impl AnimationSetKey { + /// Create a new key given a node and optional pseudo element. + pub fn new(node: OpaqueNode, pseudo_element: Option) -> Self { + AnimationSetKey { + node, + pseudo_element, + } + } + + /// Create a new key for the main content of this node. + pub fn new_for_non_pseudo(node: OpaqueNode) -> Self { + AnimationSetKey { + node, + pseudo_element: None, + } + } + + /// Create a new key for given node and pseudo element. + pub fn new_for_pseudo(node: OpaqueNode, pseudo_element: PseudoElement) -> Self { + AnimationSetKey { + node, + pseudo_element: Some(pseudo_element), + } + } +} + +#[derive(Clone, Debug, Default, MallocSizeOf)] +/// A set of animations for a document. +pub struct DocumentAnimationSet { + /// The `ElementAnimationSet`s that this set contains. + #[ignore_malloc_size_of = "Arc is hard"] + pub sets: Arc>>, +} + +impl DocumentAnimationSet { + /// Return whether or not the provided node has active CSS animations. + pub fn has_active_animations(&self, key: &AnimationSetKey) -> bool { + self.sets + .read() + .get(key) + .map_or(false, |set| set.has_active_animation()) + } + + /// Return whether or not the provided node has active CSS transitions. + pub fn has_active_transitions(&self, key: &AnimationSetKey) -> bool { + self.sets + .read() + .get(key) + .map_or(false, |set| set.has_active_transition()) + } + + /// Return a locked PropertyDeclarationBlock with animation values for the given + /// key and time. + pub fn get_animation_declarations( + &self, + key: &AnimationSetKey, + time: f64, + shared_lock: &SharedRwLock, + ) -> Option>> { + self.sets + .read() + .get(key) + .and_then(|set| set.get_value_map_for_active_animations(time)) + .map(|map| { + let block = PropertyDeclarationBlock::from_animation_value_map(&map); + Arc::new(shared_lock.wrap(block)) + }) + } + + /// Return a locked PropertyDeclarationBlock with transition values for the given + /// key and time. + pub fn get_transition_declarations( + &self, + key: &AnimationSetKey, + time: f64, + shared_lock: &SharedRwLock, + ) -> Option>> { + self.sets + .read() + .get(key) + .and_then(|set| set.get_value_map_for_active_transitions(time)) + .map(|map| { + let block = PropertyDeclarationBlock::from_animation_value_map(&map); + Arc::new(shared_lock.wrap(block)) + }) + } + + /// Get all the animation declarations for the given key, returning an empty + /// `AnimationDeclarations` if there are no animations. + pub fn get_all_declarations( + &self, + key: &AnimationSetKey, + time: f64, + shared_lock: &SharedRwLock, + ) -> AnimationDeclarations { + let sets = self.sets.read(); + let set = match sets.get(key) { + Some(set) => set, + None => return Default::default(), + }; + + let animations = set.get_value_map_for_active_animations(time).map(|map| { + let block = PropertyDeclarationBlock::from_animation_value_map(&map); + Arc::new(shared_lock.wrap(block)) + }); + let transitions = set.get_value_map_for_active_transitions(time).map(|map| { + let block = PropertyDeclarationBlock::from_animation_value_map(&map); + Arc::new(shared_lock.wrap(block)) + }); + AnimationDeclarations { + animations, + transitions, + } + } + + /// Cancel all animations for set at the given key. + pub fn cancel_all_animations_for_key(&self, key: &AnimationSetKey) { + if let Some(set) = self.sets.write().get_mut(key) { + set.cancel_all_animations(); + } + } +} + +/// Kick off any new transitions for this node and return all of the properties that are +/// transitioning. This is at the end of calculating style for a single node. +pub fn start_transitions_if_applicable( + context: &SharedStyleContext, + old_style: &ComputedValues, + new_style: &Arc, + animation_state: &mut ElementAnimationSet, +) -> LonghandIdSet { + let mut properties_that_transition = LonghandIdSet::new(); + for transition in new_style.transition_properties() { + let physical_property = transition.longhand_id.to_physical(new_style.writing_mode); + if properties_that_transition.contains(physical_property) { + continue; + } + + properties_that_transition.insert(physical_property); + animation_state.start_transition_if_applicable( + context, + physical_property, + transition.index, + old_style, + new_style, + ); + } + + properties_that_transition +} + +/// Triggers animations for a given node looking at the animation property +/// values. +pub fn maybe_start_animations( + element: E, + context: &SharedStyleContext, + new_style: &Arc, + animation_state: &mut ElementAnimationSet, + resolver: &mut StyleResolverForElement, +) where + E: TElement, +{ + let style = new_style.get_ui(); + for (i, name) in style.animation_name_iter().enumerate() { + let name = match name.as_atom() { + Some(atom) => atom, + None => continue, + }; + + debug!("maybe_start_animations: name={}", name); + let duration = style.animation_duration_mod(i).seconds() as f64; + if duration == 0. { + continue; + } + + let keyframe_animation = match context.stylist.get_animation(name, element) { + Some(animation) => animation, + None => continue, + }; + + debug!("maybe_start_animations: animation {} found", name); + + // If this animation doesn't have any keyframe, we can just continue + // without submitting it to the compositor, since both the first and + // the second keyframes would be synthetised from the computed + // values. + if keyframe_animation.steps.is_empty() { + continue; + } + + // NB: This delay may be negative, meaning that the animation may be created + // in a state where we have advanced one or more iterations or even that the + // animation begins in a finished state. + let delay = style.animation_delay_mod(i).seconds(); + + let iteration_count = style.animation_iteration_count_mod(i); + let iteration_state = if iteration_count.0.is_infinite() { + KeyframesIterationState::Infinite(0.0) + } else { + KeyframesIterationState::Finite(0.0, iteration_count.0 as f64) + }; + + let animation_direction = style.animation_direction_mod(i); + + let initial_direction = match animation_direction { + AnimationDirection::Normal | AnimationDirection::Alternate => { + AnimationDirection::Normal + }, + AnimationDirection::Reverse | AnimationDirection::AlternateReverse => { + AnimationDirection::Reverse + }, + }; + + let now = context.current_time_for_animations; + let started_at = now + delay as f64; + let mut starting_progress = (now - started_at) / duration; + let state = match style.animation_play_state_mod(i) { + AnimationPlayState::Paused => AnimationState::Paused(starting_progress), + AnimationPlayState::Running => AnimationState::Pending, + }; + + let computed_steps = ComputedKeyframe::generate_for_keyframes( + element, + &keyframe_animation, + context, + new_style, + style.animation_timing_function_mod(i), + resolver, + ); + + let mut new_animation = Animation { + name: name.clone(), + properties_changed: keyframe_animation.properties_changed, + computed_steps, + started_at, + duration, + fill_mode: style.animation_fill_mode_mod(i), + delay: delay as f64, + iteration_state, + state, + direction: animation_direction, + current_direction: initial_direction, + cascade_style: new_style.clone(), + is_new: true, + }; + + // If we started with a negative delay, make sure we iterate the animation if + // the delay moves us past the first iteration. + while starting_progress > 1. && !new_animation.on_last_iteration() { + new_animation.iterate(); + starting_progress -= 1.; + } + + animation_state.dirty = true; + + // If the animation was already present in the list for the node, just update its state. + for existing_animation in animation_state.animations.iter_mut() { + if existing_animation.state == AnimationState::Canceled { + continue; + } + + if new_animation.name == existing_animation.name { + existing_animation + .update_from_other(&new_animation, context.current_time_for_animations); + return; + } + } + + animation_state.animations.push(new_animation); + } +} diff --git a/servo/components/style/applicable_declarations.rs b/servo/components/style/applicable_declarations.rs new file mode 100644 index 0000000000..a0dbb60da8 --- /dev/null +++ b/servo/components/style/applicable_declarations.rs @@ -0,0 +1,210 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Applicable declarations management. + +use crate::properties::PropertyDeclarationBlock; +use crate::rule_tree::{CascadeLevel, StyleSource}; +use crate::shared_lock::Locked; +use crate::stylesheets::layer_rule::LayerOrder; +use servo_arc::Arc; +use smallvec::SmallVec; + +/// List of applicable declarations. This is a transient structure that shuttles +/// declarations between selector matching and inserting into the rule tree, and +/// therefore we want to avoid heap-allocation where possible. +/// +/// In measurements on wikipedia, we pretty much never have more than 8 applicable +/// declarations, so we could consider making this 8 entries instead of 16. +/// However, it may depend a lot on workload, and stack space is cheap. +pub type ApplicableDeclarationList = SmallVec<[ApplicableDeclarationBlock; 16]>; + +/// Blink uses 18 bits to store source order, and does not check overflow [1]. +/// That's a limit that could be reached in realistic webpages, so we use +/// 24 bits and enforce defined behavior in the overflow case. +/// +/// Note that right now this restriction could be lifted if wanted (because we +/// no longer stash the cascade level in the remaining bits), but we keep it in +/// place in case we come up with a use-case for them, lacking reports of the +/// current limit being too small. +/// +/// [1] https://cs.chromium.org/chromium/src/third_party/WebKit/Source/core/css/ +/// RuleSet.h?l=128&rcl=90140ab80b84d0f889abc253410f44ed54ae04f3 +const SOURCE_ORDER_BITS: usize = 24; +const SOURCE_ORDER_MAX: u32 = (1 << SOURCE_ORDER_BITS) - 1; +const SOURCE_ORDER_MASK: u32 = SOURCE_ORDER_MAX; + +/// The cascade-level+layer order of this declaration. +#[derive(Clone, Copy, Debug, Eq, Hash, MallocSizeOf, PartialEq)] +pub struct CascadePriority { + cascade_level: CascadeLevel, + layer_order: LayerOrder, +} + +const_assert_eq!( + std::mem::size_of::(), + std::mem::size_of::() +); + +impl PartialOrd for CascadePriority { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for CascadePriority { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.cascade_level.cmp(&other.cascade_level).then_with(|| { + let ordering = self.layer_order.cmp(&other.layer_order); + if ordering == std::cmp::Ordering::Equal { + return ordering; + } + // https://drafts.csswg.org/css-cascade-5/#cascade-layering + // + // Cascade layers (like declarations) are ordered by order + // of appearance. When comparing declarations that belong to + // different layers, then for normal rules the declaration + // whose cascade layer is last wins, and for important rules + // the declaration whose cascade layer is first wins. + // + // But the style attribute layer for some reason is special. + if self.cascade_level.is_important() && + !self.layer_order.is_style_attribute_layer() && + !other.layer_order.is_style_attribute_layer() + { + ordering.reverse() + } else { + ordering + } + }) + } +} + +impl CascadePriority { + /// Construct a new CascadePriority for a given (level, order) pair. + pub fn new(cascade_level: CascadeLevel, layer_order: LayerOrder) -> Self { + Self { + cascade_level, + layer_order, + } + } + + /// Returns the layer order. + #[inline] + pub fn layer_order(&self) -> LayerOrder { + self.layer_order + } + + /// Returns the cascade level. + #[inline] + pub fn cascade_level(&self) -> CascadeLevel { + self.cascade_level + } + + /// Whether this declaration should be allowed if `revert` or `revert-layer` + /// have been specified on a given origin. + /// + /// `self` is the priority at which the `revert` or `revert-layer` keyword + /// have been specified. + pub fn allows_when_reverted(&self, other: &Self, origin_revert: bool) -> bool { + if origin_revert { + other.cascade_level.origin() < self.cascade_level.origin() + } else { + other.unimportant() < self.unimportant() + } + } + + /// Convert this priority from "important" to "non-important", if needed. + pub fn unimportant(&self) -> Self { + Self::new(self.cascade_level().unimportant(), self.layer_order()) + } + + /// Convert this priority from "non-important" to "important", if needed. + pub fn important(&self) -> Self { + Self::new(self.cascade_level().important(), self.layer_order()) + } +} + +/// A property declaration together with its precedence among rules of equal +/// specificity so that we can sort them. +/// +/// This represents the declarations in a given declaration block for a given +/// importance. +#[derive(Clone, Debug, MallocSizeOf, PartialEq)] +pub struct ApplicableDeclarationBlock { + /// The style source, either a style rule, or a property declaration block. + #[ignore_malloc_size_of = "Arc"] + pub source: StyleSource, + /// The bits containing the source order, cascade level, and shadow cascade + /// order. + source_order: u32, + /// The specificity of the selector. + pub specificity: u32, + /// The cascade priority of the rule. + pub cascade_priority: CascadePriority, +} + +impl ApplicableDeclarationBlock { + /// Constructs an applicable declaration block from a given property + /// declaration block and importance. + #[inline] + pub fn from_declarations( + declarations: Arc>, + level: CascadeLevel, + layer_order: LayerOrder, + ) -> Self { + ApplicableDeclarationBlock { + source: StyleSource::from_declarations(declarations), + source_order: 0, + specificity: 0, + cascade_priority: CascadePriority::new(level, layer_order), + } + } + + /// Constructs an applicable declaration block from the given components. + #[inline] + pub fn new( + source: StyleSource, + source_order: u32, + level: CascadeLevel, + specificity: u32, + layer_order: LayerOrder, + ) -> Self { + ApplicableDeclarationBlock { + source, + source_order: source_order & SOURCE_ORDER_MASK, + specificity, + cascade_priority: CascadePriority::new(level, layer_order), + } + } + + /// Returns the source order of the block. + #[inline] + pub fn source_order(&self) -> u32 { + self.source_order + } + + /// Returns the cascade level of the block. + #[inline] + pub fn level(&self) -> CascadeLevel { + self.cascade_priority.cascade_level() + } + + /// Returns the cascade level of the block. + #[inline] + pub fn layer_order(&self) -> LayerOrder { + self.cascade_priority.layer_order() + } + + /// Convenience method to consume self and return the right thing for the + /// rule tree to iterate over. + #[inline] + pub fn for_rule_tree(self) -> (StyleSource, CascadePriority) { + (self.source, self.cascade_priority) + } +} + +// Size of this struct determines sorting and selector-matching performance. +size_of_test!(ApplicableDeclarationBlock, 24); diff --git a/servo/components/style/attr.rs b/servo/components/style/attr.rs new file mode 100644 index 0000000000..05833fa08d --- /dev/null +++ b/servo/components/style/attr.rs @@ -0,0 +1,599 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Parsed representations of [DOM attributes][attr]. +//! +//! [attr]: https://dom.spec.whatwg.org/#interface-attr + +use crate::properties::PropertyDeclarationBlock; +use crate::shared_lock::Locked; +use crate::str::str_join; +use crate::str::{read_exponent, read_fraction, HTML_SPACE_CHARACTERS}; +use crate::str::{read_numbers, split_commas, split_html_space_chars}; +use crate::values::specified::Length; +use crate::values::AtomString; +use crate::{Atom, LocalName, Namespace, Prefix}; +use app_units::Au; +use cssparser::{self, Color, RGBA}; +use euclid::num::Zero; +use num_traits::ToPrimitive; +use selectors::attr::AttrSelectorOperation; +use servo_arc::Arc; +use servo_url::ServoUrl; +use std::str::FromStr; + +// Duplicated from script::dom::values. +const UNSIGNED_LONG_MAX: u32 = 2147483647; + +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "servo", derive(MallocSizeOf))] +pub enum LengthOrPercentageOrAuto { + Auto, + Percentage(f32), + Length(Au), +} + +#[derive(Clone, Debug)] +#[cfg_attr(feature = "servo", derive(MallocSizeOf))] +pub enum AttrValue { + String(String), + TokenList(String, Vec), + UInt(String, u32), + Int(String, i32), + Double(String, f64), + Atom(Atom), + Length(String, Option), + Color(String, Option), + Dimension(String, LengthOrPercentageOrAuto), + + /// Stores a URL, computed from the input string and a document's base URL. + /// + /// The URL is resolved at setting-time, so this kind of attribute value is + /// not actually suitable for most URL-reflecting IDL attributes. + ResolvedUrl(String, Option), + + /// Note that this variant is only used transitively as a fast path to set + /// the property declaration block relevant to the style of an element when + /// set from the inline declaration of that element (that is, + /// `element.style`). + /// + /// This can, as of this writing, only correspond to the value of the + /// `style` element, and is set from its relevant CSSInlineStyleDeclaration, + /// and then converted to a string in Element::attribute_mutated. + /// + /// Note that we don't necessarily need to do that (we could just clone the + /// declaration block), but that avoids keeping a refcounted + /// declarationblock for longer than needed. + Declaration( + String, + #[ignore_malloc_size_of = "Arc"] Arc>, + ), +} + +/// Shared implementation to parse an integer according to +/// or +/// +fn do_parse_integer>(input: T) -> Result { + let mut input = input + .skip_while(|c| HTML_SPACE_CHARACTERS.iter().any(|s| s == c)) + .peekable(); + + let sign = match input.peek() { + None => return Err(()), + Some(&'-') => { + input.next(); + -1 + }, + Some(&'+') => { + input.next(); + 1 + }, + Some(_) => 1, + }; + + let (value, _) = read_numbers(input); + + value.and_then(|value| value.checked_mul(sign)).ok_or(()) +} + +/// Parse an integer according to +/// . +pub fn parse_integer>(input: T) -> Result { + do_parse_integer(input).and_then(|result| result.to_i32().ok_or(())) +} + +/// Parse an integer according to +/// +pub fn parse_unsigned_integer>(input: T) -> Result { + do_parse_integer(input).and_then(|result| result.to_u32().ok_or(())) +} + +/// Parse a floating-point number according to +/// +pub fn parse_double(string: &str) -> Result { + let trimmed = string.trim_matches(HTML_SPACE_CHARACTERS); + let mut input = trimmed.chars().peekable(); + + let (value, divisor, chars_skipped) = match input.peek() { + None => return Err(()), + Some(&'-') => { + input.next(); + (-1f64, -1f64, 1) + }, + Some(&'+') => { + input.next(); + (1f64, 1f64, 1) + }, + _ => (1f64, 1f64, 0), + }; + + let (value, value_digits) = if let Some(&'.') = input.peek() { + (0f64, 0) + } else { + let (read_val, read_digits) = read_numbers(input); + ( + value * read_val.and_then(|result| result.to_f64()).unwrap_or(1f64), + read_digits, + ) + }; + + let input = trimmed + .chars() + .skip(value_digits + chars_skipped) + .peekable(); + + let (mut value, fraction_digits) = read_fraction(input, divisor, value); + + let input = trimmed + .chars() + .skip(value_digits + chars_skipped + fraction_digits) + .peekable(); + + if let Some(exp) = read_exponent(input) { + value *= 10f64.powi(exp) + }; + + Ok(value) +} + +impl AttrValue { + pub fn from_serialized_tokenlist(tokens: String) -> AttrValue { + let atoms = + split_html_space_chars(&tokens) + .map(Atom::from) + .fold(vec![], |mut acc, atom| { + if !acc.contains(&atom) { + acc.push(atom) + } + acc + }); + AttrValue::TokenList(tokens, atoms) + } + + pub fn from_comma_separated_tokenlist(tokens: String) -> AttrValue { + let atoms = split_commas(&tokens) + .map(Atom::from) + .fold(vec![], |mut acc, atom| { + if !acc.contains(&atom) { + acc.push(atom) + } + acc + }); + AttrValue::TokenList(tokens, atoms) + } + + pub fn from_atomic_tokens(atoms: Vec) -> AttrValue { + // TODO(ajeffrey): effecient conversion of Vec to String + let tokens = String::from(str_join(&atoms, "\x20")); + AttrValue::TokenList(tokens, atoms) + } + + // https://html.spec.whatwg.org/multipage/#reflecting-content-attributes-in-idl-attributes:idl-unsigned-long + pub fn from_u32(string: String, default: u32) -> AttrValue { + let result = parse_unsigned_integer(string.chars()).unwrap_or(default); + let result = if result > UNSIGNED_LONG_MAX { + default + } else { + result + }; + AttrValue::UInt(string, result) + } + + pub fn from_i32(string: String, default: i32) -> AttrValue { + let result = parse_integer(string.chars()).unwrap_or(default); + AttrValue::Int(string, result) + } + + // https://html.spec.whatwg.org/multipage/#reflecting-content-attributes-in-idl-attributes:idl-double + pub fn from_double(string: String, default: f64) -> AttrValue { + let result = parse_double(&string).unwrap_or(default); + + if result.is_normal() { + AttrValue::Double(string, result) + } else { + AttrValue::Double(string, default) + } + } + + // https://html.spec.whatwg.org/multipage/#limited-to-only-non-negative-numbers + pub fn from_limited_i32(string: String, default: i32) -> AttrValue { + let result = parse_integer(string.chars()).unwrap_or(default); + + if result < 0 { + AttrValue::Int(string, default) + } else { + AttrValue::Int(string, result) + } + } + + // https://html.spec.whatwg.org/multipage/#limited-to-only-non-negative-numbers-greater-than-zero + pub fn from_limited_u32(string: String, default: u32) -> AttrValue { + let result = parse_unsigned_integer(string.chars()).unwrap_or(default); + let result = if result == 0 || result > UNSIGNED_LONG_MAX { + default + } else { + result + }; + AttrValue::UInt(string, result) + } + + pub fn from_atomic(string: String) -> AttrValue { + let value = Atom::from(string); + AttrValue::Atom(value) + } + + pub fn from_resolved_url(base: &ServoUrl, url: String) -> AttrValue { + let joined = base.join(&url).ok(); + AttrValue::ResolvedUrl(url, joined) + } + + pub fn from_legacy_color(string: String) -> AttrValue { + let parsed = parse_legacy_color(&string).ok(); + AttrValue::Color(string, parsed) + } + + pub fn from_dimension(string: String) -> AttrValue { + let parsed = parse_length(&string); + AttrValue::Dimension(string, parsed) + } + + pub fn from_nonzero_dimension(string: String) -> AttrValue { + let parsed = parse_nonzero_length(&string); + AttrValue::Dimension(string, parsed) + } + + /// Assumes the `AttrValue` is a `TokenList` and returns its tokens + /// + /// ## Panics + /// + /// Panics if the `AttrValue` is not a `TokenList` + pub fn as_tokens(&self) -> &[Atom] { + match *self { + AttrValue::TokenList(_, ref tokens) => tokens, + _ => panic!("Tokens not found"), + } + } + + /// Assumes the `AttrValue` is an `Atom` and returns its value + /// + /// ## Panics + /// + /// Panics if the `AttrValue` is not an `Atom` + pub fn as_atom(&self) -> &Atom { + match *self { + AttrValue::Atom(ref value) => value, + _ => panic!("Atom not found"), + } + } + + /// Assumes the `AttrValue` is a `Color` and returns its value + /// + /// ## Panics + /// + /// Panics if the `AttrValue` is not a `Color` + pub fn as_color(&self) -> Option<&RGBA> { + match *self { + AttrValue::Color(_, ref color) => color.as_ref(), + _ => panic!("Color not found"), + } + } + + /// Assumes the `AttrValue` is a `Dimension` and returns its value + /// + /// ## Panics + /// + /// Panics if the `AttrValue` is not a `Dimension` + pub fn as_dimension(&self) -> &LengthOrPercentageOrAuto { + match *self { + AttrValue::Dimension(_, ref l) => l, + _ => panic!("Dimension not found"), + } + } + + /// Assumes the `AttrValue` is a `ResolvedUrl` and returns its value. + /// + /// ## Panics + /// + /// Panics if the `AttrValue` is not a `ResolvedUrl` + pub fn as_resolved_url(&self) -> Option<&ServoUrl> { + match *self { + AttrValue::ResolvedUrl(_, ref url) => url.as_ref(), + _ => panic!("Url not found"), + } + } + + /// Return the AttrValue as its integer representation, if any. + /// This corresponds to attribute values returned as `AttrValue::UInt(_)` + /// by `VirtualMethods::parse_plain_attribute()`. + /// + /// ## Panics + /// + /// Panics if the `AttrValue` is not a `UInt` + pub fn as_uint(&self) -> u32 { + if let AttrValue::UInt(_, value) = *self { + value + } else { + panic!("Uint not found"); + } + } + + /// Return the AttrValue as a dimension computed from its integer + /// representation, assuming that integer representation specifies pixels. + /// + /// This corresponds to attribute values returned as `AttrValue::UInt(_)` + /// by `VirtualMethods::parse_plain_attribute()`. + /// + /// ## Panics + /// + /// Panics if the `AttrValue` is not a `UInt` + pub fn as_uint_px_dimension(&self) -> LengthOrPercentageOrAuto { + if let AttrValue::UInt(_, value) = *self { + LengthOrPercentageOrAuto::Length(Au::from_px(value as i32)) + } else { + panic!("Uint not found"); + } + } + + pub fn eval_selector(&self, selector: &AttrSelectorOperation<&AtomString>) -> bool { + // FIXME(SimonSapin) this can be more efficient by matching on `(self, selector)` variants + // and doing Atom comparisons instead of string comparisons where possible, + // with SelectorImpl::AttrValue changed to Atom. + selector.eval_str(self) + } +} + +impl ::std::ops::Deref for AttrValue { + type Target = str; + + fn deref(&self) -> &str { + match *self { + AttrValue::String(ref value) | + AttrValue::TokenList(ref value, _) | + AttrValue::UInt(ref value, _) | + AttrValue::Double(ref value, _) | + AttrValue::Length(ref value, _) | + AttrValue::Color(ref value, _) | + AttrValue::Int(ref value, _) | + AttrValue::ResolvedUrl(ref value, _) | + AttrValue::Declaration(ref value, _) | + AttrValue::Dimension(ref value, _) => &value, + AttrValue::Atom(ref value) => &value, + } + } +} + +impl PartialEq for AttrValue { + fn eq(&self, other: &Atom) -> bool { + match *self { + AttrValue::Atom(ref value) => value == other, + _ => other == &**self, + } + } +} + +/// +pub fn parse_nonzero_length(value: &str) -> LengthOrPercentageOrAuto { + match parse_length(value) { + LengthOrPercentageOrAuto::Length(x) if x == Au::zero() => LengthOrPercentageOrAuto::Auto, + LengthOrPercentageOrAuto::Percentage(x) if x == 0. => LengthOrPercentageOrAuto::Auto, + x => x, + } +} + +/// Parses a [legacy color][color]. If unparseable, `Err` is returned. +/// +/// [color]: https://html.spec.whatwg.org/multipage/#rules-for-parsing-a-legacy-colour-value +pub fn parse_legacy_color(mut input: &str) -> Result { + // Steps 1 and 2. + if input.is_empty() { + return Err(()); + } + + // Step 3. + input = input.trim_matches(HTML_SPACE_CHARACTERS); + + // Step 4. + if input.eq_ignore_ascii_case("transparent") { + return Err(()); + } + + // Step 5. + if let Ok(Color::RGBA(rgba)) = cssparser::parse_color_keyword(input) { + return Ok(rgba); + } + + // Step 6. + if input.len() == 4 { + if let (b'#', Ok(r), Ok(g), Ok(b)) = ( + input.as_bytes()[0], + hex(input.as_bytes()[1] as char), + hex(input.as_bytes()[2] as char), + hex(input.as_bytes()[3] as char), + ) { + return Ok(RGBA::new(r * 17, g * 17, b * 17, 255)); + } + } + + // Step 7. + let mut new_input = String::new(); + for ch in input.chars() { + if ch as u32 > 0xffff { + new_input.push_str("00") + } else { + new_input.push(ch) + } + } + let mut input = &*new_input; + + // Step 8. + for (char_count, (index, _)) in input.char_indices().enumerate() { + if char_count == 128 { + input = &input[..index]; + break; + } + } + + // Step 9. + if input.as_bytes()[0] == b'#' { + input = &input[1..] + } + + // Step 10. + let mut new_input = Vec::new(); + for ch in input.chars() { + if hex(ch).is_ok() { + new_input.push(ch as u8) + } else { + new_input.push(b'0') + } + } + let mut input = new_input; + + // Step 11. + while input.is_empty() || (input.len() % 3) != 0 { + input.push(b'0') + } + + // Step 12. + let mut length = input.len() / 3; + let (mut red, mut green, mut blue) = ( + &input[..length], + &input[length..length * 2], + &input[length * 2..], + ); + + // Step 13. + if length > 8 { + red = &red[length - 8..]; + green = &green[length - 8..]; + blue = &blue[length - 8..]; + length = 8 + } + + // Step 14. + while length > 2 && red[0] == b'0' && green[0] == b'0' && blue[0] == b'0' { + red = &red[1..]; + green = &green[1..]; + blue = &blue[1..]; + length -= 1 + } + + // Steps 15-20. + return Ok(RGBA::new( + hex_string(red).unwrap(), + hex_string(green).unwrap(), + hex_string(blue).unwrap(), + 255, + )); + + fn hex(ch: char) -> Result { + match ch { + '0'..='9' => Ok((ch as u8) - b'0'), + 'a'..='f' => Ok((ch as u8) - b'a' + 10), + 'A'..='F' => Ok((ch as u8) - b'A' + 10), + _ => Err(()), + } + } + + fn hex_string(string: &[u8]) -> Result { + match string.len() { + 0 => Err(()), + 1 => hex(string[0] as char), + _ => { + let upper = hex(string[0] as char)?; + let lower = hex(string[1] as char)?; + Ok((upper << 4) | lower) + }, + } + } +} + +/// Parses a [dimension value][dim]. If unparseable, `Auto` is returned. +/// +/// [dim]: https://html.spec.whatwg.org/multipage/#rules-for-parsing-dimension-values +// TODO: this function can be rewritten to return Result +pub fn parse_length(mut value: &str) -> LengthOrPercentageOrAuto { + // Steps 1 & 2 are not relevant + + // Step 3 + value = value.trim_start_matches(HTML_SPACE_CHARACTERS); + + // Step 4 + match value.chars().nth(0) { + Some('0'..='9') => {}, + _ => return LengthOrPercentageOrAuto::Auto, + } + + // Steps 5 to 8 + // We trim the string length to the minimum of: + // 1. the end of the string + // 2. the first occurence of a '%' (U+0025 PERCENT SIGN) + // 3. the second occurrence of a '.' (U+002E FULL STOP) + // 4. the occurrence of a character that is neither a digit nor '%' nor '.' + // Note: Step 7.4 is directly subsumed by FromStr::from_str + let mut end_index = value.len(); + let (mut found_full_stop, mut found_percent) = (false, false); + for (i, ch) in value.chars().enumerate() { + match ch { + '0'..='9' => continue, + '%' => { + found_percent = true; + end_index = i; + break; + }, + '.' if !found_full_stop => { + found_full_stop = true; + continue; + }, + _ => { + end_index = i; + break; + }, + } + } + value = &value[..end_index]; + + if found_percent { + let result: Result = FromStr::from_str(value); + match result { + Ok(number) => return LengthOrPercentageOrAuto::Percentage((number as f32) / 100.0), + Err(_) => return LengthOrPercentageOrAuto::Auto, + } + } + + match FromStr::from_str(value) { + Ok(number) => LengthOrPercentageOrAuto::Length(Au::from_f64_px(number)), + Err(_) => LengthOrPercentageOrAuto::Auto, + } +} + +/// A struct that uniquely identifies an element's attribute. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "servo", derive(MallocSizeOf))] +pub struct AttrIdentifier { + pub local_name: LocalName, + pub name: LocalName, + pub namespace: Namespace, + pub prefix: Option, +} diff --git a/servo/components/style/author_styles.rs b/servo/components/style/author_styles.rs new file mode 100644 index 0000000000..a0223dcecc --- /dev/null +++ b/servo/components/style/author_styles.rs @@ -0,0 +1,70 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! A set of author stylesheets and their computed representation, such as the +//! ones used for ShadowRoot. + +use crate::dom::TElement; +use crate::invalidation::media_queries::ToMediaListKey; +use crate::shared_lock::SharedRwLockReadGuard; +use crate::stylesheet_set::AuthorStylesheetSet; +use crate::stylesheets::StylesheetInDocument; +use crate::stylist::CascadeData; +use crate::stylist::Stylist; +use servo_arc::Arc; + +/// A set of author stylesheets and their computed representation, such as the +/// ones used for ShadowRoot. +#[derive(MallocSizeOf)] +pub struct GenericAuthorStyles +where + S: StylesheetInDocument + PartialEq + 'static, +{ + /// The sheet collection, which holds the sheet pointers, the invalidations, + /// and all that stuff. + pub stylesheets: AuthorStylesheetSet, + /// The actual cascade data computed from the stylesheets. + #[ignore_malloc_size_of = "Measured as part of the stylist"] + pub data: Arc, +} + +pub use self::GenericAuthorStyles as AuthorStyles; + +lazy_static! { + static ref EMPTY_CASCADE_DATA: Arc = Arc::new_leaked(CascadeData::new()); +} + +impl GenericAuthorStyles +where + S: StylesheetInDocument + PartialEq + 'static, +{ + /// Create an empty AuthorStyles. + #[inline] + pub fn new() -> Self { + Self { + stylesheets: AuthorStylesheetSet::new(), + data: EMPTY_CASCADE_DATA.clone(), + } + } + + /// Flush the pending sheet changes, updating `data` as appropriate. + /// + /// TODO(emilio): Need a host element and a snapshot map to do invalidation + /// properly. + #[inline] + pub fn flush(&mut self, stylist: &mut Stylist, guard: &SharedRwLockReadGuard) + where + E: TElement, + S: ToMediaListKey, + { + let flusher = self + .stylesheets + .flush::(/* host = */ None, /* snapshot_map = */ None); + + let result = stylist.rebuild_author_data(&self.data, flusher.sheets, guard); + if let Ok(Some(new_data)) = result { + self.data = new_data; + } + } +} diff --git a/servo/components/style/bezier.rs b/servo/components/style/bezier.rs new file mode 100644 index 0000000000..dd520ac0ed --- /dev/null +++ b/servo/components/style/bezier.rs @@ -0,0 +1,176 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Parametric Bézier curves. +//! +//! This is based on `WebCore/platform/graphics/UnitBezier.h` in WebKit. + +#![deny(missing_docs)] + +use crate::values::CSSFloat; + +const NEWTON_METHOD_ITERATIONS: u8 = 8; + +/// A unit cubic Bézier curve, used for timing functions in CSS transitions and animations. +pub struct Bezier { + ax: f64, + bx: f64, + cx: f64, + ay: f64, + by: f64, + cy: f64, +} + +impl Bezier { + /// Calculate the output of a unit cubic Bézier curve from the two middle control points. + /// + /// X coordinate is time, Y coordinate is function advancement. + /// The nominal range for both is 0 to 1. + /// + /// The start and end points are always (0, 0) and (1, 1) so that a transition or animation + /// starts at 0% and ends at 100%. + pub fn calculate_bezier_output( + progress: f64, + epsilon: f64, + x1: f32, + y1: f32, + x2: f32, + y2: f32, + ) -> f64 { + // Check for a linear curve. + if x1 == y1 && x2 == y2 { + return progress; + } + + // Ensure that we return 0 or 1 on both edges. + if progress == 0.0 { + return 0.0; + } + if progress == 1.0 { + return 1.0; + } + + // For negative values, try to extrapolate with tangent (p1 - p0) or, + // if p1 is coincident with p0, with (p2 - p0). + if progress < 0.0 { + if x1 > 0.0 { + return progress * y1 as f64 / x1 as f64; + } + if y1 == 0.0 && x2 > 0.0 { + return progress * y2 as f64 / x2 as f64; + } + // If we can't calculate a sensible tangent, don't extrapolate at all. + return 0.0; + } + + // For values greater than 1, try to extrapolate with tangent (p2 - p3) or, + // if p2 is coincident with p3, with (p1 - p3). + if progress > 1.0 { + if x2 < 1.0 { + return 1.0 + (progress - 1.0) * (y2 as f64 - 1.0) / (x2 as f64 - 1.0); + } + if y2 == 1.0 && x1 < 1.0 { + return 1.0 + (progress - 1.0) * (y1 as f64 - 1.0) / (x1 as f64 - 1.0); + } + // If we can't calculate a sensible tangent, don't extrapolate at all. + return 1.0; + } + + Bezier::new(x1, y1, x2, y2).solve(progress, epsilon) + } + + #[inline] + fn new(x1: CSSFloat, y1: CSSFloat, x2: CSSFloat, y2: CSSFloat) -> Bezier { + let cx = 3. * x1 as f64; + let bx = 3. * (x2 as f64 - x1 as f64) - cx; + + let cy = 3. * y1 as f64; + let by = 3. * (y2 as f64 - y1 as f64) - cy; + + Bezier { + ax: 1.0 - cx - bx, + bx: bx, + cx: cx, + ay: 1.0 - cy - by, + by: by, + cy: cy, + } + } + + #[inline] + fn sample_curve_x(&self, t: f64) -> f64 { + // ax * t^3 + bx * t^2 + cx * t + ((self.ax * t + self.bx) * t + self.cx) * t + } + + #[inline] + fn sample_curve_y(&self, t: f64) -> f64 { + ((self.ay * t + self.by) * t + self.cy) * t + } + + #[inline] + fn sample_curve_derivative_x(&self, t: f64) -> f64 { + (3.0 * self.ax * t + 2.0 * self.bx) * t + self.cx + } + + #[inline] + fn solve_curve_x(&self, x: f64, epsilon: f64) -> f64 { + // Fast path: Use Newton's method. + let mut t = x; + for _ in 0..NEWTON_METHOD_ITERATIONS { + let x2 = self.sample_curve_x(t); + if x2.approx_eq(x, epsilon) { + return t; + } + let dx = self.sample_curve_derivative_x(t); + if dx.approx_eq(0.0, 1e-6) { + break; + } + t -= (x2 - x) / dx; + } + + // Slow path: Use bisection. + let (mut lo, mut hi, mut t) = (0.0, 1.0, x); + + if t < lo { + return lo; + } + if t > hi { + return hi; + } + + while lo < hi { + let x2 = self.sample_curve_x(t); + if x2.approx_eq(x, epsilon) { + return t; + } + if x > x2 { + lo = t + } else { + hi = t + } + t = (hi - lo) / 2.0 + lo + } + + t + } + + /// Solve the bezier curve for a given `x` and an `epsilon`, that should be + /// between zero and one. + #[inline] + fn solve(&self, x: f64, epsilon: f64) -> f64 { + self.sample_curve_y(self.solve_curve_x(x, epsilon)) + } +} + +trait ApproxEq { + fn approx_eq(self, value: Self, epsilon: Self) -> bool; +} + +impl ApproxEq for f64 { + #[inline] + fn approx_eq(self, value: f64, epsilon: f64) -> bool { + (self - value).abs() < epsilon + } +} diff --git a/servo/components/style/bloom.rs b/servo/components/style/bloom.rs new file mode 100644 index 0000000000..c111454392 --- /dev/null +++ b/servo/components/style/bloom.rs @@ -0,0 +1,400 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! The style bloom filter is used as an optimization when matching deep +//! descendant selectors. + +#![deny(missing_docs)] + +use crate::dom::{SendElement, TElement}; +use atomic_refcell::{AtomicRefCell, AtomicRefMut}; +use owning_ref::OwningHandle; +use selectors::bloom::BloomFilter; +use servo_arc::Arc; +use smallvec::SmallVec; +use std::mem::ManuallyDrop; + +thread_local! { + /// Bloom filters are large allocations, so we store them in thread-local storage + /// such that they can be reused across style traversals. StyleBloom is responsible + /// for ensuring that the bloom filter is zeroed when it is dropped. + /// + /// We intentionally leak this from TLS because we don't have the guarantee + /// of TLS destructors to run in worker threads. + /// + /// We could change this once https://github.com/rayon-rs/rayon/issues/688 + /// is fixed, hopefully. + static BLOOM_KEY: ManuallyDrop>> = + ManuallyDrop::new(Arc::new_leaked(Default::default())); +} + +/// A struct that allows us to fast-reject deep descendant selectors avoiding +/// selector-matching. +/// +/// This is implemented using a counting bloom filter, and it's a standard +/// optimization. See Gecko's `AncestorFilter`, and Blink's and WebKit's +/// `SelectorFilter`. +/// +/// The constraints for Servo's style system are a bit different compared to +/// traditional style systems given Servo does a parallel breadth-first +/// traversal instead of a sequential depth-first traversal. +/// +/// This implies that we need to track a bit more state than other browsers to +/// ensure we're doing the correct thing during the traversal, and being able to +/// apply this optimization effectively. +/// +/// Concretely, we have a bloom filter instance per worker thread, and we track +/// the current DOM depth in order to find a common ancestor when it doesn't +/// match the previous element we've styled. +/// +/// This is usually a pretty fast operation (we use to be one level deeper than +/// the previous one), but in the case of work-stealing, we may needed to push +/// and pop multiple elements. +/// +/// See the `insert_parents_recovering`, where most of the magic happens. +/// +/// Regarding thread-safety, this struct is safe because: +/// +/// * We clear this after a restyle. +/// * The DOM shape and attributes (and every other thing we access here) are +/// immutable during a restyle. +/// +pub struct StyleBloom { + /// A handle to the bloom filter from the thread upon which this StyleBloom + /// was created. We use AtomicRefCell so that this is all |Send|, which allows + /// StyleBloom to live in ThreadLocalStyleContext, which is dropped from the + /// parent thread. + filter: OwningHandle>, AtomicRefMut<'static, BloomFilter>>, + + /// The stack of elements that this bloom filter contains, along with the + /// number of hashes pushed for each element. + elements: SmallVec<[PushedElement; 16]>, + + /// Stack of hashes that have been pushed onto this filter. + pushed_hashes: SmallVec<[u32; 64]>, +} + +/// The very rough benchmarks in the selectors crate show clear() +/// costing about 25 times more than remove_hash(). We use this to implement +/// clear() more efficiently when only a small number of hashes have been +/// pushed. +/// +/// One subtly to note is that remove_hash() will not touch the value +/// if the filter overflowed. However, overflow can only occur if we +/// get 255 collisions on the same hash value, and 25 < 255. +const MEMSET_CLEAR_THRESHOLD: usize = 25; + +struct PushedElement { + /// The element that was pushed. + element: SendElement, + + /// The number of hashes pushed for the element. + num_hashes: usize, +} + +impl PushedElement { + fn new(el: E, num_hashes: usize) -> Self { + PushedElement { + element: unsafe { SendElement::new(el) }, + num_hashes, + } + } +} + +/// Returns whether the attribute name is excluded from the bloom filter. +/// +/// We do this for attributes that are very common but not commonly used in +/// selectors. +#[inline] +pub fn is_attr_name_excluded_from_filter(atom: &crate::Atom) -> bool { + *atom == atom!("class") || *atom == atom!("id") || *atom == atom!("style") +} + +fn each_relevant_element_hash(element: E, mut f: F) +where + E: TElement, + F: FnMut(u32), +{ + f(element.local_name().get_hash()); + f(element.namespace().get_hash()); + + if let Some(id) = element.id() { + f(id.get_hash()); + } + + element.each_class(|class| f(class.get_hash())); + + element.each_attr_name(|name| { + if !is_attr_name_excluded_from_filter(name) { + f(name.get_hash()) + } + }); +} + +impl Drop for StyleBloom { + fn drop(&mut self) { + // Leave the reusable bloom filter in a zeroed state. + self.clear(); + } +} + +impl StyleBloom { + /// Create an empty `StyleBloom`. Because StyleBloom acquires the thread- + /// local filter buffer, creating multiple live StyleBloom instances at + /// the same time on the same thread will panic. + + // Forced out of line to limit stack frame sizes after extra inlining from + // https://github.com/rust-lang/rust/pull/43931 + // + // See https://github.com/servo/servo/pull/18420#issuecomment-328769322 + #[inline(never)] + pub fn new() -> Self { + let bloom_arc = BLOOM_KEY.with(|b| Arc::clone(&*b)); + let filter = + OwningHandle::new_with_fn(bloom_arc, |x| unsafe { x.as_ref() }.unwrap().borrow_mut()); + debug_assert!( + filter.is_zeroed(), + "Forgot to zero the bloom filter last time" + ); + StyleBloom { + filter, + elements: Default::default(), + pushed_hashes: Default::default(), + } + } + + /// Return the bloom filter used properly by the `selectors` crate. + pub fn filter(&self) -> &BloomFilter { + &*self.filter + } + + /// Push an element to the bloom filter, knowing that it's a child of the + /// last element parent. + pub fn push(&mut self, element: E) { + if cfg!(debug_assertions) { + if self.elements.is_empty() { + assert!(element.traversal_parent().is_none()); + } + } + self.push_internal(element); + } + + /// Same as `push`, but without asserting, in order to use it from + /// `rebuild`. + fn push_internal(&mut self, element: E) { + let mut count = 0; + each_relevant_element_hash(element, |hash| { + count += 1; + self.filter.insert_hash(hash); + self.pushed_hashes.push(hash); + }); + self.elements.push(PushedElement::new(element, count)); + } + + /// Pop the last element in the bloom filter and return it. + #[inline] + fn pop(&mut self) -> Option { + let PushedElement { + element, + num_hashes, + } = self.elements.pop()?; + let popped_element = *element; + + // Verify that the pushed hashes match the ones we'd get from the element. + let mut expected_hashes = vec![]; + if cfg!(debug_assertions) { + each_relevant_element_hash(popped_element, |hash| expected_hashes.push(hash)); + } + + for _ in 0..num_hashes { + let hash = self.pushed_hashes.pop().unwrap(); + debug_assert_eq!(expected_hashes.pop().unwrap(), hash); + self.filter.remove_hash(hash); + } + + Some(popped_element) + } + + /// Returns the DOM depth of elements that can be correctly + /// matched against the bloom filter (that is, the number of + /// elements in our list). + pub fn matching_depth(&self) -> usize { + self.elements.len() + } + + /// Clears the bloom filter. + pub fn clear(&mut self) { + self.elements.clear(); + + if self.pushed_hashes.len() > MEMSET_CLEAR_THRESHOLD { + self.filter.clear(); + self.pushed_hashes.clear(); + } else { + for hash in self.pushed_hashes.drain(..) { + self.filter.remove_hash(hash); + } + debug_assert!(self.filter.is_zeroed()); + } + } + + /// Rebuilds the bloom filter up to the parent of the given element. + pub fn rebuild(&mut self, mut element: E) { + self.clear(); + + let mut parents_to_insert = SmallVec::<[E; 16]>::new(); + while let Some(parent) = element.traversal_parent() { + parents_to_insert.push(parent); + element = parent; + } + + for parent in parents_to_insert.drain(..).rev() { + self.push(parent); + } + } + + /// In debug builds, asserts that all the parents of `element` are in the + /// bloom filter. + /// + /// Goes away in release builds. + pub fn assert_complete(&self, mut element: E) { + if cfg!(debug_assertions) { + let mut checked = 0; + while let Some(parent) = element.traversal_parent() { + assert_eq!( + parent, + *(self.elements[self.elements.len() - 1 - checked].element) + ); + element = parent; + checked += 1; + } + assert_eq!(checked, self.elements.len()); + } + } + + /// Get the element that represents the chain of things inserted + /// into the filter right now. That chain is the given element + /// (if any) and its ancestors. + #[inline] + pub fn current_parent(&self) -> Option { + self.elements.last().map(|ref el| *el.element) + } + + /// Insert the parents of an element in the bloom filter, trying to recover + /// the filter if the last element inserted doesn't match. + /// + /// Gets the element depth in the dom, to make it efficient, or if not + /// provided always rebuilds the filter from scratch. + /// + /// Returns the new bloom filter depth, that the traversal code is + /// responsible to keep around if it wants to get an effective filter. + pub fn insert_parents_recovering(&mut self, element: E, element_depth: usize) { + // Easy case, we're in a different restyle, or we're empty. + if self.elements.is_empty() { + self.rebuild(element); + return; + } + + let traversal_parent = match element.traversal_parent() { + Some(parent) => parent, + None => { + // Yay, another easy case. + self.clear(); + return; + }, + }; + + if self.current_parent() == Some(traversal_parent) { + // Ta da, cache hit, we're all done. + return; + } + + if element_depth == 0 { + self.clear(); + return; + } + + // We should've early exited above. + debug_assert!( + element_depth != 0, + "We should have already cleared the bloom filter" + ); + debug_assert!(!self.elements.is_empty(), "How! We should've just rebuilt!"); + + // Now the fun begins: We have the depth of the dom and the depth of the + // last element inserted in the filter, let's try to find a common + // parent. + // + // The current depth, that is, the depth of the last element inserted in + // the bloom filter, is the number of elements _minus one_, that is: if + // there's one element, it must be the root -> depth zero. + let mut current_depth = self.elements.len() - 1; + + // If the filter represents an element too deep in the dom, we need to + // pop ancestors. + while current_depth > element_depth - 1 { + self.pop().expect("Emilio is bad at math"); + current_depth -= 1; + } + + // Now let's try to find a common parent in the bloom filter chain, + // starting with traversal_parent. + let mut common_parent = traversal_parent; + let mut common_parent_depth = element_depth - 1; + + // Let's collect the parents we are going to need to insert once we've + // found the common one. + let mut parents_to_insert = SmallVec::<[E; 16]>::new(); + + // If the bloom filter still doesn't have enough elements, the common + // parent is up in the dom. + while common_parent_depth > current_depth { + // TODO(emilio): Seems like we could insert parents here, then + // reverse the slice. + parents_to_insert.push(common_parent); + common_parent = common_parent.traversal_parent().expect("We were lied to"); + common_parent_depth -= 1; + } + + // Now the two depths are the same. + debug_assert_eq!(common_parent_depth, current_depth); + + // Happy case: The parents match, we only need to push the ancestors + // we've collected and we'll never enter in this loop. + // + // Not-so-happy case: Parent's don't match, so we need to keep going up + // until we find a common ancestor. + // + // Gecko currently models native anonymous content that conceptually + // hangs off the document (such as scrollbars) as a separate subtree + // from the document root. + // + // Thus it's possible with Gecko that we do not find any common + // ancestor. + while *(self.elements.last().unwrap().element) != common_parent { + parents_to_insert.push(common_parent); + self.pop().unwrap(); + common_parent = match common_parent.traversal_parent() { + Some(parent) => parent, + None => { + debug_assert!(self.elements.is_empty()); + if cfg!(feature = "gecko") { + break; + } else { + panic!("should have found a common ancestor"); + } + }, + } + } + + // Now the parents match, so insert the stack of elements we have been + // collecting so far. + for parent in parents_to_insert.drain(..).rev() { + self.push(parent); + } + + debug_assert_eq!(self.elements.len(), element_depth); + + // We're done! Easy. + } +} diff --git a/servo/components/style/build.rs b/servo/components/style/build.rs new file mode 100644 index 0000000000..2247e87618 --- /dev/null +++ b/servo/components/style/build.rs @@ -0,0 +1,91 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#[macro_use] +extern crate lazy_static; + +use std::env; +use std::path::Path; +use std::process::{exit, Command}; +use walkdir::WalkDir; + +#[cfg(feature = "gecko")] +mod build_gecko; + +#[cfg(not(feature = "gecko"))] +mod build_gecko { + pub fn generate() {} +} + +lazy_static! { + pub static ref PYTHON: String = env::var("PYTHON3").ok().unwrap_or_else(|| { + let candidates = if cfg!(windows) { + ["python3.exe"] + } else { + ["python3"] + }; + for &name in &candidates { + if Command::new(name) + .arg("--version") + .output() + .ok() + .map_or(false, |out| out.status.success()) + { + return name.to_owned(); + } + } + panic!( + "Can't find python (tried {})! Try fixing PATH or setting the PYTHON3 env var", + candidates.join(", ") + ) + }); +} + +fn generate_properties(engine: &str) { + for entry in WalkDir::new("properties") { + let entry = entry.unwrap(); + match entry.path().extension().and_then(|e| e.to_str()) { + Some("mako") | Some("rs") | Some("py") | Some("zip") => { + println!("cargo:rerun-if-changed={}", entry.path().display()); + }, + _ => {}, + } + } + + let script = Path::new(&env::var_os("CARGO_MANIFEST_DIR").unwrap()) + .join("properties") + .join("build.py"); + + let status = Command::new(&*PYTHON) + .arg(&script) + .arg(engine) + .arg("style-crate") + .status() + .unwrap(); + if !status.success() { + exit(1) + } +} + +fn main() { + let gecko = cfg!(feature = "gecko"); + let servo = cfg!(feature = "servo"); + let l2013 = cfg!(feature = "servo-layout-2013"); + let l2020 = cfg!(feature = "servo-layout-2020"); + let engine = match (gecko, servo, l2013, l2020) { + (true, false, false, false) => "gecko", + (false, true, true, false) => "servo-2013", + (false, true, false, true) => "servo-2020", + _ => panic!( + "\n\n\ + The style crate requires enabling one of its 'servo' or 'gecko' feature flags \ + and, in the 'servo' case, one of 'servo-layout-2013' or 'servo-layout-2020'.\ + \n\n" + ), + }; + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:out_dir={}", env::var("OUT_DIR").unwrap()); + generate_properties(engine); + build_gecko::generate(); +} diff --git a/servo/components/style/build_gecko.rs b/servo/components/style/build_gecko.rs new file mode 100644 index 0000000000..a83c5dbc6d --- /dev/null +++ b/servo/components/style/build_gecko.rs @@ -0,0 +1,400 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use super::PYTHON; +use bindgen::{Builder, CodegenConfig}; +use regex::Regex; +use std::cmp; +use std::collections::HashSet; +use std::env; +use std::fs::{self, File}; +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; +use std::process::{exit, Command}; +use std::slice; +use std::sync::Mutex; +use std::time::SystemTime; +use toml; +use toml::value::Table; + +lazy_static! { + static ref OUTDIR_PATH: PathBuf = PathBuf::from(env::var_os("OUT_DIR").unwrap()).join("gecko"); +} + +const STRUCTS_FILE: &'static str = "structs.rs"; + +fn read_config(path: &PathBuf) -> Table { + println!("cargo:rerun-if-changed={}", path.to_str().unwrap()); + update_last_modified(&path); + + let mut contents = String::new(); + File::open(path) + .expect("Failed to open config file") + .read_to_string(&mut contents) + .expect("Failed to read config file"); + match toml::from_str::(&contents) { + Ok(result) => result, + Err(e) => panic!("Failed to parse config file: {}", e), + } +} + +lazy_static! { + static ref CONFIG: Table = { + // Load Gecko's binding generator config from the source tree. + let path = mozbuild::TOPSRCDIR.join("layout/style/ServoBindings.toml"); + read_config(&path) + }; + static ref BINDGEN_FLAGS: Vec = { + // Load build-specific config overrides. + let path = mozbuild::TOPOBJDIR.join("layout/style/extra-bindgen-flags"); + println!("cargo:rerun-if-changed={}", path.to_str().unwrap()); + fs::read_to_string(path).expect("Failed to read extra-bindgen-flags file") + .split_whitespace() + .map(std::borrow::ToOwned::to_owned) + .collect() + }; + static ref INCLUDE_RE: Regex = Regex::new(r#"#include\s*"(.+?)""#).unwrap(); + static ref DISTDIR_PATH: PathBuf = mozbuild::TOPOBJDIR.join("dist"); + static ref SEARCH_PATHS: Vec = vec![ + DISTDIR_PATH.join("include"), + DISTDIR_PATH.join("include/nspr"), + ]; + static ref ADDED_PATHS: Mutex> = Mutex::new(HashSet::new()); + static ref LAST_MODIFIED: Mutex = + Mutex::new(get_modified_time(&env::current_exe().unwrap()) + .expect("Failed to get modified time of executable")); +} + +fn get_modified_time(file: &Path) -> Option { + file.metadata().and_then(|m| m.modified()).ok() +} + +fn update_last_modified(file: &Path) { + let modified = get_modified_time(file).expect("Couldn't get file modification time"); + let mut last_modified = LAST_MODIFIED.lock().unwrap(); + *last_modified = cmp::max(modified, *last_modified); +} + +fn search_include(name: &str) -> Option { + for path in SEARCH_PATHS.iter() { + let file = path.join(name); + if file.is_file() { + update_last_modified(&file); + return Some(file); + } + } + None +} + +fn add_headers_recursively(path: PathBuf, added_paths: &mut HashSet) { + if added_paths.contains(&path) { + return; + } + let mut file = File::open(&path).unwrap(); + let mut content = String::new(); + file.read_to_string(&mut content).unwrap(); + added_paths.insert(path); + // Find all includes and add them recursively + for cap in INCLUDE_RE.captures_iter(&content) { + if let Some(path) = search_include(cap.get(1).unwrap().as_str()) { + add_headers_recursively(path, added_paths); + } + } +} + +fn add_include(name: &str) -> String { + let mut added_paths = ADDED_PATHS.lock().unwrap(); + let file = match search_include(name) { + Some(file) => file, + None => panic!("Include not found: {}", name), + }; + let result = String::from(file.to_str().unwrap()); + add_headers_recursively(file, &mut *added_paths); + result +} + +trait BuilderExt { + fn get_initial_builder() -> Builder; + fn include>(self, file: T) -> Builder; +} + +impl BuilderExt for Builder { + fn get_initial_builder() -> Builder { + // Disable rust unions, because we replace some types inside of + // them. + let mut builder = Builder::default() + .size_t_is_usize(true) + .disable_untagged_union(); + + let rustfmt_path = env::var_os("RUSTFMT") + // This can be replaced with + // > .filter(|p| !p.is_empty()).map(PathBuf::from) + // once we can use 1.27+. + .and_then(|p| { + if p.is_empty() { + None + } else { + Some(PathBuf::from(p)) + } + }); + if let Some(path) = rustfmt_path { + builder = builder.with_rustfmt(path); + } + + for dir in SEARCH_PATHS.iter() { + builder = builder.clang_arg("-I").clang_arg(dir.to_str().unwrap()); + } + + builder = builder.include(add_include("mozilla-config.h")); + + if env::var("CARGO_FEATURE_GECKO_DEBUG").is_ok() { + builder = builder.clang_arg("-DDEBUG=1").clang_arg("-DJS_DEBUG=1"); + } + + for item in &*BINDGEN_FLAGS { + builder = builder.clang_arg(item); + } + + builder + } + fn include>(self, file: T) -> Builder { + self.clang_arg("-include").clang_arg(file) + } +} + +struct Fixup { + pat: String, + rep: String, +} + +fn write_binding_file(builder: Builder, file: &str, fixups: &[Fixup]) { + let out_file = OUTDIR_PATH.join(file); + if let Some(modified) = get_modified_time(&out_file) { + // Don't generate the file if nothing it depends on was modified. + let last_modified = LAST_MODIFIED.lock().unwrap(); + if *last_modified <= modified { + return; + } + } + let command_line_opts = builder.command_line_flags(); + let result = builder.generate(); + let mut result = match result { + Ok(bindings) => bindings.to_string(), + Err(_) => { + panic!( + "Failed to generate bindings, flags: {:?}", + command_line_opts + ); + }, + }; + + for fixup in fixups.iter() { + result = Regex::new(&fixup.pat) + .unwrap() + .replace_all(&result, &*fixup.rep) + .into_owned() + .into(); + } + let bytes = result.into_bytes(); + File::create(&out_file) + .unwrap() + .write_all(&bytes) + .expect("Unable to write output"); +} + +struct BuilderWithConfig<'a> { + builder: Builder, + config: &'a Table, + used_keys: HashSet<&'static str>, +} +impl<'a> BuilderWithConfig<'a> { + fn new(builder: Builder, config: &'a Table) -> Self { + BuilderWithConfig { + builder, + config, + used_keys: HashSet::new(), + } + } + + fn handle_list(self, key: &'static str, func: F) -> BuilderWithConfig<'a> + where + F: FnOnce(Builder, slice::Iter<'a, toml::Value>) -> Builder, + { + let mut builder = self.builder; + let config = self.config; + let mut used_keys = self.used_keys; + if let Some(list) = config.get(key) { + used_keys.insert(key); + builder = func(builder, list.as_array().unwrap().as_slice().iter()); + } + BuilderWithConfig { + builder, + config, + used_keys, + } + } + fn handle_items(self, key: &'static str, mut func: F) -> BuilderWithConfig<'a> + where + F: FnMut(Builder, &'a toml::Value) -> Builder, + { + self.handle_list(key, |b, iter| iter.fold(b, |b, item| func(b, item))) + } + fn handle_str_items(self, key: &'static str, mut func: F) -> BuilderWithConfig<'a> + where + F: FnMut(Builder, &'a str) -> Builder, + { + self.handle_items(key, |b, item| func(b, item.as_str().unwrap())) + } + fn handle_table_items(self, key: &'static str, mut func: F) -> BuilderWithConfig<'a> + where + F: FnMut(Builder, &'a Table) -> Builder, + { + self.handle_items(key, |b, item| func(b, item.as_table().unwrap())) + } + fn handle_common(self, fixups: &mut Vec) -> BuilderWithConfig<'a> { + self.handle_str_items("headers", |b, item| b.header(add_include(item))) + .handle_str_items("raw-lines", |b, item| b.raw_line(item)) + .handle_str_items("hide-types", |b, item| b.blocklist_type(item)) + .handle_table_items("fixups", |builder, item| { + fixups.push(Fixup { + pat: item["pat"].as_str().unwrap().into(), + rep: item["rep"].as_str().unwrap().into(), + }); + builder + }) + } + + fn get_builder(self) -> Builder { + for key in self.config.keys() { + if !self.used_keys.contains(key.as_str()) { + panic!("Unknown key: {}", key); + } + } + self.builder + } +} + +fn generate_structs() { + let builder = Builder::get_initial_builder() + .enable_cxx_namespaces() + .with_codegen_config(CodegenConfig::TYPES | CodegenConfig::VARS | CodegenConfig::FUNCTIONS); + let mut fixups = vec![]; + let builder = BuilderWithConfig::new(builder, CONFIG["structs"].as_table().unwrap()) + .handle_common(&mut fixups) + .handle_str_items("allowlist-functions", |b, item| b.allowlist_function(item)) + .handle_str_items("bitfield-enums", |b, item| b.bitfield_enum(item)) + .handle_str_items("rusty-enums", |b, item| b.rustified_enum(item)) + .handle_str_items("allowlist-vars", |b, item| b.allowlist_var(item)) + .handle_str_items("allowlist-types", |b, item| b.allowlist_type(item)) + .handle_str_items("opaque-types", |b, item| b.opaque_type(item)) + .handle_table_items("cbindgen-types", |b, item| { + let gecko = item["gecko"].as_str().unwrap(); + let servo = item["servo"].as_str().unwrap(); + b.blocklist_type(format!("mozilla::{}", gecko)) + .module_raw_line("root::mozilla", format!("pub use {} as {};", servo, gecko)) + }) + .handle_table_items("mapped-generic-types", |builder, item| { + let generic = item["generic"].as_bool().unwrap(); + let gecko = item["gecko"].as_str().unwrap(); + let servo = item["servo"].as_str().unwrap(); + let gecko_name = gecko.rsplit("::").next().unwrap(); + let gecko = gecko + .split("::") + .map(|s| format!("\\s*{}\\s*", s)) + .collect::>() + .join("::"); + + fixups.push(Fixup { + pat: format!("\\broot\\s*::\\s*{}\\b", gecko), + rep: format!("crate::gecko_bindings::structs::{}", gecko_name), + }); + builder.blocklist_type(gecko).raw_line(format!( + "pub type {0}{2} = {1}{2};", + gecko_name, + servo, + if generic { "" } else { "" } + )) + }) + .get_builder(); + write_binding_file(builder, STRUCTS_FILE, &fixups); +} + +fn setup_logging() -> bool { + struct BuildLogger { + file: Option>, + filter: String, + } + + impl log::Log for BuildLogger { + fn enabled(&self, meta: &log::Metadata) -> bool { + self.file.is_some() && meta.target().contains(&self.filter) + } + + fn log(&self, record: &log::Record) { + if !self.enabled(record.metadata()) { + return; + } + + let mut file = self.file.as_ref().unwrap().lock().unwrap(); + let _ = writeln!( + file, + "{} - {} - {} @ {}:{}", + record.level(), + record.target(), + record.args(), + record.file().unwrap_or(""), + record.line().unwrap_or(0) + ); + } + + fn flush(&self) { + if let Some(ref file) = self.file { + file.lock().unwrap().flush().unwrap(); + } + } + } + + if let Some(path) = env::var_os("STYLO_BUILD_LOG") { + log::set_max_level(log::LevelFilter::Debug); + log::set_boxed_logger(Box::new(BuildLogger { + file: fs::File::create(path).ok().map(Mutex::new), + filter: env::var("STYLO_BUILD_FILTER") + .ok() + .unwrap_or_else(|| "bindgen".to_owned()), + })) + .expect("Failed to set logger."); + + true + } else { + false + } +} + +fn generate_atoms() { + let script = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()) + .join("gecko") + .join("regen_atoms.py"); + println!("cargo:rerun-if-changed={}", script.display()); + let status = Command::new(&*PYTHON) + .arg(&script) + .arg(DISTDIR_PATH.as_os_str()) + .arg(OUTDIR_PATH.as_os_str()) + .status() + .unwrap(); + if !status.success() { + exit(1); + } +} + +pub fn generate() { + println!("cargo:rerun-if-changed=build_gecko.rs"); + fs::create_dir_all(&*OUTDIR_PATH).unwrap(); + setup_logging(); + generate_structs(); + generate_atoms(); + + for path in ADDED_PATHS.lock().unwrap().iter() { + println!("cargo:rerun-if-changed={}", path.to_str().unwrap()); + } +} diff --git a/servo/components/style/color/convert.rs b/servo/components/style/color/convert.rs new file mode 100644 index 0000000000..4fa037f9d6 --- /dev/null +++ b/servo/components/style/color/convert.rs @@ -0,0 +1,888 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Color conversion algorithms. +//! +//! Algorithms, matrices and constants are from the [color-4] specification, +//! unless otherwise specified: +//! +//! https://drafts.csswg.org/css-color-4/#color-conversion-code +//! +//! NOTE: Matrices has to be transposed from the examples in the spec for use +//! with the `euclid` library. + +use crate::color::ColorComponents; +use std::f32::consts::PI; + +type Transform = euclid::default::Transform3D; +type Vector = euclid::default::Vector3D; + +const RAD_PER_DEG: f32 = PI / 180.0; +const DEG_PER_RAD: f32 = 180.0 / PI; + +/// Normalize hue into [0, 360). +#[inline] +fn normalize_hue(hue: f32) -> f32 { + hue - 360. * (hue / 360.).floor() +} + +/// Calculate the hue from RGB components and return it along with the min and +/// max RGB values. +#[inline] +fn rgb_to_hue_min_max(red: f32, green: f32, blue: f32) -> (f32, f32, f32) { + let max = red.max(green).max(blue); + let min = red.min(green).min(blue); + + let delta = max - min; + + let hue = if delta != 0.0 { + 60.0 * if max == red { + (green - blue) / delta + if green < blue { 6.0 } else { 0.0 } + } else if max == green { + (blue - red) / delta + 2.0 + } else { + (red - green) / delta + 4.0 + } + } else { + f32::NAN + }; + + (hue, min, max) +} + +/// Convert a hue value into red, green, blue components. +#[inline] +fn hue_to_rgb(t1: f32, t2: f32, hue: f32) -> f32 { + let hue = normalize_hue(hue); + + if hue * 6.0 < 360.0 { + t1 + (t2 - t1) * hue / 60.0 + } else if hue * 2.0 < 360.0 { + t2 + } else if hue * 3.0 < 720.0 { + t1 + (t2 - t1) * (240.0 - hue) / 60.0 + } else { + t1 + } +} + +/// Convert from HSL notation to RGB notation. +/// https://drafts.csswg.org/css-color-4/#hsl-to-rgb +#[inline] +pub fn hsl_to_rgb(from: &ColorComponents) -> ColorComponents { + let ColorComponents(hue, saturation, lightness) = *from; + + let t2 = if lightness <= 0.5 { + lightness * (saturation + 1.0) + } else { + lightness + saturation - lightness * saturation + }; + let t1 = lightness * 2.0 - t2; + + ColorComponents( + hue_to_rgb(t1, t2, hue + 120.0), + hue_to_rgb(t1, t2, hue), + hue_to_rgb(t1, t2, hue - 120.0), + ) +} + +/// Convert from RGB notation to HSL notation. +/// https://drafts.csswg.org/css-color-4/#rgb-to-hsl +pub fn rgb_to_hsl(from: &ColorComponents) -> ColorComponents { + let ColorComponents(red, green, blue) = *from; + + let (hue, min, max) = rgb_to_hue_min_max(red, green, blue); + + let lightness = (min + max) / 2.0; + let delta = max - min; + + let saturation = if delta != 0.0 { + if lightness == 0.0 || lightness == 1.0 { + 0.0 + } else { + (max - lightness) / lightness.min(1.0 - lightness) + } + } else { + 0.0 + }; + + ColorComponents(hue, saturation, lightness) +} + +/// Convert from HWB notation to RGB notation. +/// https://drafts.csswg.org/css-color-4/#hwb-to-rgb +#[inline] +pub fn hwb_to_rgb(from: &ColorComponents) -> ColorComponents { + let ColorComponents(hue, whiteness, blackness) = *from; + + if whiteness + blackness > 1.0 { + let gray = whiteness / (whiteness + blackness); + return ColorComponents(gray, gray, gray); + } + + let x = 1.0 - whiteness - blackness; + hsl_to_rgb(&ColorComponents(hue, 1.0, 0.5)).map(|v| v * x + whiteness) +} + +/// Convert from RGB notation to HWB notation. +/// https://drafts.csswg.org/css-color-4/#rgb-to-hwb +#[inline] +pub fn rgb_to_hwb(from: &ColorComponents) -> ColorComponents { + let ColorComponents(red, green, blue) = *from; + + let (hue, min, max) = rgb_to_hue_min_max(red, green, blue); + + let whiteness = min; + let blackness = 1.0 - max; + + ColorComponents(hue, whiteness, blackness) +} + +/// Convert from Lab to Lch. This calculation works for both Lab and Olab. +/// +#[inline] +pub fn lab_to_lch(from: &ColorComponents) -> ColorComponents { + let ColorComponents(lightness, a, b) = *from; + + let hue = normalize_hue(b.atan2(a) * 180.0 / PI); + let chroma = (a.powf(2.0) + b.powf(2.0)).sqrt(); + + ColorComponents(lightness, chroma, hue) +} + +/// Convert from Lch to Lab. This calculation works for both Lch and Oklch. +/// +#[inline] +pub fn lch_to_lab(from: &ColorComponents) -> ColorComponents { + let ColorComponents(lightness, chroma, hue) = *from; + + let a = chroma * (hue * PI / 180.0).cos(); + let b = chroma * (hue * PI / 180.0).sin(); + + ColorComponents(lightness, a, b) +} + +#[inline] +fn transform(from: &ColorComponents, mat: &Transform) -> ColorComponents { + let result = mat.transform_vector3d(Vector::new(from.0, from.1, from.2)); + ColorComponents(result.x, result.y, result.z) +} + +fn xyz_d65_to_xyz_d50(from: &ColorComponents) -> ColorComponents { + #[rustfmt::skip] + const MAT: Transform = Transform::new( + 1.0479298208405488, 0.029627815688159344, -0.009243058152591178, 0.0, + 0.022946793341019088, 0.990434484573249, 0.015055144896577895, 0.0, + -0.05019222954313557, -0.01707382502938514, 0.7518742899580008, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); + + transform(from, &MAT) +} + +fn xyz_d50_to_xyz_d65(from: &ColorComponents) -> ColorComponents { + #[rustfmt::skip] + const MAT: Transform = Transform::new( + 0.9554734527042182, -0.028369706963208136, 0.012314001688319899, 0.0, + -0.023098536874261423, 1.0099954580058226, -0.020507696433477912, 0.0, + 0.0632593086610217, 0.021041398966943008, 1.3303659366080753, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); + + transform(from, &MAT) +} + +/// A reference white that is used during color conversion. +pub enum WhitePoint { + /// D50 white reference. + D50, + /// D65 white reference. + D65, +} + +fn convert_white_point(from: WhitePoint, to: WhitePoint, components: &mut ColorComponents) { + match (from, to) { + (WhitePoint::D50, WhitePoint::D65) => *components = xyz_d50_to_xyz_d65(components), + (WhitePoint::D65, WhitePoint::D50) => *components = xyz_d65_to_xyz_d50(components), + + _ => {}, + } +} + +/// A trait that allows conversion of color spaces to and from XYZ coordinate +/// space with a specified white point. +/// +/// Allows following the specified method of converting between color spaces: +/// - Convert to values to sRGB linear light. +/// - Convert to XYZ coordinate space. +/// - Adjust white point to target white point. +/// - Convert to sRGB linear light in target color space. +/// - Convert to sRGB gamma encoded in target color space. +/// +/// https://drafts.csswg.org/css-color-4/#color-conversion +pub trait ColorSpaceConversion { + /// The white point that the implementer is represented in. + const WHITE_POINT: WhitePoint; + + /// Convert the components from sRGB gamma encoded values to sRGB linear + /// light values. + fn to_linear_light(from: &ColorComponents) -> ColorComponents; + + /// Convert the components from sRGB linear light values to XYZ coordinate + /// space. + fn to_xyz(from: &ColorComponents) -> ColorComponents; + + /// Convert the components from XYZ coordinate space to sRGB linear light + /// values. + fn from_xyz(from: &ColorComponents) -> ColorComponents; + + /// Convert the components from sRGB linear light values to sRGB gamma + /// encoded values. + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents; +} + +/// Convert the color components from the specified color space to XYZ and +/// return the components and the white point they are in. +pub fn to_xyz(from: &ColorComponents) -> (ColorComponents, WhitePoint) { + // Convert the color components where in-gamut values are in the range + // [0 - 1] to linear light (un-companded) form. + let result = From::to_linear_light(from); + + // Convert the color components from the source color space to XYZ. + (From::to_xyz(&result), From::WHITE_POINT) +} + +/// Convert the color components from XYZ at the given white point to the +/// specified color space. +pub fn from_xyz( + from: &ColorComponents, + white_point: WhitePoint, +) -> ColorComponents { + let mut xyz = from.clone(); + + // Convert the white point if needed. + convert_white_point(white_point, To::WHITE_POINT, &mut xyz); + + // Convert the color from XYZ to the target color space. + let result = To::from_xyz(&xyz); + + // Convert the color components of linear-light values in the range + // [0 - 1] to a gamma corrected form. + To::to_gamma_encoded(&result) +} + +/// The sRGB color space. +/// https://drafts.csswg.org/css-color-4/#predefined-sRGB +pub struct Srgb; + +impl Srgb { + #[rustfmt::skip] + const TO_XYZ: Transform = Transform::new( + 0.4123907992659595, 0.21263900587151036, 0.01933081871559185, 0.0, + 0.35758433938387796, 0.7151686787677559, 0.11919477979462599, 0.0, + 0.1804807884018343, 0.07219231536073371, 0.9505321522496606, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); + + #[rustfmt::skip] + const FROM_XYZ: Transform = Transform::new( + 3.2409699419045213, -0.9692436362808798, 0.05563007969699361, 0.0, + -1.5373831775700935, 1.8759675015077206, -0.20397695888897657, 0.0, + -0.4986107602930033, 0.04155505740717561, 1.0569715142428786, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); +} + +impl ColorSpaceConversion for Srgb { + const WHITE_POINT: WhitePoint = WhitePoint::D65; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + from.clone().map(|value| { + let abs = value.abs(); + + if abs < 0.04045 { + value / 12.92 + } else { + value.signum() * ((abs + 0.055) / 1.055).powf(2.4) + } + }) + } + + fn to_xyz(from: &ColorComponents) -> ColorComponents { + transform(from, &Self::TO_XYZ) + } + + fn from_xyz(from: &ColorComponents) -> ColorComponents { + transform(from, &Self::FROM_XYZ) + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + from.clone().map(|value| { + let abs = value.abs(); + + if abs > 0.0031308 { + value.signum() * (1.055 * abs.powf(1.0 / 2.4) - 0.055) + } else { + 12.92 * value + } + }) + } +} + +/// Color specified with hue, saturation and lightness components. +pub struct Hsl; + +impl ColorSpaceConversion for Hsl { + const WHITE_POINT: WhitePoint = Srgb::WHITE_POINT; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + Srgb::to_linear_light(&hsl_to_rgb(from)) + } + + #[inline] + fn to_xyz(from: &ColorComponents) -> ColorComponents { + Srgb::to_xyz(from) + } + + #[inline] + fn from_xyz(from: &ColorComponents) -> ColorComponents { + Srgb::from_xyz(from) + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + rgb_to_hsl(&Srgb::to_gamma_encoded(from)) + } +} + +/// Color specified with hue, whiteness and blackness components. +pub struct Hwb; + +impl ColorSpaceConversion for Hwb { + const WHITE_POINT: WhitePoint = Srgb::WHITE_POINT; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + Srgb::to_linear_light(&hwb_to_rgb(from)) + } + + #[inline] + fn to_xyz(from: &ColorComponents) -> ColorComponents { + Srgb::to_xyz(from) + } + + #[inline] + fn from_xyz(from: &ColorComponents) -> ColorComponents { + Srgb::from_xyz(from) + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + rgb_to_hwb(&Srgb::to_gamma_encoded(from)) + } +} + +/// The same as sRGB color space, except the transfer function is linear light. +/// https://drafts.csswg.org/css-color-4/#predefined-sRGB-linear +pub struct SrgbLinear; + +impl ColorSpaceConversion for SrgbLinear { + const WHITE_POINT: WhitePoint = Srgb::WHITE_POINT; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + // Already in linear light form. + from.clone() + } + + fn to_xyz(from: &ColorComponents) -> ColorComponents { + Srgb::to_xyz(from) + } + + fn from_xyz(from: &ColorComponents) -> ColorComponents { + Srgb::from_xyz(from) + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + // Stay in linear light form. + from.clone() + } +} + +/// The Display-P3 color space. +/// https://drafts.csswg.org/css-color-4/#predefined-display-p3 +pub struct DisplayP3; + +impl DisplayP3 { + #[rustfmt::skip] + const TO_XYZ: Transform = Transform::new( + 0.48657094864821626, 0.22897456406974884, 0.0, 0.0, + 0.26566769316909294, 0.6917385218365062, 0.045113381858902575, 0.0, + 0.1982172852343625, 0.079286914093745, 1.0439443689009757, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); + + #[rustfmt::skip] + const FROM_XYZ: Transform = Transform::new( + 2.4934969119414245, -0.829488969561575, 0.035845830243784335, 0.0, + -0.9313836179191236, 1.7626640603183468, -0.07617238926804171, 0.0, + -0.40271078445071684, 0.02362468584194359, 0.9568845240076873, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); +} + +impl ColorSpaceConversion for DisplayP3 { + const WHITE_POINT: WhitePoint = WhitePoint::D65; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + Srgb::to_linear_light(from) + } + + fn to_xyz(from: &ColorComponents) -> ColorComponents { + transform(from, &Self::TO_XYZ) + } + + fn from_xyz(from: &ColorComponents) -> ColorComponents { + transform(from, &Self::FROM_XYZ) + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + Srgb::to_gamma_encoded(from) + } +} + +/// The a98-rgb color space. +/// https://drafts.csswg.org/css-color-4/#predefined-a98-rgb +pub struct A98Rgb; + +impl A98Rgb { + #[rustfmt::skip] + const TO_XYZ: Transform = Transform::new( + 0.5766690429101308, 0.29734497525053616, 0.027031361386412378, 0.0, + 0.18555823790654627, 0.627363566255466, 0.07068885253582714, 0.0, + 0.18822864623499472, 0.07529145849399789, 0.9913375368376389, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); + + #[rustfmt::skip] + const FROM_XYZ: Transform = Transform::new( + 2.041587903810746, -0.9692436362808798, 0.013444280632031024, 0.0, + -0.5650069742788596, 1.8759675015077206, -0.11836239223101824, 0.0, + -0.3447313507783295, 0.04155505740717561, 1.0151749943912054, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); +} + +impl ColorSpaceConversion for A98Rgb { + const WHITE_POINT: WhitePoint = WhitePoint::D65; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + from.clone().map(|v| v.signum() * v.abs().powf(2.19921875)) + } + + fn to_xyz(from: &ColorComponents) -> ColorComponents { + transform(from, &Self::TO_XYZ) + } + + fn from_xyz(from: &ColorComponents) -> ColorComponents { + transform(from, &Self::FROM_XYZ) + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + from.clone() + .map(|v| v.signum() * v.abs().powf(0.4547069271758437)) + } +} + +/// The ProPhoto RGB color space. +/// https://drafts.csswg.org/css-color-4/#predefined-prophoto-rgb +pub struct ProphotoRgb; + +impl ProphotoRgb { + #[rustfmt::skip] + const TO_XYZ: Transform = Transform::new( + 0.7977604896723027, 0.2880711282292934, 0.0, 0.0, + 0.13518583717574031, 0.7118432178101014, 0.0, 0.0, + 0.0313493495815248, 0.00008565396060525902, 0.8251046025104601, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); + + #[rustfmt::skip] + const FROM_XYZ: Transform = Transform::new( + 1.3457989731028281, -0.5446224939028347, 0.0, 0.0, + -0.25558010007997534, 1.5082327413132781, 0.0, 0.0, + -0.05110628506753401, 0.02053603239147973, 1.2119675456389454, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); +} + +impl ColorSpaceConversion for ProphotoRgb { + const WHITE_POINT: WhitePoint = WhitePoint::D50; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + from.clone().map(|value| { + const ET2: f32 = 16.0 / 512.0; + + let abs = value.abs(); + + if abs <= ET2 { + value / 16.0 + } else { + value.signum() * abs.powf(1.8) + } + }) + } + + fn to_xyz(from: &ColorComponents) -> ColorComponents { + transform(from, &Self::TO_XYZ) + } + + fn from_xyz(from: &ColorComponents) -> ColorComponents { + transform(from, &Self::FROM_XYZ) + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + const ET: f32 = 1.0 / 512.0; + + from.clone().map(|v| { + let abs = v.abs(); + if abs >= ET { + v.signum() * abs.powf(1.0 / 1.8) + } else { + 16.0 * v + } + }) + } +} + +/// The Rec.2020 color space. +/// https://drafts.csswg.org/css-color-4/#predefined-rec2020 +pub struct Rec2020; + +impl Rec2020 { + const ALPHA: f32 = 1.09929682680944; + const BETA: f32 = 0.018053968510807; + + #[rustfmt::skip] + const TO_XYZ: Transform = Transform::new( + 0.6369580483012913, 0.26270021201126703, 0.0, 0.0, + 0.14461690358620838, 0.677998071518871, 0.028072693049087508, 0.0, + 0.16888097516417205, 0.059301716469861945, 1.0609850577107909, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); + + #[rustfmt::skip] + const FROM_XYZ: Transform = Transform::new( + 1.7166511879712676, -0.666684351832489, 0.017639857445310915, 0.0, + -0.3556707837763924, 1.616481236634939, -0.042770613257808655, 0.0, + -0.2533662813736598, 0.01576854581391113, 0.942103121235474, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); +} + +impl ColorSpaceConversion for Rec2020 { + const WHITE_POINT: WhitePoint = WhitePoint::D65; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + from.clone().map(|value| { + let abs = value.abs(); + + if abs < Self::BETA * 4.5 { + value / 4.5 + } else { + value.signum() * ((abs + Self::ALPHA - 1.0) / Self::ALPHA).powf(1.0 / 0.45) + } + }) + } + + fn to_xyz(from: &ColorComponents) -> ColorComponents { + transform(from, &Self::TO_XYZ) + } + + fn from_xyz(from: &ColorComponents) -> ColorComponents { + transform(from, &Self::FROM_XYZ) + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + from.clone().map(|v| { + let abs = v.abs(); + + if abs > Self::BETA { + v.signum() * (Self::ALPHA * abs.powf(0.45) - (Self::ALPHA - 1.0)) + } else { + 4.5 * v + } + }) + } +} + +/// A color in the XYZ coordinate space with a D50 white reference. +/// https://drafts.csswg.org/css-color-4/#predefined-xyz +pub struct XyzD50; + +impl ColorSpaceConversion for XyzD50 { + const WHITE_POINT: WhitePoint = WhitePoint::D50; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + from.clone() + } + + fn to_xyz(from: &ColorComponents) -> ColorComponents { + from.clone() + } + + fn from_xyz(from: &ColorComponents) -> ColorComponents { + from.clone() + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + from.clone() + } +} + +/// A color in the XYZ coordinate space with a D65 white reference. +/// https://drafts.csswg.org/css-color-4/#predefined-xyz +pub struct XyzD65; + +impl ColorSpaceConversion for XyzD65 { + const WHITE_POINT: WhitePoint = WhitePoint::D65; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + from.clone() + } + + fn to_xyz(from: &ColorComponents) -> ColorComponents { + from.clone() + } + + fn from_xyz(from: &ColorComponents) -> ColorComponents { + from.clone() + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + from.clone() + } +} + +/// The Lab color space. +/// https://drafts.csswg.org/css-color-4/#specifying-lab-lch +pub struct Lab; + +impl Lab { + const KAPPA: f32 = 24389.0 / 27.0; + const EPSILON: f32 = 216.0 / 24389.0; + const WHITE: ColorComponents = ColorComponents(0.96422, 1.0, 0.82521); +} + +impl ColorSpaceConversion for Lab { + const WHITE_POINT: WhitePoint = WhitePoint::D50; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + // No need for conversion. + from.clone() + } + + /// Convert a CIELAB color to XYZ as specified in [1] and [2]. + /// + /// [1]: https://drafts.csswg.org/css-color/#lab-to-predefined + /// [2]: https://drafts.csswg.org/css-color/#color-conversion-code + fn to_xyz(from: &ColorComponents) -> ColorComponents { + let f1 = (from.0 + 16.0) / 116.0; + let f0 = (from.1 / 500.0) + f1; + let f2 = f1 - from.2 / 200.0; + + let x = if f0.powf(3.0) > Self::EPSILON { + f0.powf(3.) + } else { + (116.0 * f0 - 16.0) / Self::KAPPA + }; + let y = if from.0 > Self::KAPPA * Self::EPSILON { + ((from.0 + 16.0) / 116.0).powf(3.0) + } else { + from.0 / Self::KAPPA + }; + let z = if f2.powf(3.0) > Self::EPSILON { + f2.powf(3.0) + } else { + (116.0 * f2 - 16.0) / Self::KAPPA + }; + + ColorComponents(x * Self::WHITE.0, y * Self::WHITE.1, z * Self::WHITE.2) + } + + /// Convert an XYZ colour to LAB as specified in [1] and [2]. + /// + /// [1]: https://drafts.csswg.org/css-color/#rgb-to-lab + /// [2]: https://drafts.csswg.org/css-color/#color-conversion-code + fn from_xyz(from: &ColorComponents) -> ColorComponents { + macro_rules! compute_f { + ($value:expr) => {{ + if $value > Self::EPSILON { + $value.cbrt() + } else { + (Self::KAPPA * $value + 16.0) / 116.0 + } + }}; + } + + // 4. Convert D50-adapted XYZ to Lab. + let f = [ + compute_f!(from.0 / Self::WHITE.0), + compute_f!(from.1 / Self::WHITE.1), + compute_f!(from.2 / Self::WHITE.2), + ]; + + let lightness = 116.0 * f[1] - 16.0; + let a = 500.0 * (f[0] - f[1]); + let b = 200.0 * (f[1] - f[2]); + + ColorComponents(lightness, a, b) + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + // No need for conversion. + from.clone() + } +} + +/// The Lch color space. +/// https://drafts.csswg.org/css-color-4/#specifying-lab-lch +pub struct Lch; + +impl ColorSpaceConversion for Lch { + const WHITE_POINT: WhitePoint = Lab::WHITE_POINT; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + // No need for conversion. + from.clone() + } + + fn to_xyz(from: &ColorComponents) -> ColorComponents { + // Convert LCH to Lab first. + let hue = from.2 * RAD_PER_DEG; + let a = from.1 * hue.cos(); + let b = from.1 * hue.sin(); + + let lab = ColorComponents(from.0, a, b); + + // Then convert the Lab to XYZ. + Lab::to_xyz(&lab) + } + + fn from_xyz(from: &ColorComponents) -> ColorComponents { + // First convert the XYZ to LAB. + let ColorComponents(lightness, a, b) = Lab::from_xyz(&from); + + // Then conver the Lab to LCH. + let hue = b.atan2(a) * DEG_PER_RAD; + let chroma = (a * a + b * b).sqrt(); + + ColorComponents(lightness, chroma, hue) + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + // No need for conversion. + from.clone() + } +} + +/// The Oklab color space. +/// https://drafts.csswg.org/css-color-4/#specifying-oklab-oklch +pub struct Oklab; + +impl Oklab { + #[rustfmt::skip] + const XYZ_TO_LMS: Transform = Transform::new( + 0.8190224432164319, 0.0329836671980271, 0.048177199566046255, 0.0, + 0.3619062562801221, 0.9292868468965546, 0.26423952494422764, 0.0, + -0.12887378261216414, 0.03614466816999844, 0.6335478258136937, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); + + #[rustfmt::skip] + const LMS_TO_OKLAB: Transform = Transform::new( + 0.2104542553, 1.9779984951, 0.0259040371, 0.0, + 0.7936177850, -2.4285922050, 0.7827717662, 0.0, + -0.0040720468, 0.4505937099, -0.8086757660, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); + + #[rustfmt::skip] + const LMS_TO_XYZ: Transform = Transform::new( + 1.2268798733741557, -0.04057576262431372, -0.07637294974672142, 0.0, + -0.5578149965554813, 1.1122868293970594, -0.4214933239627914, 0.0, + 0.28139105017721583, -0.07171106666151701, 1.5869240244272418, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); + + #[rustfmt::skip] + const OKLAB_TO_LMS: Transform = Transform::new( + 0.99999999845051981432, 1.0000000088817607767, 1.0000000546724109177, 0.0, + 0.39633779217376785678, -0.1055613423236563494, -0.089484182094965759684, 0.0, + 0.21580375806075880339, -0.063854174771705903402, -1.2914855378640917399, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); +} + +impl ColorSpaceConversion for Oklab { + const WHITE_POINT: WhitePoint = WhitePoint::D65; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + // No need for conversion. + from.clone() + } + + fn to_xyz(from: &ColorComponents) -> ColorComponents { + let lms = transform(&from, &Self::OKLAB_TO_LMS); + let lms = lms.map(|v| v.powf(3.0)); + transform(&lms, &Self::LMS_TO_XYZ) + } + + fn from_xyz(from: &ColorComponents) -> ColorComponents { + let lms = transform(&from, &Self::XYZ_TO_LMS); + let lms = lms.map(|v| v.cbrt()); + transform(&lms, &Self::LMS_TO_OKLAB) + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + // No need for conversion. + from.clone() + } +} + +/// The Oklch color space. +/// https://drafts.csswg.org/css-color-4/#specifying-oklab-oklch +pub struct Oklch; + +impl ColorSpaceConversion for Oklch { + const WHITE_POINT: WhitePoint = Oklab::WHITE_POINT; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + // No need for conversion. + from.clone() + } + + fn to_xyz(from: &ColorComponents) -> ColorComponents { + // First convert OkLCH to Oklab. + let hue = from.2 * RAD_PER_DEG; + let a = from.1 * hue.cos(); + let b = from.1 * hue.sin(); + let oklab = ColorComponents(from.0, a, b); + + // Then convert Oklab to XYZ. + Oklab::to_xyz(&oklab) + } + + fn from_xyz(from: &ColorComponents) -> ColorComponents { + // First convert XYZ to Oklab. + let ColorComponents(lightness, a, b) = Oklab::from_xyz(&from); + + // Then convert Oklab to OkLCH. + let hue = b.atan2(a) * DEG_PER_RAD; + let chroma = (a * a + b * b).sqrt(); + + ColorComponents(lightness, chroma, hue) + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + // No need for conversion. + from.clone() + } +} diff --git a/servo/components/style/color/mix.rs b/servo/components/style/color/mix.rs new file mode 100644 index 0000000000..455d025265 --- /dev/null +++ b/servo/components/style/color/mix.rs @@ -0,0 +1,475 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Color mixing/interpolation. + +use super::{AbsoluteColor, ColorComponents, ColorFlags, ColorSpace}; +use crate::parser::{Parse, ParserContext}; +use cssparser::Parser; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError, ToCss}; + +/// A hue-interpolation-method as defined in [1]. +/// +/// [1]: https://drafts.csswg.org/css-color-4/#typedef-hue-interpolation-method +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + ToAnimatedValue, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum HueInterpolationMethod { + /// https://drafts.csswg.org/css-color-4/#shorter + Shorter, + /// https://drafts.csswg.org/css-color-4/#longer + Longer, + /// https://drafts.csswg.org/css-color-4/#increasing + Increasing, + /// https://drafts.csswg.org/css-color-4/#decreasing + Decreasing, + /// https://drafts.csswg.org/css-color-4/#specified + Specified, +} + +/// https://drafts.csswg.org/css-color-4/#color-interpolation-method +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + ToShmem, + ToAnimatedValue, + ToComputedValue, + ToResolvedValue, +)] +#[repr(C)] +pub struct ColorInterpolationMethod { + /// The color-space the interpolation should be done in. + pub space: ColorSpace, + /// The hue interpolation method. + pub hue: HueInterpolationMethod, +} + +impl ColorInterpolationMethod { + /// Returns the srgb interpolation method. + pub const fn srgb() -> Self { + Self { + space: ColorSpace::Srgb, + hue: HueInterpolationMethod::Shorter, + } + } + + /// Return the oklab interpolation method used for default color + /// interpolcation. + pub const fn oklab() -> Self { + Self { + space: ColorSpace::Oklab, + hue: HueInterpolationMethod::Shorter, + } + } + + /// Decides the best method for interpolating between the given colors. + /// https://drafts.csswg.org/css-color-4/#interpolation-space + pub fn best_interpolation_between(left: &AbsoluteColor, right: &AbsoluteColor) -> Self { + // The preferred color space to use for interpolating colors is Oklab. + // However, if either of the colors are in legacy rgb(), hsl() or hwb(), + // then interpolation is done in sRGB. + if !left.is_legacy_color() || !right.is_legacy_color() { + Self::oklab() + } else { + Self::srgb() + } + } +} + +impl Parse for ColorInterpolationMethod { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + input.expect_ident_matching("in")?; + let space = ColorSpace::parse(input)?; + // https://drafts.csswg.org/css-color-4/#hue-interpolation + // Unless otherwise specified, if no specific hue interpolation + // algorithm is selected by the host syntax, the default is shorter. + let hue = if space.is_polar() { + input + .try_parse(|input| -> Result<_, ParseError<'i>> { + let hue = HueInterpolationMethod::parse(input)?; + input.expect_ident_matching("hue")?; + Ok(hue) + }) + .unwrap_or(HueInterpolationMethod::Shorter) + } else { + HueInterpolationMethod::Shorter + }; + Ok(Self { space, hue }) + } +} + +impl ToCss for ColorInterpolationMethod { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: Write, + { + dest.write_str("in ")?; + self.space.to_css(dest)?; + if self.hue != HueInterpolationMethod::Shorter { + dest.write_char(' ')?; + self.hue.to_css(dest)?; + dest.write_str(" hue")?; + } + Ok(()) + } +} + +/// Mix two colors into one. +pub fn mix( + interpolation: ColorInterpolationMethod, + left_color: &AbsoluteColor, + mut left_weight: f32, + right_color: &AbsoluteColor, + mut right_weight: f32, + normalize_weights: bool, +) -> AbsoluteColor { + // https://drafts.csswg.org/css-color-5/#color-mix-percent-norm + let mut alpha_multiplier = 1.0; + if normalize_weights { + let sum = left_weight + right_weight; + if sum != 1.0 { + let scale = 1.0 / sum; + left_weight *= scale; + right_weight *= scale; + if sum < 1.0 { + alpha_multiplier = sum; + } + } + } + + mix_in( + interpolation.space, + left_color, + left_weight, + right_color, + right_weight, + interpolation.hue, + alpha_multiplier, + ) +} + +/// What the outcome of each component should be in a mix result. +#[derive(Clone, Copy)] +#[repr(u8)] +enum ComponentMixOutcome { + /// Mix the left and right sides to give the result. + Mix, + /// Carry the left side forward to the result. + UseLeft, + /// Carry the right side forward to the result. + UseRight, + /// The resulting component should also be none. + None, +} + +impl ComponentMixOutcome { + fn from_colors( + left: &AbsoluteColor, + right: &AbsoluteColor, + flags_to_check: ColorFlags, + ) -> Self { + match ( + left.flags.contains(flags_to_check), + right.flags.contains(flags_to_check), + ) { + (true, true) => Self::None, + (true, false) => Self::UseRight, + (false, true) => Self::UseLeft, + (false, false) => Self::Mix, + } + } +} + +fn mix_in( + color_space: ColorSpace, + left_color: &AbsoluteColor, + left_weight: f32, + right_color: &AbsoluteColor, + right_weight: f32, + hue_interpolation: HueInterpolationMethod, + alpha_multiplier: f32, +) -> AbsoluteColor { + let outcomes = [ + ComponentMixOutcome::from_colors(left_color, right_color, ColorFlags::C1_IS_NONE), + ComponentMixOutcome::from_colors(left_color, right_color, ColorFlags::C2_IS_NONE), + ComponentMixOutcome::from_colors(left_color, right_color, ColorFlags::C3_IS_NONE), + ComponentMixOutcome::from_colors(left_color, right_color, ColorFlags::ALPHA_IS_NONE), + ]; + + // Convert both colors into the interpolation color space. + let left = left_color.to_color_space(color_space); + let left = left.raw_components(); + + let right = right_color.to_color_space(color_space); + let right = right.raw_components(); + + let (result, result_flags) = interpolate_premultiplied( + &left, + left_weight, + &right, + right_weight, + color_space.hue_index(), + hue_interpolation, + &outcomes, + ); + + let alpha = if alpha_multiplier != 1.0 { + result[3] * alpha_multiplier + } else { + result[3] + }; + + // FIXME: In rare cases we end up with 0.999995 in the alpha channel, + // so we reduce the precision to avoid serializing to + // rgba(?, ?, ?, 1). This is not ideal, so we should look into + // ways to avoid it. Maybe pre-multiply all color components and + // then divide after calculations? + let alpha = (alpha * 1000.0).round() / 1000.0; + + let mut result = AbsoluteColor::new( + color_space, + ColorComponents(result[0], result[1], result[2]), + alpha, + ); + + result.flags = result_flags; + // If both sides are legacy RGB, then the result stays in legacy RGB. + if !left_color.is_legacy_color() || !right_color.is_legacy_color() { + result.flags.insert(ColorFlags::AS_COLOR_FUNCTION); + } + + result +} + +fn interpolate_premultiplied_component( + left: f32, + left_weight: f32, + left_alpha: f32, + right: f32, + right_weight: f32, + right_alpha: f32, +) -> f32 { + left * left_weight * left_alpha + right * right_weight * right_alpha +} + +// Normalize hue into [0, 360) +#[inline] +fn normalize_hue(v: f32) -> f32 { + v - 360. * (v / 360.).floor() +} + +fn adjust_hue(left: &mut f32, right: &mut f32, hue_interpolation: HueInterpolationMethod) { + // Adjust the hue angle as per + // https://drafts.csswg.org/css-color/#hue-interpolation. + // + // If both hue angles are NAN, they should be set to 0. Otherwise, if a + // single hue angle is NAN, it should use the other hue angle. + if left.is_nan() { + if right.is_nan() { + *left = 0.; + *right = 0.; + } else { + *left = *right; + } + } else if right.is_nan() { + *right = *left; + } + + if hue_interpolation == HueInterpolationMethod::Specified { + // Angles are not adjusted. They are interpolated like any other + // component. + return; + } + + *left = normalize_hue(*left); + *right = normalize_hue(*right); + + match hue_interpolation { + // https://drafts.csswg.org/css-color/#shorter + HueInterpolationMethod::Shorter => { + let delta = *right - *left; + + if delta > 180. { + *left += 360.; + } else if delta < -180. { + *right += 360.; + } + }, + // https://drafts.csswg.org/css-color/#longer + HueInterpolationMethod::Longer => { + let delta = *right - *left; + if 0. < delta && delta < 180. { + *left += 360.; + } else if -180. < delta && delta < 0. { + *right += 360.; + } + }, + // https://drafts.csswg.org/css-color/#increasing + HueInterpolationMethod::Increasing => { + if *right < *left { + *right += 360.; + } + }, + // https://drafts.csswg.org/css-color/#decreasing + HueInterpolationMethod::Decreasing => { + if *left < *right { + *left += 360.; + } + }, + HueInterpolationMethod::Specified => unreachable!("Handled above"), + } +} + +fn interpolate_hue( + mut left: f32, + left_weight: f32, + mut right: f32, + right_weight: f32, + hue_interpolation: HueInterpolationMethod, +) -> f32 { + adjust_hue(&mut left, &mut right, hue_interpolation); + left * left_weight + right * right_weight +} + +struct InterpolatedAlpha { + /// The adjusted left alpha value. + left: f32, + /// The adjusted right alpha value. + right: f32, + /// The interpolated alpha value. + interpolated: f32, + /// Whether the alpha component should be `none`. + is_none: bool, +} + +fn interpolate_alpha( + left: f32, + left_weight: f32, + right: f32, + right_weight: f32, + outcome: ComponentMixOutcome, +) -> InterpolatedAlpha { + // + let mut result = match outcome { + ComponentMixOutcome::Mix => { + let interpolated = left * left_weight + right * right_weight; + InterpolatedAlpha { + left, + right, + interpolated, + is_none: false, + } + }, + ComponentMixOutcome::UseLeft => InterpolatedAlpha { + left, + right: left, + interpolated: left, + is_none: false, + }, + ComponentMixOutcome::UseRight => InterpolatedAlpha { + left: right, + right, + interpolated: right, + is_none: false, + }, + ComponentMixOutcome::None => InterpolatedAlpha { + left: 1.0, + right: 1.0, + interpolated: 0.0, + is_none: true, + }, + }; + + // Clip all alpha values to [0.0..1.0]. + result.left = result.left.clamp(0.0, 1.0); + result.right = result.right.clamp(0.0, 1.0); + result.interpolated = result.interpolated.clamp(0.0, 1.0); + + result +} + +fn interpolate_premultiplied( + left: &[f32; 4], + left_weight: f32, + right: &[f32; 4], + right_weight: f32, + hue_index: Option, + hue_interpolation: HueInterpolationMethod, + outcomes: &[ComponentMixOutcome; 4], +) -> ([f32; 4], ColorFlags) { + let alpha = interpolate_alpha(left[3], left_weight, right[3], right_weight, outcomes[3]); + let mut flags = if alpha.is_none { + ColorFlags::ALPHA_IS_NONE + } else { + ColorFlags::empty() + }; + + let mut result = [0.; 4]; + + for i in 0..3 { + match outcomes[i] { + ComponentMixOutcome::Mix => { + let is_hue = hue_index == Some(i); + result[i] = if is_hue { + normalize_hue(interpolate_hue( + left[i], + left_weight, + right[i], + right_weight, + hue_interpolation, + )) + } else { + let interpolated = interpolate_premultiplied_component( + left[i], + left_weight, + alpha.left, + right[i], + right_weight, + alpha.right, + ); + + if alpha.interpolated == 0.0 { + interpolated + } else { + interpolated / alpha.interpolated + } + }; + }, + ComponentMixOutcome::UseLeft => result[i] = left[i], + ComponentMixOutcome::UseRight => result[i] = right[i], + ComponentMixOutcome::None => { + result[i] = 0.0; + match i { + 0 => flags.insert(ColorFlags::C1_IS_NONE), + 1 => flags.insert(ColorFlags::C2_IS_NONE), + 2 => flags.insert(ColorFlags::C3_IS_NONE), + _ => unreachable!(), + } + }, + } + } + result[3] = alpha.interpolated; + + (result, flags) +} diff --git a/servo/components/style/color/mod.rs b/servo/components/style/color/mod.rs new file mode 100644 index 0000000000..f8ceee9703 --- /dev/null +++ b/servo/components/style/color/mod.rs @@ -0,0 +1,465 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Color support functions. + +/// cbindgen:ignore +pub mod convert; +pub mod mix; + +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ToCss}; + +/// The 3 components that make up a color. (Does not include the alpha component) +#[derive(Copy, Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] +#[repr(C)] +pub struct ColorComponents(pub f32, pub f32, pub f32); + +impl ColorComponents { + /// Apply a function to each of the 3 components of the color. + pub fn map(self, f: impl Fn(f32) -> f32) -> Self { + Self(f(self.0), f(self.1), f(self.2)) + } +} + +/// A color space representation in the CSS specification. +/// +/// https://drafts.csswg.org/css-color-4/#typedef-color-space +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + ToAnimatedValue, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum ColorSpace { + /// A color specified in the sRGB color space with either the rgb/rgba(..) + /// functions or the newer color(srgb ..) function. If the color(..) + /// function is used, the AS_COLOR_FUNCTION flag will be set. Examples: + /// "color(srgb 0.691 0.139 0.259)", "rgb(176, 35, 66)" + Srgb = 0, + /// A color specified in the Hsl notation in the sRGB color space, e.g. + /// "hsl(289.18 93.136% 65.531%)" + /// https://drafts.csswg.org/css-color-4/#the-hsl-notation + Hsl, + /// A color specified in the Hwb notation in the sRGB color space, e.g. + /// "hwb(740deg 20% 30%)" + /// https://drafts.csswg.org/css-color-4/#the-hwb-notation + Hwb, + /// A color specified in the Lab color format, e.g. + /// "lab(29.2345% 39.3825 20.0664)". + /// https://w3c.github.io/csswg-drafts/css-color-4/#lab-colors + Lab, + /// A color specified in the Lch color format, e.g. + /// "lch(29.2345% 44.2 27)". + /// https://w3c.github.io/csswg-drafts/css-color-4/#lch-colors + Lch, + /// A color specified in the Oklab color format, e.g. + /// "oklab(40.101% 0.1147 0.0453)". + /// https://w3c.github.io/csswg-drafts/css-color-4/#lab-colors + Oklab, + /// A color specified in the Oklch color format, e.g. + /// "oklch(40.101% 0.12332 21.555)". + /// https://w3c.github.io/csswg-drafts/css-color-4/#lch-colors + Oklch, + /// A color specified with the color(..) function and the "srgb-linear" + /// color space, e.g. "color(srgb-linear 0.435 0.017 0.055)". + SrgbLinear, + /// A color specified with the color(..) function and the "display-p3" + /// color space, e.g. "color(display-p3 0.84 0.19 0.72)". + DisplayP3, + /// A color specified with the color(..) function and the "a98-rgb" color + /// space, e.g. "color(a98-rgb 0.44091 0.49971 0.37408)". + A98Rgb, + /// A color specified with the color(..) function and the "prophoto-rgb" + /// color space, e.g. "color(prophoto-rgb 0.36589 0.41717 0.31333)". + ProphotoRgb, + /// A color specified with the color(..) function and the "rec2020" color + /// space, e.g. "color(rec2020 0.42210 0.47580 0.35605)". + Rec2020, + /// A color specified with the color(..) function and the "xyz-d50" color + /// space, e.g. "color(xyz-d50 0.2005 0.14089 0.4472)". + XyzD50, + /// A color specified with the color(..) function and the "xyz-d65" or "xyz" + /// color space, e.g. "color(xyz-d65 0.21661 0.14602 0.59452)". + /// NOTE: https://drafts.csswg.org/css-color-4/#resolving-color-function-values + /// specifies that `xyz` is an alias for the `xyz-d65` color space. + #[parse(aliases = "xyz")] + XyzD65, +} + +impl ColorSpace { + /// Returns whether this is a ``. + #[inline] + pub fn is_rectangular(&self) -> bool { + !self.is_polar() + } + + /// Returns whether this is a ``. + #[inline] + pub fn is_polar(&self) -> bool { + matches!(self, Self::Hsl | Self::Hwb | Self::Lch | Self::Oklch) + } + + /// Returns an index of the hue component in the color space, otherwise + /// `None`. + #[inline] + pub fn hue_index(&self) -> Option { + match self { + Self::Hsl | Self::Hwb => Some(0), + Self::Lch | Self::Oklch => Some(2), + + _ => { + debug_assert!(!self.is_polar()); + None + }, + } + } +} + +bitflags! { + /// Flags used when serializing colors. + #[derive(Clone, Copy, Default, MallocSizeOf, PartialEq, ToShmem)] + #[repr(C)] + pub struct ColorFlags : u8 { + /// If set, serializes sRGB colors into `color(srgb ...)` instead of + /// `rgba(...)`. + const AS_COLOR_FUNCTION = 1 << 0; + /// Whether the 1st color component is `none`. + const C1_IS_NONE = 1 << 1; + /// Whether the 2nd color component is `none`. + const C2_IS_NONE = 1 << 2; + /// Whether the 3rd color component is `none`. + const C3_IS_NONE = 1 << 3; + /// Whether the alpha component is `none`. + const ALPHA_IS_NONE = 1 << 4; + } +} + +/// An absolutely specified color, using either rgb(), rgba(), lab(), lch(), +/// oklab(), oklch() or color(). +#[derive(Copy, Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] +#[repr(C)] +pub struct AbsoluteColor { + /// The 3 components that make up colors in any color space. + pub components: ColorComponents, + /// The alpha component of the color. + pub alpha: f32, + /// The current color space that the components represent. + pub color_space: ColorSpace, + /// Extra flags used durring serialization of this color. + pub flags: ColorFlags, +} + +/// Given an [`AbsoluteColor`], return the 4 float components as the type given, +/// e.g.: +/// +/// ```rust +/// let srgb = AbsoluteColor::new(ColorSpace::Srgb, 1.0, 0.0, 0.0, 0.0); +/// let floats = color_components_as!(&srgb, [f32; 4]); // [1.0, 0.0, 0.0, 0.0] +/// ``` +macro_rules! color_components_as { + ($c:expr, $t:ty) => {{ + // This macro is not an inline function, because we can't use the + // generic type ($t) in a constant expression as per: + // https://github.com/rust-lang/rust/issues/76560 + const_assert_eq!(std::mem::size_of::<$t>(), std::mem::size_of::<[f32; 4]>()); + const_assert_eq!(std::mem::align_of::<$t>(), std::mem::align_of::<[f32; 4]>()); + const_assert!(std::mem::size_of::() >= std::mem::size_of::<$t>()); + const_assert_eq!( + std::mem::align_of::(), + std::mem::align_of::<$t>() + ); + + std::mem::transmute::<&ColorComponents, &$t>(&$c.components) + }}; +} + +impl AbsoluteColor { + /// Create a new [`AbsoluteColor`] with the given [`ColorSpace`] and + /// components. + pub fn new(color_space: ColorSpace, components: ColorComponents, alpha: f32) -> Self { + let mut components = components; + + // Lightness must not be less than 0. + if matches!( + color_space, + ColorSpace::Lab | ColorSpace::Lch | ColorSpace::Oklab | ColorSpace::Oklch + ) { + components.0 = components.0.max(0.0); + } + + // Chroma must not be less than 0. + if matches!(color_space, ColorSpace::Lch | ColorSpace::Oklch) { + components.1 = components.1.max(0.0); + } + + Self { + components, + alpha: alpha.clamp(0.0, 1.0), + color_space, + flags: ColorFlags::empty(), + } + } + + /// Create a new [`AbsoluteColor`] from rgba values in the sRGB color space. + pub fn srgb(red: f32, green: f32, blue: f32, alpha: f32) -> Self { + Self::new(ColorSpace::Srgb, ColorComponents(red, green, blue), alpha) + } + + /// Create a new transparent color. + pub fn transparent() -> Self { + Self::srgb(0.0, 0.0, 0.0, 0.0) + } + + /// Create a new opaque black color. + pub fn black() -> Self { + Self::srgb(0.0, 0.0, 0.0, 1.0) + } + + /// Create a new opaque white color. + pub fn white() -> Self { + Self::srgb(1.0, 1.0, 1.0, 1.0) + } + + /// Return all the components of the color in an array. (Includes alpha) + #[inline] + pub fn raw_components(&self) -> &[f32; 4] { + unsafe { color_components_as!(self, [f32; 4]) } + } + + /// Returns true if this color is in one of the legacy color formats. + #[inline] + pub fn is_legacy_color(&self) -> bool { + // rgb(), rgba(), hsl(), hsla(), hwb(), hwba() + match self.color_space { + ColorSpace::Srgb => !self.flags.contains(ColorFlags::AS_COLOR_FUNCTION), + ColorSpace::Hsl | ColorSpace::Hwb => true, + _ => false, + } + } + + /// Return the alpha component. + #[inline] + pub fn alpha(&self) -> f32 { + self.alpha + } + + /// Convert this color to the specified color space. + pub fn to_color_space(&self, color_space: ColorSpace) -> Self { + use ColorSpace::*; + + if self.color_space == color_space { + return self.clone(); + } + + // We have simplified conversions that do not need to convert to XYZ + // first. This improves performance, because it skips 2 matrix + // multiplications and reduces float rounding errors. + match (self.color_space, color_space) { + (Srgb, Hsl) => { + return Self::new( + color_space, + convert::rgb_to_hsl(&self.components), + self.alpha, + ); + }, + + (Srgb, Hwb) => { + return Self::new( + color_space, + convert::rgb_to_hwb(&self.components), + self.alpha, + ); + }, + + (Hsl, Srgb) => { + return Self::new( + color_space, + convert::hsl_to_rgb(&self.components), + self.alpha, + ); + }, + + (Hwb, Srgb) => { + return Self::new( + color_space, + convert::hwb_to_rgb(&self.components), + self.alpha, + ); + }, + + (Lab, Lch) | (Oklab, Oklch) => { + return Self::new( + color_space, + convert::lab_to_lch(&self.components), + self.alpha, + ); + }, + + (Lch, Lab) | (Oklch, Oklab) => { + return Self::new( + color_space, + convert::lch_to_lab(&self.components), + self.alpha, + ); + }, + + _ => {}, + } + + let (xyz, white_point) = match self.color_space { + Lab => convert::to_xyz::(&self.components), + Lch => convert::to_xyz::(&self.components), + Oklab => convert::to_xyz::(&self.components), + Oklch => convert::to_xyz::(&self.components), + Srgb => convert::to_xyz::(&self.components), + Hsl => convert::to_xyz::(&self.components), + Hwb => convert::to_xyz::(&self.components), + SrgbLinear => convert::to_xyz::(&self.components), + DisplayP3 => convert::to_xyz::(&self.components), + A98Rgb => convert::to_xyz::(&self.components), + ProphotoRgb => convert::to_xyz::(&self.components), + Rec2020 => convert::to_xyz::(&self.components), + XyzD50 => convert::to_xyz::(&self.components), + XyzD65 => convert::to_xyz::(&self.components), + }; + + let result = match color_space { + Lab => convert::from_xyz::(&xyz, white_point), + Lch => convert::from_xyz::(&xyz, white_point), + Oklab => convert::from_xyz::(&xyz, white_point), + Oklch => convert::from_xyz::(&xyz, white_point), + Srgb => convert::from_xyz::(&xyz, white_point), + Hsl => convert::from_xyz::(&xyz, white_point), + Hwb => convert::from_xyz::(&xyz, white_point), + SrgbLinear => convert::from_xyz::(&xyz, white_point), + DisplayP3 => convert::from_xyz::(&xyz, white_point), + A98Rgb => convert::from_xyz::(&xyz, white_point), + ProphotoRgb => convert::from_xyz::(&xyz, white_point), + Rec2020 => convert::from_xyz::(&xyz, white_point), + XyzD50 => convert::from_xyz::(&xyz, white_point), + XyzD65 => convert::from_xyz::(&xyz, white_point), + }; + + Self::new(color_space, result, self.alpha) + } +} + +impl From for ColorSpace { + fn from(value: cssparser::PredefinedColorSpace) -> Self { + match value { + cssparser::PredefinedColorSpace::Srgb => ColorSpace::Srgb, + cssparser::PredefinedColorSpace::SrgbLinear => ColorSpace::SrgbLinear, + cssparser::PredefinedColorSpace::DisplayP3 => ColorSpace::DisplayP3, + cssparser::PredefinedColorSpace::A98Rgb => ColorSpace::A98Rgb, + cssparser::PredefinedColorSpace::ProphotoRgb => ColorSpace::ProphotoRgb, + cssparser::PredefinedColorSpace::Rec2020 => ColorSpace::Rec2020, + cssparser::PredefinedColorSpace::XyzD50 => ColorSpace::XyzD50, + cssparser::PredefinedColorSpace::XyzD65 => ColorSpace::XyzD65, + } + } +} + +impl ToCss for AbsoluteColor { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: Write, + { + macro_rules! value_or_none { + ($v:expr,$flag:tt) => {{ + if self.flags.contains(ColorFlags::$flag) { + None + } else { + Some($v) + } + }}; + } + + let maybe_c1 = value_or_none!(self.components.0, C1_IS_NONE); + let maybe_c2 = value_or_none!(self.components.1, C2_IS_NONE); + let maybe_c3 = value_or_none!(self.components.2, C3_IS_NONE); + let maybe_alpha = value_or_none!(self.alpha, ALPHA_IS_NONE); + + match self.color_space { + ColorSpace::Hsl => { + let rgb = convert::hsl_to_rgb(&self.components); + Self::new(ColorSpace::Srgb, rgb, self.alpha).to_css(dest) + }, + + ColorSpace::Hwb => { + let rgb = convert::hwb_to_rgb(&self.components); + + Self::new(ColorSpace::Srgb, rgb, self.alpha).to_css(dest) + }, + + ColorSpace::Srgb if !self.flags.contains(ColorFlags::AS_COLOR_FUNCTION) => { + // Althought we are passing Option<_> in here, the to_css fn + // knows that the "none" keyword is not supported in the + // rgb/rgba legacy syntax. + cssparser::ToCss::to_css( + &cssparser::RGBA::from_floats(maybe_c1, maybe_c2, maybe_c3, maybe_alpha), + dest, + ) + }, + ColorSpace::Lab => cssparser::ToCss::to_css( + &cssparser::Lab::new(maybe_c1, maybe_c2, maybe_c3, maybe_alpha), + dest, + ), + ColorSpace::Lch => cssparser::ToCss::to_css( + &cssparser::Lch::new(maybe_c1, maybe_c2, maybe_c3, maybe_alpha), + dest, + ), + ColorSpace::Oklab => cssparser::ToCss::to_css( + &cssparser::Oklab::new(maybe_c1, maybe_c2, maybe_c3, maybe_alpha), + dest, + ), + ColorSpace::Oklch => cssparser::ToCss::to_css( + &cssparser::Oklch::new(maybe_c1, maybe_c2, maybe_c3, maybe_alpha), + dest, + ), + _ => { + let color_space = match self.color_space { + ColorSpace::Srgb => { + debug_assert!( + self.flags.contains(ColorFlags::AS_COLOR_FUNCTION), + "The case without this flag should be handled in the wrapping match case!!" + ); + + cssparser::PredefinedColorSpace::Srgb + }, + ColorSpace::SrgbLinear => cssparser::PredefinedColorSpace::SrgbLinear, + ColorSpace::DisplayP3 => cssparser::PredefinedColorSpace::DisplayP3, + ColorSpace::A98Rgb => cssparser::PredefinedColorSpace::A98Rgb, + ColorSpace::ProphotoRgb => cssparser::PredefinedColorSpace::ProphotoRgb, + ColorSpace::Rec2020 => cssparser::PredefinedColorSpace::Rec2020, + ColorSpace::XyzD50 => cssparser::PredefinedColorSpace::XyzD50, + ColorSpace::XyzD65 => cssparser::PredefinedColorSpace::XyzD65, + + _ => { + unreachable!("other color spaces do not support color() syntax") + }, + }; + + let color_function = cssparser::ColorFunction { + color_space, + c1: maybe_c1, + c2: maybe_c2, + c3: maybe_c3, + alpha: maybe_alpha, + }; + let color = cssparser::Color::ColorFunction(color_function); + cssparser::ToCss::to_css(&color, dest) + }, + } + } +} diff --git a/servo/components/style/context.rs b/servo/components/style/context.rs new file mode 100644 index 0000000000..f38963539b --- /dev/null +++ b/servo/components/style/context.rs @@ -0,0 +1,698 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! The context within which style is calculated. + +#[cfg(feature = "servo")] +use crate::animation::DocumentAnimationSet; +use crate::bloom::StyleBloom; +use crate::computed_value_flags::ComputedValueFlags; +use crate::data::{EagerPseudoStyles, ElementData}; +use crate::dom::{SendElement, TElement}; +#[cfg(feature = "gecko")] +use crate::gecko_bindings::structs; +use crate::parallel::{STACK_SAFETY_MARGIN_KB, STYLE_THREAD_STACK_SIZE_KB}; +use crate::properties::ComputedValues; +#[cfg(feature = "servo")] +use crate::properties::PropertyId; +use crate::rule_cache::RuleCache; +use crate::rule_tree::StrongRuleNode; +use crate::selector_parser::{SnapshotMap, EAGER_PSEUDO_COUNT}; +use crate::shared_lock::StylesheetGuards; +use crate::sharing::StyleSharingCache; +use crate::stylist::Stylist; +use crate::thread_state::{self, ThreadState}; +use crate::traversal::DomTraversal; +use crate::traversal_flags::TraversalFlags; +use app_units::Au; +use euclid::default::Size2D; +use euclid::Scale; +#[cfg(feature = "servo")] +use fxhash::FxHashMap; +use selectors::NthIndexCache; +#[cfg(feature = "gecko")] +use servo_arc::Arc; +#[cfg(feature = "servo")] +use servo_atoms::Atom; +use std::fmt; +use std::ops; +use style_traits::CSSPixel; +use style_traits::DevicePixel; +#[cfg(feature = "servo")] +use style_traits::SpeculativePainter; +use time; + +pub use selectors::matching::QuirksMode; + +/// A global options structure for the style system. We use this instead of +/// opts to abstract across Gecko and Servo. +#[derive(Clone)] +pub struct StyleSystemOptions { + /// Whether the style sharing cache is disabled. + pub disable_style_sharing_cache: bool, + /// Whether we should dump statistics about the style system. + pub dump_style_statistics: bool, + /// The minimum number of elements that must be traversed to trigger a dump + /// of style statistics. + pub style_statistics_threshold: usize, +} + +#[cfg(feature = "gecko")] +fn get_env_bool(name: &str) -> bool { + use std::env; + match env::var(name) { + Ok(s) => !s.is_empty(), + Err(_) => false, + } +} + +const DEFAULT_STATISTICS_THRESHOLD: usize = 50; + +#[cfg(feature = "gecko")] +fn get_env_usize(name: &str) -> Option { + use std::env; + env::var(name).ok().map(|s| { + s.parse::() + .expect("Couldn't parse environmental variable as usize") + }) +} + +/// A global variable holding the state of +/// `StyleSystemOptions::default().disable_style_sharing_cache`. +/// See [#22854](https://github.com/servo/servo/issues/22854). +#[cfg(feature = "servo")] +pub static DEFAULT_DISABLE_STYLE_SHARING_CACHE: std::sync::atomic::AtomicBool = + std::sync::atomic::AtomicBool::new(false); + +/// A global variable holding the state of +/// `StyleSystemOptions::default().dump_style_statistics`. +/// See [#22854](https://github.com/servo/servo/issues/22854). +#[cfg(feature = "servo")] +pub static DEFAULT_DUMP_STYLE_STATISTICS: std::sync::atomic::AtomicBool = + std::sync::atomic::AtomicBool::new(false); + +impl Default for StyleSystemOptions { + #[cfg(feature = "servo")] + fn default() -> Self { + use std::sync::atomic::Ordering; + + StyleSystemOptions { + disable_style_sharing_cache: DEFAULT_DISABLE_STYLE_SHARING_CACHE + .load(Ordering::Relaxed), + dump_style_statistics: DEFAULT_DUMP_STYLE_STATISTICS.load(Ordering::Relaxed), + style_statistics_threshold: DEFAULT_STATISTICS_THRESHOLD, + } + } + + #[cfg(feature = "gecko")] + fn default() -> Self { + StyleSystemOptions { + disable_style_sharing_cache: get_env_bool("DISABLE_STYLE_SHARING_CACHE"), + dump_style_statistics: get_env_bool("DUMP_STYLE_STATISTICS"), + style_statistics_threshold: get_env_usize("STYLE_STATISTICS_THRESHOLD") + .unwrap_or(DEFAULT_STATISTICS_THRESHOLD), + } + } +} + +/// A shared style context. +/// +/// There's exactly one of these during a given restyle traversal, and it's +/// shared among the worker threads. +pub struct SharedStyleContext<'a> { + /// The CSS selector stylist. + pub stylist: &'a Stylist, + + /// Whether visited styles are enabled. + /// + /// They may be disabled when Gecko's pref layout.css.visited_links_enabled + /// is false, or when in private browsing mode. + pub visited_styles_enabled: bool, + + /// Configuration options. + pub options: StyleSystemOptions, + + /// Guards for pre-acquired locks + pub guards: StylesheetGuards<'a>, + + /// The current time for transitions and animations. This is needed to ensure + /// a consistent sampling time and also to adjust the time for testing. + pub current_time_for_animations: f64, + + /// Flags controlling how we traverse the tree. + pub traversal_flags: TraversalFlags, + + /// A map with our snapshots in order to handle restyle hints. + pub snapshot_map: &'a SnapshotMap, + + /// The state of all animations for our styled elements. + #[cfg(feature = "servo")] + pub animations: DocumentAnimationSet, + + /// Paint worklets + #[cfg(feature = "servo")] + pub registered_speculative_painters: &'a dyn RegisteredSpeculativePainters, +} + +impl<'a> SharedStyleContext<'a> { + /// Return a suitable viewport size in order to be used for viewport units. + pub fn viewport_size(&self) -> Size2D { + self.stylist.device().au_viewport_size() + } + + /// The device pixel ratio + pub fn device_pixel_ratio(&self) -> Scale { + self.stylist.device().device_pixel_ratio() + } + + /// The quirks mode of the document. + pub fn quirks_mode(&self) -> QuirksMode { + self.stylist.quirks_mode() + } +} + +/// The structure holds various intermediate inputs that are eventually used by +/// by the cascade. +/// +/// The matching and cascading process stores them in this format temporarily +/// within the `CurrentElementInfo`. At the end of the cascade, they are folded +/// down into the main `ComputedValues` to reduce memory usage per element while +/// still remaining accessible. +#[derive(Clone, Debug, Default)] +pub struct CascadeInputs { + /// The rule node representing the ordered list of rules matched for this + /// node. + pub rules: Option, + + /// The rule node representing the ordered list of rules matched for this + /// node if visited, only computed if there's a relevant link for this + /// element. A element's "relevant link" is the element being matched if it + /// is a link or the nearest ancestor link. + pub visited_rules: Option, + + /// The set of flags from container queries that we need for invalidation. + pub flags: ComputedValueFlags, +} + +impl CascadeInputs { + /// Construct inputs from previous cascade results, if any. + pub fn new_from_style(style: &ComputedValues) -> Self { + Self { + rules: style.rules.clone(), + visited_rules: style.visited_style().and_then(|v| v.rules.clone()), + flags: style.flags.for_cascade_inputs(), + } + } +} + +/// A list of cascade inputs for eagerly-cascaded pseudo-elements. +/// The list is stored inline. +#[derive(Debug)] +pub struct EagerPseudoCascadeInputs(Option<[Option; EAGER_PSEUDO_COUNT]>); + +// Manually implement `Clone` here because the derived impl of `Clone` for +// array types assumes the value inside is `Copy`. +impl Clone for EagerPseudoCascadeInputs { + fn clone(&self) -> Self { + if self.0.is_none() { + return EagerPseudoCascadeInputs(None); + } + let self_inputs = self.0.as_ref().unwrap(); + let mut inputs: [Option; EAGER_PSEUDO_COUNT] = Default::default(); + for i in 0..EAGER_PSEUDO_COUNT { + inputs[i] = self_inputs[i].clone(); + } + EagerPseudoCascadeInputs(Some(inputs)) + } +} + +impl EagerPseudoCascadeInputs { + /// Construct inputs from previous cascade results, if any. + fn new_from_style(styles: &EagerPseudoStyles) -> Self { + EagerPseudoCascadeInputs(styles.as_optional_array().map(|styles| { + let mut inputs: [Option; EAGER_PSEUDO_COUNT] = Default::default(); + for i in 0..EAGER_PSEUDO_COUNT { + inputs[i] = styles[i].as_ref().map(|s| CascadeInputs::new_from_style(s)); + } + inputs + })) + } + + /// Returns the list of rules, if they exist. + pub fn into_array(self) -> Option<[Option; EAGER_PSEUDO_COUNT]> { + self.0 + } +} + +/// The cascade inputs associated with a node, including those for any +/// pseudo-elements. +/// +/// The matching and cascading process stores them in this format temporarily +/// within the `CurrentElementInfo`. At the end of the cascade, they are folded +/// down into the main `ComputedValues` to reduce memory usage per element while +/// still remaining accessible. +#[derive(Clone, Debug)] +pub struct ElementCascadeInputs { + /// The element's cascade inputs. + pub primary: CascadeInputs, + /// A list of the inputs for the element's eagerly-cascaded pseudo-elements. + pub pseudos: EagerPseudoCascadeInputs, +} + +impl ElementCascadeInputs { + /// Construct inputs from previous cascade results, if any. + #[inline] + pub fn new_from_element_data(data: &ElementData) -> Self { + debug_assert!(data.has_styles()); + ElementCascadeInputs { + primary: CascadeInputs::new_from_style(data.styles.primary()), + pseudos: EagerPseudoCascadeInputs::new_from_style(&data.styles.pseudos), + } + } +} + +/// Statistics gathered during the traversal. We gather statistics on each +/// thread and then combine them after the threads join via the Add +/// implementation below. +#[derive(AddAssign, Clone, Default)] +pub struct PerThreadTraversalStatistics { + /// The total number of elements traversed. + pub elements_traversed: u32, + /// The number of elements where has_styles() went from false to true. + pub elements_styled: u32, + /// The number of elements for which we performed selector matching. + pub elements_matched: u32, + /// The number of cache hits from the StyleSharingCache. + pub styles_shared: u32, + /// The number of styles reused via rule node comparison from the + /// StyleSharingCache. + pub styles_reused: u32, +} + +/// Statistics gathered during the traversal plus some information from +/// other sources including stylist. +#[derive(Default)] +pub struct TraversalStatistics { + /// Aggregated statistics gathered during the traversal. + pub aggregated: PerThreadTraversalStatistics, + /// The number of selectors in the stylist. + pub selectors: u32, + /// The number of revalidation selectors. + pub revalidation_selectors: u32, + /// The number of state/attr dependencies in the dependency set. + pub dependency_selectors: u32, + /// The number of declarations in the stylist. + pub declarations: u32, + /// The number of times the stylist was rebuilt. + pub stylist_rebuilds: u32, + /// Time spent in the traversal, in milliseconds. + pub traversal_time_ms: f64, + /// Whether this was a parallel traversal. + pub is_parallel: bool, + /// Whether this is a "large" traversal. + pub is_large: bool, +} + +/// Format the statistics in a way that the performance test harness understands. +/// See https://bugzilla.mozilla.org/show_bug.cgi?id=1331856#c2 +impl fmt::Display for TraversalStatistics { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + debug_assert!( + self.traversal_time_ms != 0.0, + "should have set traversal time" + ); + writeln!(f, "[PERF] perf block start")?; + writeln!( + f, + "[PERF],traversal,{}", + if self.is_parallel { + "parallel" + } else { + "sequential" + } + )?; + writeln!( + f, + "[PERF],elements_traversed,{}", + self.aggregated.elements_traversed + )?; + writeln!( + f, + "[PERF],elements_styled,{}", + self.aggregated.elements_styled + )?; + writeln!( + f, + "[PERF],elements_matched,{}", + self.aggregated.elements_matched + )?; + writeln!(f, "[PERF],styles_shared,{}", self.aggregated.styles_shared)?; + writeln!(f, "[PERF],styles_reused,{}", self.aggregated.styles_reused)?; + writeln!(f, "[PERF],selectors,{}", self.selectors)?; + writeln!( + f, + "[PERF],revalidation_selectors,{}", + self.revalidation_selectors + )?; + writeln!( + f, + "[PERF],dependency_selectors,{}", + self.dependency_selectors + )?; + writeln!(f, "[PERF],declarations,{}", self.declarations)?; + writeln!(f, "[PERF],stylist_rebuilds,{}", self.stylist_rebuilds)?; + writeln!(f, "[PERF],traversal_time_ms,{}", self.traversal_time_ms)?; + writeln!(f, "[PERF] perf block end") + } +} + +impl TraversalStatistics { + /// Generate complete traversal statistics. + /// + /// The traversal time is computed given the start time in seconds. + pub fn new( + aggregated: PerThreadTraversalStatistics, + traversal: &D, + parallel: bool, + start: f64, + ) -> TraversalStatistics + where + E: TElement, + D: DomTraversal, + { + let threshold = traversal + .shared_context() + .options + .style_statistics_threshold; + let stylist = traversal.shared_context().stylist; + let is_large = aggregated.elements_traversed as usize >= threshold; + TraversalStatistics { + aggregated, + selectors: stylist.num_selectors() as u32, + revalidation_selectors: stylist.num_revalidation_selectors() as u32, + dependency_selectors: stylist.num_invalidations() as u32, + declarations: stylist.num_declarations() as u32, + stylist_rebuilds: stylist.num_rebuilds() as u32, + traversal_time_ms: (time::precise_time_s() - start) * 1000.0, + is_parallel: parallel, + is_large, + } + } +} + +#[cfg(feature = "gecko")] +bitflags! { + /// Represents which tasks are performed in a SequentialTask of + /// UpdateAnimations which is a result of normal restyle. + pub struct UpdateAnimationsTasks: u8 { + /// Update CSS Animations. + const CSS_ANIMATIONS = structs::UpdateAnimationsTasks_CSSAnimations; + /// Update CSS Transitions. + const CSS_TRANSITIONS = structs::UpdateAnimationsTasks_CSSTransitions; + /// Update effect properties. + const EFFECT_PROPERTIES = structs::UpdateAnimationsTasks_EffectProperties; + /// Update animation cacade results for animations running on the compositor. + const CASCADE_RESULTS = structs::UpdateAnimationsTasks_CascadeResults; + /// Display property was changed from none. + /// Script animations keep alive on display:none elements, so we need to trigger + /// the second animation restyles for the script animations in the case where + /// the display property was changed from 'none' to others. + const DISPLAY_CHANGED_FROM_NONE = structs::UpdateAnimationsTasks_DisplayChangedFromNone; + /// Update CSS named scroll progress timelines. + const SCROLL_TIMELINES = structs::UpdateAnimationsTasks_ScrollTimelines; + /// Update CSS named view progress timelines. + const VIEW_TIMELINES = structs::UpdateAnimationsTasks_ViewTimelines; + } +} + +#[cfg(feature = "gecko")] +bitflags! { + /// Represents which tasks are performed in a SequentialTask as a result of + /// animation-only restyle. + pub struct PostAnimationTasks: u8 { + /// Display property was changed from none in animation-only restyle so + /// that we need to resolve styles for descendants in a subsequent + /// normal restyle. + const DISPLAY_CHANGED_FROM_NONE_FOR_SMIL = 0x01; + } +} + +/// A task to be run in sequential mode on the parent (non-worker) thread. This +/// is used by the style system to queue up work which is not safe to do during +/// the parallel traversal. +pub enum SequentialTask { + /// Entry to avoid an unused type parameter error on servo. + Unused(SendElement), + + /// Performs one of a number of possible tasks related to updating + /// animations based on the |tasks| field. These include updating CSS + /// animations/transitions that changed as part of the non-animation style + /// traversal, and updating the computed effect properties. + #[cfg(feature = "gecko")] + UpdateAnimations { + /// The target element or pseudo-element. + el: SendElement, + /// The before-change style for transitions. We use before-change style + /// as the initial value of its Keyframe. Required if |tasks| includes + /// CSSTransitions. + before_change_style: Option>, + /// The tasks which are performed in this SequentialTask. + tasks: UpdateAnimationsTasks, + }, + + /// Performs one of a number of possible tasks as a result of animation-only + /// restyle. + /// + /// Currently we do only process for resolving descendant elements that were + /// display:none subtree for SMIL animation. + #[cfg(feature = "gecko")] + PostAnimation { + /// The target element. + el: SendElement, + /// The tasks which are performed in this SequentialTask. + tasks: PostAnimationTasks, + }, +} + +impl SequentialTask { + /// Executes this task. + pub fn execute(self) { + use self::SequentialTask::*; + debug_assert_eq!(thread_state::get(), ThreadState::LAYOUT); + match self { + Unused(_) => unreachable!(), + #[cfg(feature = "gecko")] + UpdateAnimations { + el, + before_change_style, + tasks, + } => { + el.update_animations(before_change_style, tasks); + }, + #[cfg(feature = "gecko")] + PostAnimation { el, tasks } => { + el.process_post_animation(tasks); + }, + } + } + + /// Creates a task to update various animation-related state on a given + /// (pseudo-)element. + #[cfg(feature = "gecko")] + pub fn update_animations( + el: E, + before_change_style: Option>, + tasks: UpdateAnimationsTasks, + ) -> Self { + use self::SequentialTask::*; + UpdateAnimations { + el: unsafe { SendElement::new(el) }, + before_change_style, + tasks, + } + } + + /// Creates a task to do post-process for a given element as a result of + /// animation-only restyle. + #[cfg(feature = "gecko")] + pub fn process_post_animation(el: E, tasks: PostAnimationTasks) -> Self { + use self::SequentialTask::*; + PostAnimation { + el: unsafe { SendElement::new(el) }, + tasks, + } + } +} + +/// A list of SequentialTasks that get executed on Drop. +pub struct SequentialTaskList(Vec>) +where + E: TElement; + +impl ops::Deref for SequentialTaskList +where + E: TElement, +{ + type Target = Vec>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl ops::DerefMut for SequentialTaskList +where + E: TElement, +{ + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Drop for SequentialTaskList +where + E: TElement, +{ + fn drop(&mut self) { + debug_assert_eq!(thread_state::get(), ThreadState::LAYOUT); + for task in self.0.drain(..) { + task.execute() + } + } +} + +/// A helper type for stack limit checking. This assumes that stacks grow +/// down, which is true for all non-ancient CPU architectures. +pub struct StackLimitChecker { + lower_limit: usize, +} + +impl StackLimitChecker { + /// Create a new limit checker, for this thread, allowing further use + /// of up to |stack_size| bytes beyond (below) the current stack pointer. + #[inline(never)] + pub fn new(stack_size_limit: usize) -> Self { + StackLimitChecker { + lower_limit: StackLimitChecker::get_sp() - stack_size_limit, + } + } + + /// Checks whether the previously stored stack limit has now been exceeded. + #[inline(never)] + pub fn limit_exceeded(&self) -> bool { + let curr_sp = StackLimitChecker::get_sp(); + + // Do some sanity-checking to ensure that our invariants hold, even in + // the case where we've exceeded the soft limit. + // + // The correctness of depends on the assumption that no stack wraps + // around the end of the address space. + if cfg!(debug_assertions) { + // Compute the actual bottom of the stack by subtracting our safety + // margin from our soft limit. Note that this will be slightly below + // the actual bottom of the stack, because there are a few initial + // frames on the stack before we do the measurement that computes + // the limit. + let stack_bottom = self.lower_limit - STACK_SAFETY_MARGIN_KB * 1024; + + // The bottom of the stack should be below the current sp. If it + // isn't, that means we've either waited too long to check the limit + // and burned through our safety margin (in which case we probably + // would have segfaulted by now), or we're using a limit computed for + // a different thread. + debug_assert!(stack_bottom < curr_sp); + + // Compute the distance between the current sp and the bottom of + // the stack, and compare it against the current stack. It should be + // no further from us than the total stack size. We allow some slop + // to handle the fact that stack_bottom is a bit further than the + // bottom of the stack, as discussed above. + let distance_to_stack_bottom = curr_sp - stack_bottom; + let max_allowable_distance = (STYLE_THREAD_STACK_SIZE_KB + 10) * 1024; + debug_assert!(distance_to_stack_bottom <= max_allowable_distance); + } + + // The actual bounds check. + curr_sp <= self.lower_limit + } + + // Technically, rustc can optimize this away, but shouldn't for now. + // We should fix this once black_box is stable. + #[inline(always)] + fn get_sp() -> usize { + let mut foo: usize = 42; + (&mut foo as *mut usize) as usize + } +} + +/// A thread-local style context. +/// +/// This context contains data that needs to be used during restyling, but is +/// not required to be unique among worker threads, so we create one per worker +/// thread in order to be able to mutate it without locking. +pub struct ThreadLocalStyleContext { + /// A cache to share style among siblings. + pub sharing_cache: StyleSharingCache, + /// A cache from matched properties to elements that match those. + pub rule_cache: RuleCache, + /// The bloom filter used to fast-reject selector-matching. + pub bloom_filter: StyleBloom, + /// A set of tasks to be run (on the parent thread) in sequential mode after + /// the rest of the styling is complete. This is useful for + /// infrequently-needed non-threadsafe operations. + /// + /// It's important that goes after the style sharing cache and the bloom + /// filter, to ensure they're dropped before we execute the tasks, which + /// could create another ThreadLocalStyleContext for style computation. + pub tasks: SequentialTaskList, + /// Statistics about the traversal. + pub statistics: PerThreadTraversalStatistics, + /// A checker used to ensure that parallel.rs does not recurse indefinitely + /// even on arbitrarily deep trees. See Gecko bug 1376883. + pub stack_limit_checker: StackLimitChecker, + /// A cache for nth-index-like selectors. + pub nth_index_cache: NthIndexCache, +} + +impl ThreadLocalStyleContext { + /// Creates a new `ThreadLocalStyleContext` + pub fn new() -> Self { + ThreadLocalStyleContext { + sharing_cache: StyleSharingCache::new(), + rule_cache: RuleCache::new(), + bloom_filter: StyleBloom::new(), + tasks: SequentialTaskList(Vec::new()), + statistics: PerThreadTraversalStatistics::default(), + stack_limit_checker: StackLimitChecker::new( + (STYLE_THREAD_STACK_SIZE_KB - STACK_SAFETY_MARGIN_KB) * 1024, + ), + nth_index_cache: NthIndexCache::default(), + } + } +} + +/// A `StyleContext` is just a simple container for a immutable reference to a +/// shared style context, and a mutable reference to a local one. +pub struct StyleContext<'a, E: TElement + 'a> { + /// The shared style context reference. + pub shared: &'a SharedStyleContext<'a>, + /// The thread-local style context (mutable) reference. + pub thread_local: &'a mut ThreadLocalStyleContext, +} + +/// A registered painter +#[cfg(feature = "servo")] +pub trait RegisteredSpeculativePainter: SpeculativePainter { + /// The name it was registered with + fn name(&self) -> Atom; + /// The properties it was registered with + fn properties(&self) -> &FxHashMap; +} + +/// A set of registered painters +#[cfg(feature = "servo")] +pub trait RegisteredSpeculativePainters: Sync { + /// Look up a speculative painter + fn get(&self, name: &Atom) -> Option<&dyn RegisteredSpeculativePainter>; +} diff --git a/servo/components/style/counter_style/mod.rs b/servo/components/style/counter_style/mod.rs new file mode 100644 index 0000000000..65143d6990 --- /dev/null +++ b/servo/components/style/counter_style/mod.rs @@ -0,0 +1,697 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! The [`@counter-style`][counter-style] at-rule. +//! +//! [counter-style]: https://drafts.csswg.org/css-counter-styles/ + +use crate::error_reporting::ContextualParseError; +use crate::parser::{Parse, ParserContext}; +use crate::shared_lock::{SharedRwLockReadGuard, ToCssWithGuard}; +use crate::str::CssStringWriter; +use crate::values::specified::Integer; +use crate::values::CustomIdent; +use crate::Atom; +use cssparser::{ + AtRuleParser, DeclarationParser, QualifiedRuleParser, RuleBodyItemParser, RuleBodyParser, +}; +use cssparser::{CowRcStr, Parser, SourceLocation, Token}; +use selectors::parser::SelectorParseErrorKind; +use std::fmt::{self, Write}; +use std::mem; +use std::num::Wrapping; +use style_traits::{Comma, CssWriter, OneOrMoreSeparated, ParseError}; +use style_traits::{StyleParseErrorKind, ToCss}; + +/// Parse a counter style name reference. +/// +/// This allows the reserved counter style names "decimal" and "disc". +pub fn parse_counter_style_name<'i, 't>( + input: &mut Parser<'i, 't>, +) -> Result> { + macro_rules! predefined { + ($($name: expr,)+) => { + { + ascii_case_insensitive_phf_map! { + // FIXME: use static atoms https://github.com/rust-lang/rust/issues/33156 + predefined -> &'static str = { + $( + $name => $name, + )+ + } + } + + let location = input.current_source_location(); + let ident = input.expect_ident()?; + if let Some(&lower_cased) = predefined(&ident) { + Ok(CustomIdent(Atom::from(lower_cased))) + } else { + // none is always an invalid value. + CustomIdent::from_ident(location, ident, &["none"]) + } + } + } + } + include!("predefined.rs") +} + +fn is_valid_name_definition(ident: &CustomIdent) -> bool { + ident.0 != atom!("decimal") && + ident.0 != atom!("disc") && + ident.0 != atom!("circle") && + ident.0 != atom!("square") && + ident.0 != atom!("disclosure-closed") && + ident.0 != atom!("disclosure-open") +} + +/// Parse the prelude of an @counter-style rule +pub fn parse_counter_style_name_definition<'i, 't>( + input: &mut Parser<'i, 't>, +) -> Result> { + parse_counter_style_name(input).and_then(|ident| { + if !is_valid_name_definition(&ident) { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } else { + Ok(ident) + } + }) +} + +/// Parse the body (inside `{}`) of an @counter-style rule +pub fn parse_counter_style_body<'i, 't>( + name: CustomIdent, + context: &ParserContext, + input: &mut Parser<'i, 't>, + location: SourceLocation, +) -> Result> { + let start = input.current_source_location(); + let mut rule = CounterStyleRuleData::empty(name, location); + { + let mut parser = CounterStyleRuleParser { + context, + rule: &mut rule, + }; + let mut iter = RuleBodyParser::new(input, &mut parser); + while let Some(declaration) = iter.next() { + if let Err((error, slice)) = declaration { + let location = error.location; + let error = ContextualParseError::UnsupportedCounterStyleDescriptorDeclaration( + slice, error, + ); + context.log_css_error(location, error) + } + } + } + let error = match *rule.resolved_system() { + ref system @ System::Cyclic | + ref system @ System::Fixed { .. } | + ref system @ System::Symbolic | + ref system @ System::Alphabetic | + ref system @ System::Numeric + if rule.symbols.is_none() => + { + let system = system.to_css_string(); + Some(ContextualParseError::InvalidCounterStyleWithoutSymbols( + system, + )) + }, + ref system @ System::Alphabetic | ref system @ System::Numeric + if rule.symbols().unwrap().0.len() < 2 => + { + let system = system.to_css_string(); + Some(ContextualParseError::InvalidCounterStyleNotEnoughSymbols( + system, + )) + }, + System::Additive if rule.additive_symbols.is_none() => { + Some(ContextualParseError::InvalidCounterStyleWithoutAdditiveSymbols) + }, + System::Extends(_) if rule.symbols.is_some() => { + Some(ContextualParseError::InvalidCounterStyleExtendsWithSymbols) + }, + System::Extends(_) if rule.additive_symbols.is_some() => { + Some(ContextualParseError::InvalidCounterStyleExtendsWithAdditiveSymbols) + }, + _ => None, + }; + if let Some(error) = error { + context.log_css_error(start, error); + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } else { + Ok(rule) + } +} + +struct CounterStyleRuleParser<'a, 'b: 'a> { + context: &'a ParserContext<'b>, + rule: &'a mut CounterStyleRuleData, +} + +/// Default methods reject all at rules. +impl<'a, 'b, 'i> AtRuleParser<'i> for CounterStyleRuleParser<'a, 'b> { + type Prelude = (); + type AtRule = (); + type Error = StyleParseErrorKind<'i>; +} + +impl<'a, 'b, 'i> QualifiedRuleParser<'i> for CounterStyleRuleParser<'a, 'b> { + type Prelude = (); + type QualifiedRule = (); + type Error = StyleParseErrorKind<'i>; +} + +impl<'a, 'b, 'i> RuleBodyItemParser<'i, (), StyleParseErrorKind<'i>> + for CounterStyleRuleParser<'a, 'b> +{ + fn parse_qualified(&self) -> bool { + false + } + fn parse_declarations(&self) -> bool { + true + } +} + +macro_rules! checker { + ($self:ident._($value:ident)) => {}; + ($self:ident. $checker:ident($value:ident)) => { + if !$self.$checker(&$value) { + return false; + } + }; +} + +macro_rules! counter_style_descriptors { + ( + $( #[$doc: meta] $name: tt $ident: ident / $setter: ident [$checker: tt]: $ty: ty, )+ + ) => { + /// An @counter-style rule + #[derive(Clone, Debug, ToShmem)] + pub struct CounterStyleRuleData { + name: CustomIdent, + generation: Wrapping, + $( + #[$doc] + $ident: Option<$ty>, + )+ + /// Line and column of the @counter-style rule source code. + pub source_location: SourceLocation, + } + + impl CounterStyleRuleData { + fn empty(name: CustomIdent, source_location: SourceLocation) -> Self { + CounterStyleRuleData { + name: name, + generation: Wrapping(0), + $( + $ident: None, + )+ + source_location, + } + } + + $( + #[$doc] + pub fn $ident(&self) -> Option<&$ty> { + self.$ident.as_ref() + } + )+ + + $( + #[$doc] + pub fn $setter(&mut self, value: $ty) -> bool { + checker!(self.$checker(value)); + self.$ident = Some(value); + self.generation += Wrapping(1); + true + } + )+ + } + + impl<'a, 'b, 'i> DeclarationParser<'i> for CounterStyleRuleParser<'a, 'b> { + type Declaration = (); + type Error = StyleParseErrorKind<'i>; + + fn parse_value<'t>( + &mut self, + name: CowRcStr<'i>, + input: &mut Parser<'i, 't>, + ) -> Result<(), ParseError<'i>> { + match_ignore_ascii_case! { &*name, + $( + $name => { + // DeclarationParser also calls parse_entirely so we’d normally not + // need to, but in this case we do because we set the value as a side + // effect rather than returning it. + let value = input.parse_entirely(|i| Parse::parse(self.context, i))?; + self.rule.$ident = Some(value) + }, + )* + _ => return Err(input.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))), + } + Ok(()) + } + } + + impl ToCssWithGuard for CounterStyleRuleData { + fn to_css(&self, _guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result { + dest.write_str("@counter-style ")?; + self.name.to_css(&mut CssWriter::new(dest))?; + dest.write_str(" { ")?; + $( + if let Some(ref value) = self.$ident { + dest.write_str(concat!($name, ": "))?; + ToCss::to_css(value, &mut CssWriter::new(dest))?; + dest.write_str("; ")?; + } + )+ + dest.write_char('}') + } + } + } +} + +counter_style_descriptors! { + /// + "system" system / set_system [check_system]: System, + + /// + "negative" negative / set_negative [_]: Negative, + + /// + "prefix" prefix / set_prefix [_]: Symbol, + + /// + "suffix" suffix / set_suffix [_]: Symbol, + + /// + "range" range / set_range [_]: CounterRanges, + + /// + "pad" pad / set_pad [_]: Pad, + + /// + "fallback" fallback / set_fallback [_]: Fallback, + + /// + "symbols" symbols / set_symbols [check_symbols]: Symbols, + + /// + "additive-symbols" additive_symbols / + set_additive_symbols [check_additive_symbols]: AdditiveSymbols, + + /// + "speak-as" speak_as / set_speak_as [_]: SpeakAs, +} + +// Implements the special checkers for some setters. +// See +impl CounterStyleRuleData { + /// Check that the system is effectively not changed. Only params + /// of system descriptor is changeable. + fn check_system(&self, value: &System) -> bool { + mem::discriminant(self.resolved_system()) == mem::discriminant(value) + } + + fn check_symbols(&self, value: &Symbols) -> bool { + match *self.resolved_system() { + // These two systems require at least two symbols. + System::Numeric | System::Alphabetic => value.0.len() >= 2, + // No symbols should be set for extends system. + System::Extends(_) => false, + _ => true, + } + } + + fn check_additive_symbols(&self, _value: &AdditiveSymbols) -> bool { + match *self.resolved_system() { + // No additive symbols should be set for extends system. + System::Extends(_) => false, + _ => true, + } + } +} + +impl CounterStyleRuleData { + /// Get the name of the counter style rule. + pub fn name(&self) -> &CustomIdent { + &self.name + } + + /// Set the name of the counter style rule. Caller must ensure that + /// the name is valid. + pub fn set_name(&mut self, name: CustomIdent) { + debug_assert!(is_valid_name_definition(&name)); + self.name = name; + } + + /// Get the current generation of the counter style rule. + pub fn generation(&self) -> u32 { + self.generation.0 + } + + /// Get the system of this counter style rule, default to + /// `symbolic` if not specified. + pub fn resolved_system(&self) -> &System { + match self.system { + Some(ref system) => system, + None => &System::Symbolic, + } + } +} + +/// +#[derive(Clone, Debug, ToShmem)] +pub enum System { + /// 'cyclic' + Cyclic, + /// 'numeric' + Numeric, + /// 'alphabetic' + Alphabetic, + /// 'symbolic' + Symbolic, + /// 'additive' + Additive, + /// 'fixed ?' + Fixed { + /// '?' + first_symbol_value: Option, + }, + /// 'extends ' + Extends(CustomIdent), +} + +impl Parse for System { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + try_match_ident_ignore_ascii_case! { input, + "cyclic" => Ok(System::Cyclic), + "numeric" => Ok(System::Numeric), + "alphabetic" => Ok(System::Alphabetic), + "symbolic" => Ok(System::Symbolic), + "additive" => Ok(System::Additive), + "fixed" => { + let first_symbol_value = input.try_parse(|i| Integer::parse(context, i)).ok(); + Ok(System::Fixed { first_symbol_value }) + }, + "extends" => { + let other = parse_counter_style_name(input)?; + Ok(System::Extends(other)) + }, + } + } +} + +impl ToCss for System { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: Write, + { + match *self { + System::Cyclic => dest.write_str("cyclic"), + System::Numeric => dest.write_str("numeric"), + System::Alphabetic => dest.write_str("alphabetic"), + System::Symbolic => dest.write_str("symbolic"), + System::Additive => dest.write_str("additive"), + System::Fixed { first_symbol_value } => { + if let Some(value) = first_symbol_value { + dest.write_str("fixed ")?; + value.to_css(dest) + } else { + dest.write_str("fixed") + } + }, + System::Extends(ref other) => { + dest.write_str("extends ")?; + other.to_css(dest) + }, + } + } +} + +/// +#[derive( + Clone, Debug, Eq, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToCss, ToShmem, +)] +#[repr(u8)] +pub enum Symbol { + /// + String(crate::OwnedStr), + /// + Ident(CustomIdent), + // Not implemented: + // /// + // Image(Image), +} + +impl Parse for Symbol { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + let location = input.current_source_location(); + match *input.next()? { + Token::QuotedString(ref s) => Ok(Symbol::String(s.as_ref().to_owned().into())), + Token::Ident(ref s) => Ok(Symbol::Ident(CustomIdent::from_ident(location, s, &[])?)), + ref t => Err(location.new_unexpected_token_error(t.clone())), + } + } +} + +impl Symbol { + /// Returns whether this symbol is allowed in symbols() function. + pub fn is_allowed_in_symbols(&self) -> bool { + match self { + // Identifier is not allowed. + &Symbol::Ident(_) => false, + _ => true, + } + } +} + +/// +#[derive(Clone, Debug, ToCss, ToShmem)] +pub struct Negative(pub Symbol, pub Option); + +impl Parse for Negative { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + Ok(Negative( + Symbol::parse(context, input)?, + input.try_parse(|input| Symbol::parse(context, input)).ok(), + )) + } +} + +/// +#[derive(Clone, Debug, ToCss, ToShmem)] +pub struct CounterRange { + /// The start of the range. + pub start: CounterBound, + /// The end of the range. + pub end: CounterBound, +} + +/// +/// +/// Empty represents 'auto' +#[derive(Clone, Debug, ToCss, ToShmem)] +#[css(comma)] +pub struct CounterRanges(#[css(iterable, if_empty = "auto")] pub crate::OwnedSlice); + +/// A bound found in `CounterRanges`. +#[derive(Clone, Copy, Debug, ToCss, ToShmem)] +pub enum CounterBound { + /// An integer bound. + Integer(Integer), + /// The infinite bound. + Infinite, +} + +impl Parse for CounterRanges { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + if input + .try_parse(|input| input.expect_ident_matching("auto")) + .is_ok() + { + return Ok(CounterRanges(Default::default())); + } + + let ranges = input.parse_comma_separated(|input| { + let start = parse_bound(context, input)?; + let end = parse_bound(context, input)?; + if let (CounterBound::Integer(start), CounterBound::Integer(end)) = (start, end) { + if start > end { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + } + Ok(CounterRange { start, end }) + })?; + + Ok(CounterRanges(ranges.into())) + } +} + +fn parse_bound<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, +) -> Result> { + if let Ok(integer) = input.try_parse(|input| Integer::parse(context, input)) { + return Ok(CounterBound::Integer(integer)); + } + input.expect_ident_matching("infinite")?; + Ok(CounterBound::Infinite) +} + +/// +#[derive(Clone, Debug, ToCss, ToShmem)] +pub struct Pad(pub Integer, pub Symbol); + +impl Parse for Pad { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + let pad_with = input.try_parse(|input| Symbol::parse(context, input)); + let min_length = Integer::parse_non_negative(context, input)?; + let pad_with = pad_with.or_else(|_| Symbol::parse(context, input))?; + Ok(Pad(min_length, pad_with)) + } +} + +/// +#[derive(Clone, Debug, ToCss, ToShmem)] +pub struct Fallback(pub CustomIdent); + +impl Parse for Fallback { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + Ok(Fallback(parse_counter_style_name(input)?)) + } +} + +/// +#[derive( + Clone, Debug, Eq, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToCss, ToShmem, +)] +#[repr(C)] +pub struct Symbols(#[css(iterable)] pub crate::OwnedSlice); + +impl Parse for Symbols { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + let mut symbols = Vec::new(); + while let Ok(s) = input.try_parse(|input| Symbol::parse(context, input)) { + symbols.push(s); + } + if symbols.is_empty() { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + Ok(Symbols(symbols.into())) + } +} + +/// +#[derive(Clone, Debug, ToCss, ToShmem)] +#[css(comma)] +pub struct AdditiveSymbols(#[css(iterable)] pub crate::OwnedSlice); + +impl Parse for AdditiveSymbols { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + let tuples = Vec::::parse(context, input)?; + // FIXME maybe? https://github.com/w3c/csswg-drafts/issues/1220 + if tuples + .windows(2) + .any(|window| window[0].weight <= window[1].weight) + { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + Ok(AdditiveSymbols(tuples.into())) + } +} + +/// && +#[derive(Clone, Debug, ToCss, ToShmem)] +pub struct AdditiveTuple { + /// + pub weight: Integer, + /// + pub symbol: Symbol, +} + +impl OneOrMoreSeparated for AdditiveTuple { + type S = Comma; +} + +impl Parse for AdditiveTuple { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + let symbol = input.try_parse(|input| Symbol::parse(context, input)); + let weight = Integer::parse_non_negative(context, input)?; + let symbol = symbol.or_else(|_| Symbol::parse(context, input))?; + Ok(Self { weight, symbol }) + } +} + +/// +#[derive(Clone, Debug, ToCss, ToShmem)] +pub enum SpeakAs { + /// auto + Auto, + /// bullets + Bullets, + /// numbers + Numbers, + /// words + Words, + // /// spell-out, not supported, see bug 1024178 + // SpellOut, + /// + Other(CustomIdent), +} + +impl Parse for SpeakAs { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + let mut is_spell_out = false; + let result = input.try_parse(|input| { + let ident = input.expect_ident().map_err(|_| ())?; + match_ignore_ascii_case! { &*ident, + "auto" => Ok(SpeakAs::Auto), + "bullets" => Ok(SpeakAs::Bullets), + "numbers" => Ok(SpeakAs::Numbers), + "words" => Ok(SpeakAs::Words), + "spell-out" => { + is_spell_out = true; + Err(()) + }, + _ => Err(()), + } + }); + if is_spell_out { + // spell-out is not supported, but don’t parse it as a . + // See bug 1024178. + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + result.or_else(|_| Ok(SpeakAs::Other(parse_counter_style_name(input)?))) + } +} diff --git a/servo/components/style/counter_style/predefined.rs b/servo/components/style/counter_style/predefined.rs new file mode 100644 index 0000000000..7243e3b3f3 --- /dev/null +++ b/servo/components/style/counter_style/predefined.rs @@ -0,0 +1,61 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +predefined! { + "decimal", + "decimal-leading-zero", + "arabic-indic", + "armenian", + "upper-armenian", + "lower-armenian", + "bengali", + "cambodian", + "khmer", + "cjk-decimal", + "devanagari", + "georgian", + "gujarati", + "gurmukhi", + "hebrew", + "kannada", + "lao", + "malayalam", + "mongolian", + "myanmar", + "oriya", + "persian", + "lower-roman", + "upper-roman", + "tamil", + "telugu", + "thai", + "tibetan", + "lower-alpha", + "lower-latin", + "upper-alpha", + "upper-latin", + "cjk-earthly-branch", + "cjk-heavenly-stem", + "lower-greek", + "hiragana", + "hiragana-iroha", + "katakana", + "katakana-iroha", + "disc", + "circle", + "square", + "disclosure-open", + "disclosure-closed", + "japanese-informal", + "japanese-formal", + "korean-hangul-formal", + "korean-hanja-informal", + "korean-hanja-formal", + "simp-chinese-informal", + "simp-chinese-formal", + "trad-chinese-informal", + "trad-chinese-formal", + "cjk-ideographic", + "ethiopic-numeric", +} diff --git a/servo/components/style/counter_style/update_predefined.py b/servo/components/style/counter_style/update_predefined.py new file mode 100755 index 0000000000..1523958ff3 --- /dev/null +++ b/servo/components/style/counter_style/update_predefined.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python + +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import os.path +import re +import urllib + + +def main(filename): + names = [ + re.search('>([^>]+)(| VariableValue; + +struct EnvironmentVariable { + name: Atom, + evaluator: EnvironmentEvaluator, +} + +macro_rules! make_variable { + ($name:expr, $evaluator:expr) => {{ + EnvironmentVariable { + name: $name, + evaluator: $evaluator, + } + }}; +} + +fn get_safearea_inset_top(device: &Device) -> VariableValue { + VariableValue::pixels(device.safe_area_insets().top) +} + +fn get_safearea_inset_bottom(device: &Device) -> VariableValue { + VariableValue::pixels(device.safe_area_insets().bottom) +} + +fn get_safearea_inset_left(device: &Device) -> VariableValue { + VariableValue::pixels(device.safe_area_insets().left) +} + +fn get_safearea_inset_right(device: &Device) -> VariableValue { + VariableValue::pixels(device.safe_area_insets().right) +} + +fn get_content_preferred_color_scheme(device: &Device) -> VariableValue { + use crate::gecko::media_features::PrefersColorScheme; + let prefers_color_scheme = unsafe { + crate::gecko_bindings::bindings::Gecko_MediaFeatures_PrefersColorScheme( + device.document(), + /* use_content = */ true, + ) + }; + VariableValue::ident(match prefers_color_scheme { + PrefersColorScheme::Light => "light", + PrefersColorScheme::Dark => "dark", + }) +} + +fn get_scrollbar_inline_size(device: &Device) -> VariableValue { + VariableValue::pixels(device.scrollbar_inline_size().px()) +} + +static ENVIRONMENT_VARIABLES: [EnvironmentVariable; 4] = [ + make_variable!(atom!("safe-area-inset-top"), get_safearea_inset_top), + make_variable!(atom!("safe-area-inset-bottom"), get_safearea_inset_bottom), + make_variable!(atom!("safe-area-inset-left"), get_safearea_inset_left), + make_variable!(atom!("safe-area-inset-right"), get_safearea_inset_right), +]; + +macro_rules! lnf_int { + ($id:ident) => { + unsafe { + crate::gecko_bindings::bindings::Gecko_GetLookAndFeelInt( + crate::gecko_bindings::bindings::LookAndFeel_IntID::$id as i32, + ) + } + }; +} + +macro_rules! lnf_int_variable { + ($atom:expr, $id:ident, $ctor:ident) => {{ + fn __eval(_: &Device) -> VariableValue { + VariableValue::$ctor(lnf_int!($id)) + } + make_variable!($atom, __eval) + }}; +} + +static CHROME_ENVIRONMENT_VARIABLES: [EnvironmentVariable; 6] = [ + lnf_int_variable!( + atom!("-moz-gtk-csd-titlebar-radius"), + TitlebarRadius, + int_pixels + ), + lnf_int_variable!( + atom!("-moz-gtk-csd-close-button-position"), + GTKCSDCloseButtonPosition, + integer + ), + lnf_int_variable!( + atom!("-moz-gtk-csd-minimize-button-position"), + GTKCSDMinimizeButtonPosition, + integer + ), + lnf_int_variable!( + atom!("-moz-gtk-csd-maximize-button-position"), + GTKCSDMaximizeButtonPosition, + integer + ), + make_variable!( + atom!("-moz-content-preferred-color-scheme"), + get_content_preferred_color_scheme + ), + make_variable!(atom!("scrollbar-inline-size"), get_scrollbar_inline_size), +]; + +impl CssEnvironment { + #[inline] + fn get(&self, name: &Atom, device: &Device) -> Option { + if let Some(var) = ENVIRONMENT_VARIABLES.iter().find(|var| var.name == *name) { + return Some((var.evaluator)(device)); + } + if !device.chrome_rules_enabled_for_document() { + return None; + } + let var = CHROME_ENVIRONMENT_VARIABLES + .iter() + .find(|var| var.name == *name)?; + Some((var.evaluator)(device)) + } +} + +/// A custom property name is just an `Atom`. +/// +/// Note that this does not include the `--` prefix +pub type Name = Atom; + +/// Parse a custom property name. +/// +/// +pub fn parse_name(s: &str) -> Result<&str, ()> { + if s.starts_with("--") && s.len() > 2 { + Ok(&s[2..]) + } else { + Err(()) + } +} + +/// A value for a custom property is just a set of tokens. +/// +/// We preserve the original CSS for serialization, and also the variable +/// references to other custom property names. +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] +pub struct VariableValue { + css: String, + + first_token_type: TokenSerializationType, + last_token_type: TokenSerializationType, + + /// Whether a variable value has a reference to an environment variable. + /// + /// If this is the case, we need to perform variable substitution on the + /// value. + references_environment: bool, + + /// Custom property names in var() functions. + references: Box<[Name]>, +} + +impl ToCss for SpecifiedValue { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: Write, + { + dest.write_str(&self.css) + } +} + +/// A map from CSS variable names to CSS variable computed values, used for +/// resolving. +/// +/// A consistent ordering is required for CSSDeclaration objects in the +/// DOM. CSSDeclarations expose property names as indexed properties, which +/// need to be stable. So we keep an array of property names which order is +/// determined on the order that they are added to the name-value map. +/// +/// The variable values are guaranteed to not have references to other +/// properties. +pub type CustomPropertiesMap = + IndexMap, BuildHasherDefault>; + +/// Both specified and computed values are VariableValues, the difference is +/// whether var() functions are expanded. +pub type SpecifiedValue = VariableValue; +/// Both specified and computed values are VariableValues, the difference is +/// whether var() functions are expanded. +pub type ComputedValue = VariableValue; + +/// A struct holding information about the external references to that a custom +/// property value may have. +#[derive(Default)] +struct VarOrEnvReferences { + custom_property_references: PrecomputedHashSet, + references_environment: bool, +} + +impl VariableValue { + fn empty() -> Self { + Self { + css: String::new(), + last_token_type: TokenSerializationType::nothing(), + first_token_type: TokenSerializationType::nothing(), + references: Default::default(), + references_environment: false, + } + } + + fn push<'i>( + &mut self, + input: &Parser<'i, '_>, + css: &str, + css_first_token_type: TokenSerializationType, + css_last_token_type: TokenSerializationType, + ) -> Result<(), ParseError<'i>> { + /// Prevent values from getting terribly big since you can use custom + /// properties exponentially. + /// + /// This number (2MB) is somewhat arbitrary, but silly enough that no + /// reasonable page should hit it. We could limit by number of total + /// substitutions, but that was very easy to work around in practice + /// (just choose a larger initial value and boom). + const MAX_VALUE_LENGTH_IN_BYTES: usize = 2 * 1024 * 1024; + + if self.css.len() + css.len() > MAX_VALUE_LENGTH_IN_BYTES { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + // This happens e.g. between two subsequent var() functions: + // `var(--a)var(--b)`. + // + // In that case, css_*_token_type is nonsensical. + if css.is_empty() { + return Ok(()); + } + + self.first_token_type.set_if_nothing(css_first_token_type); + // If self.first_token_type was nothing, + // self.last_token_type is also nothing and this will be false: + if self + .last_token_type + .needs_separator_when_before(css_first_token_type) + { + self.css.push_str("/**/") + } + self.css.push_str(css); + self.last_token_type = css_last_token_type; + Ok(()) + } + + fn push_from<'i>( + &mut self, + input: &Parser<'i, '_>, + position: (SourcePosition, TokenSerializationType), + last_token_type: TokenSerializationType, + ) -> Result<(), ParseError<'i>> { + self.push( + input, + input.slice_from(position.0), + position.1, + last_token_type, + ) + } + + fn push_variable<'i>( + &mut self, + input: &Parser<'i, '_>, + variable: &ComputedValue, + ) -> Result<(), ParseError<'i>> { + debug_assert!(variable.references.is_empty()); + self.push( + input, + &variable.css, + variable.first_token_type, + variable.last_token_type, + ) + } + + /// Parse a custom property value. + pub fn parse<'i, 't>(input: &mut Parser<'i, 't>) -> Result, ParseError<'i>> { + let mut references = VarOrEnvReferences::default(); + + let (first_token_type, css, last_token_type) = + parse_self_contained_declaration_value(input, Some(&mut references))?; + + let custom_property_references = references + .custom_property_references + .into_iter() + .collect::>() + .into_boxed_slice(); + + let mut css = css.into_owned(); + css.shrink_to_fit(); + + Ok(Arc::new(VariableValue { + css, + first_token_type, + last_token_type, + references: custom_property_references, + references_environment: references.references_environment, + })) + } + + /// Create VariableValue from an int. + fn integer(number: i32) -> Self { + Self::from_token(Token::Number { + has_sign: false, + value: number as f32, + int_value: Some(number), + }) + } + + /// Create VariableValue from an int. + fn ident(ident: &'static str) -> Self { + Self::from_token(Token::Ident(ident.into())) + } + + /// Create VariableValue from a float amount of CSS pixels. + fn pixels(number: f32) -> Self { + // FIXME (https://github.com/servo/rust-cssparser/issues/266): + // No way to get TokenSerializationType::Dimension without creating + // Token object. + Self::from_token(Token::Dimension { + has_sign: false, + value: number, + int_value: None, + unit: CowRcStr::from("px"), + }) + } + + /// Create VariableValue from an integer amount of CSS pixels. + fn int_pixels(number: i32) -> Self { + Self::from_token(Token::Dimension { + has_sign: false, + value: number as f32, + int_value: Some(number), + unit: CowRcStr::from("px"), + }) + } + + fn from_token(token: Token) -> Self { + let token_type = token.serialization_type(); + let mut css = token.to_css_string(); + css.shrink_to_fit(); + + VariableValue { + css, + first_token_type: token_type, + last_token_type: token_type, + references: Default::default(), + references_environment: false, + } + } +} + +/// Parse the value of a non-custom property that contains `var()` references. +pub fn parse_non_custom_with_var<'i, 't>( + input: &mut Parser<'i, 't>, +) -> Result<(TokenSerializationType, Cow<'i, str>), ParseError<'i>> { + let (first_token_type, css, _) = parse_self_contained_declaration_value(input, None)?; + Ok((first_token_type, css)) +} + +fn parse_self_contained_declaration_value<'i, 't>( + input: &mut Parser<'i, 't>, + references: Option<&mut VarOrEnvReferences>, +) -> Result<(TokenSerializationType, Cow<'i, str>, TokenSerializationType), ParseError<'i>> { + let start_position = input.position(); + let mut missing_closing_characters = String::new(); + let (first, last) = + parse_declaration_value(input, references, &mut missing_closing_characters)?; + let mut css: Cow = input.slice_from(start_position).into(); + if !missing_closing_characters.is_empty() { + // Unescaped backslash at EOF in a quoted string is ignored. + if css.ends_with("\\") && matches!(missing_closing_characters.as_bytes()[0], b'"' | b'\'') { + css.to_mut().pop(); + } + css.to_mut().push_str(&missing_closing_characters); + } + Ok((first, css, last)) +} + +/// +fn parse_declaration_value<'i, 't>( + input: &mut Parser<'i, 't>, + references: Option<&mut VarOrEnvReferences>, + missing_closing_characters: &mut String, +) -> Result<(TokenSerializationType, TokenSerializationType), ParseError<'i>> { + input.parse_until_before(Delimiter::Bang | Delimiter::Semicolon, |input| { + parse_declaration_value_block(input, references, missing_closing_characters) + }) +} + +/// Like parse_declaration_value, but accept `!` and `;` since they are only +/// invalid at the top level +fn parse_declaration_value_block<'i, 't>( + input: &mut Parser<'i, 't>, + mut references: Option<&mut VarOrEnvReferences>, + missing_closing_characters: &mut String, +) -> Result<(TokenSerializationType, TokenSerializationType), ParseError<'i>> { + input.skip_whitespace(); + let mut token_start = input.position(); + let mut token = match input.next_including_whitespace_and_comments() { + Ok(token) => token, + Err(_) => { + return Ok(( + TokenSerializationType::nothing(), + TokenSerializationType::nothing(), + )); + }, + }; + let first_token_type = token.serialization_type(); + loop { + macro_rules! nested { + () => { + input.parse_nested_block(|input| { + parse_declaration_value_block( + input, + references.as_mut().map(|r| &mut **r), + missing_closing_characters, + ) + })? + }; + } + macro_rules! check_closed { + ($closing:expr) => { + if !input.slice_from(token_start).ends_with($closing) { + missing_closing_characters.push_str($closing) + } + }; + } + let last_token_type = match *token { + Token::Comment(_) => { + let serialization_type = token.serialization_type(); + let token_slice = input.slice_from(token_start); + if !token_slice.ends_with("*/") { + missing_closing_characters.push_str(if token_slice.ends_with('*') { + "/" + } else { + "*/" + }) + } + serialization_type + }, + Token::BadUrl(ref u) => { + let e = StyleParseErrorKind::BadUrlInDeclarationValueBlock(u.clone()); + return Err(input.new_custom_error(e)); + }, + Token::BadString(ref s) => { + let e = StyleParseErrorKind::BadStringInDeclarationValueBlock(s.clone()); + return Err(input.new_custom_error(e)); + }, + Token::CloseParenthesis => { + let e = StyleParseErrorKind::UnbalancedCloseParenthesisInDeclarationValueBlock; + return Err(input.new_custom_error(e)); + }, + Token::CloseSquareBracket => { + let e = StyleParseErrorKind::UnbalancedCloseSquareBracketInDeclarationValueBlock; + return Err(input.new_custom_error(e)); + }, + Token::CloseCurlyBracket => { + let e = StyleParseErrorKind::UnbalancedCloseCurlyBracketInDeclarationValueBlock; + return Err(input.new_custom_error(e)); + }, + Token::Function(ref name) => { + if name.eq_ignore_ascii_case("var") { + let args_start = input.state(); + input.parse_nested_block(|input| { + parse_var_function(input, references.as_mut().map(|r| &mut **r)) + })?; + input.reset(&args_start); + } else if name.eq_ignore_ascii_case("env") { + let args_start = input.state(); + input.parse_nested_block(|input| { + parse_env_function(input, references.as_mut().map(|r| &mut **r)) + })?; + input.reset(&args_start); + } + nested!(); + check_closed!(")"); + Token::CloseParenthesis.serialization_type() + }, + Token::ParenthesisBlock => { + nested!(); + check_closed!(")"); + Token::CloseParenthesis.serialization_type() + }, + Token::CurlyBracketBlock => { + nested!(); + check_closed!("}"); + Token::CloseCurlyBracket.serialization_type() + }, + Token::SquareBracketBlock => { + nested!(); + check_closed!("]"); + Token::CloseSquareBracket.serialization_type() + }, + Token::QuotedString(_) => { + let serialization_type = token.serialization_type(); + let token_slice = input.slice_from(token_start); + let quote = &token_slice[..1]; + debug_assert!(matches!(quote, "\"" | "'")); + if !(token_slice.ends_with(quote) && token_slice.len() > 1) { + missing_closing_characters.push_str(quote) + } + serialization_type + }, + Token::Ident(ref value) | + Token::AtKeyword(ref value) | + Token::Hash(ref value) | + Token::IDHash(ref value) | + Token::UnquotedUrl(ref value) | + Token::Dimension { + unit: ref value, .. + } => { + let serialization_type = token.serialization_type(); + let is_unquoted_url = matches!(token, Token::UnquotedUrl(_)); + if value.ends_with("�") && input.slice_from(token_start).ends_with("\\") { + // Unescaped backslash at EOF in these contexts is interpreted as U+FFFD + // Check the value in case the final backslash was itself escaped. + // Serialize as escaped U+FFFD, which is also interpreted as U+FFFD. + // (Unescaped U+FFFD would also work, but removing the backslash is annoying.) + missing_closing_characters.push_str("�") + } + if is_unquoted_url { + check_closed!(")"); + } + serialization_type + }, + _ => token.serialization_type(), + }; + + token_start = input.position(); + token = match input.next_including_whitespace_and_comments() { + Ok(token) => token, + Err(..) => return Ok((first_token_type, last_token_type)), + }; + } +} + +fn parse_fallback<'i, 't>(input: &mut Parser<'i, 't>) -> Result<(), ParseError<'i>> { + // Exclude `!` and `;` at the top level + // https://drafts.csswg.org/css-syntax/#typedef-declaration-value + input.parse_until_before(Delimiter::Bang | Delimiter::Semicolon, |input| { + // Skip until the end. + while input.next_including_whitespace_and_comments().is_ok() {} + Ok(()) + }) +} + +// If the var function is valid, return Ok((custom_property_name, fallback)) +fn parse_var_function<'i, 't>( + input: &mut Parser<'i, 't>, + references: Option<&mut VarOrEnvReferences>, +) -> Result<(), ParseError<'i>> { + let name = input.expect_ident_cloned()?; + let name = parse_name(&name).map_err(|()| { + input.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone())) + })?; + if input.try_parse(|input| input.expect_comma()).is_ok() { + parse_fallback(input)?; + } + if let Some(refs) = references { + refs.custom_property_references.insert(Atom::from(name)); + } + Ok(()) +} + +fn parse_env_function<'i, 't>( + input: &mut Parser<'i, 't>, + references: Option<&mut VarOrEnvReferences>, +) -> Result<(), ParseError<'i>> { + // TODO(emilio): This should be per spec, but no other + // browser does that, see https://github.com/w3c/csswg-drafts/issues/3262. + input.expect_ident()?; + if input.try_parse(|input| input.expect_comma()).is_ok() { + parse_fallback(input)?; + } + if let Some(references) = references { + references.references_environment = true; + } + Ok(()) +} + +/// A struct that takes care of encapsulating the cascade process for custom +/// properties. +pub struct CustomPropertiesBuilder<'a> { + seen: PrecomputedHashSet<&'a Name>, + may_have_cycles: bool, + custom_properties: Option, + inherited: Option<&'a Arc>, + reverted: PrecomputedHashMap<&'a Name, (CascadePriority, bool)>, + device: &'a Device, +} + +impl<'a> CustomPropertiesBuilder<'a> { + /// Create a new builder, inheriting from a given custom properties map. + pub fn new(inherited: Option<&'a Arc>, device: &'a Device) -> Self { + Self { + seen: PrecomputedHashSet::default(), + reverted: Default::default(), + may_have_cycles: false, + custom_properties: None, + inherited, + device, + } + } + + /// Cascade a given custom property declaration. + pub fn cascade(&mut self, declaration: &'a CustomDeclaration, priority: CascadePriority) { + let CustomDeclaration { + ref name, + ref value, + } = *declaration; + + if let Some(&(reverted_priority, is_origin_revert)) = self.reverted.get(&name) { + if !reverted_priority.allows_when_reverted(&priority, is_origin_revert) { + return; + } + } + + let was_already_present = !self.seen.insert(name); + if was_already_present { + return; + } + + if !self.value_may_affect_style(name, value) { + return; + } + + if self.custom_properties.is_none() { + self.custom_properties = Some(match self.inherited { + Some(inherited) => (**inherited).clone(), + None => CustomPropertiesMap::default(), + }); + } + + let map = self.custom_properties.as_mut().unwrap(); + match *value { + CustomDeclarationValue::Value(ref unparsed_value) => { + let has_references = !unparsed_value.references.is_empty(); + self.may_have_cycles |= has_references; + + // If the variable value has no references and it has an + // environment variable here, perform substitution here instead + // of forcing a full traversal in `substitute_all` afterwards. + let value = if !has_references && unparsed_value.references_environment { + let result = substitute_references_in_value(unparsed_value, &map, &self.device); + match result { + Ok(new_value) => new_value, + Err(..) => { + map.remove(name); + return; + }, + } + } else { + (*unparsed_value).clone() + }; + map.insert(name.clone(), value); + }, + CustomDeclarationValue::CSSWideKeyword(keyword) => match keyword { + CSSWideKeyword::RevertLayer | CSSWideKeyword::Revert => { + let origin_revert = keyword == CSSWideKeyword::Revert; + self.seen.remove(name); + self.reverted.insert(name, (priority, origin_revert)); + }, + CSSWideKeyword::Initial => { + map.remove(name); + }, + // handled in value_may_affect_style + CSSWideKeyword::Unset | CSSWideKeyword::Inherit => unreachable!(), + }, + } + } + + fn value_may_affect_style(&self, name: &Name, value: &CustomDeclarationValue) -> bool { + match *value { + CustomDeclarationValue::CSSWideKeyword(CSSWideKeyword::Unset) | + CustomDeclarationValue::CSSWideKeyword(CSSWideKeyword::Inherit) => { + // Custom properties are inherited by default. So + // explicit 'inherit' or 'unset' means we can just use + // any existing value in the inherited CustomPropertiesMap. + return false; + }, + _ => {}, + } + + let existing_value = self + .custom_properties + .as_ref() + .and_then(|m| m.get(name)) + .or_else(|| self.inherited.and_then(|m| m.get(name))); + + match (existing_value, value) { + (None, &CustomDeclarationValue::CSSWideKeyword(CSSWideKeyword::Initial)) => { + // The initial value of a custom property is the same as it + // not existing in the map. + return false; + }, + (Some(existing_value), &CustomDeclarationValue::Value(ref value)) => { + // Don't bother overwriting an existing inherited value with + // the same specified value. + if existing_value == value { + return false; + } + }, + _ => {}, + } + + true + } + + fn inherited_properties_match(&self, map: &CustomPropertiesMap) -> bool { + let inherited = match self.inherited { + Some(inherited) => inherited, + None => return false, + }; + if inherited.len() != map.len() { + return false; + } + for name in self.seen.iter() { + if inherited.get(*name) != map.get(*name) { + return false; + } + } + true + } + + /// Returns the final map of applicable custom properties. + /// + /// If there was any specified property, we've created a new map and now we + /// need to remove any potential cycles, and wrap it in an arc. + /// + /// Otherwise, just use the inherited custom properties map. + pub fn build(mut self) -> Option> { + let mut map = match self.custom_properties.take() { + Some(m) => m, + None => return self.inherited.cloned(), + }; + + if self.may_have_cycles { + substitute_all(&mut map, &self.seen, self.device); + } + + // Some pages apply a lot of redundant custom properties, see e.g. + // bug 1758974 comment 5. Try to detect the case where the values + // haven't really changed, and save some memory by reusing the inherited + // map in that case. + if self.inherited_properties_match(&map) { + return self.inherited.cloned(); + } + + map.shrink_to_fit(); + Some(Arc::new(map)) + } +} + +/// Resolve all custom properties to either substituted, invalid, or unset +/// (meaning we should use the inherited value). +/// +/// It does cycle dependencies removal at the same time as substitution. +fn substitute_all( + custom_properties_map: &mut CustomPropertiesMap, + seen: &PrecomputedHashSet<&Name>, + device: &Device, +) { + // The cycle dependencies removal in this function is a variant + // of Tarjan's algorithm. It is mostly based on the pseudo-code + // listed in + // https://en.wikipedia.org/w/index.php? + // title=Tarjan%27s_strongly_connected_components_algorithm&oldid=801728495 + + /// Struct recording necessary information for each variable. + #[derive(Debug)] + struct VarInfo { + /// The name of the variable. It will be taken to save addref + /// when the corresponding variable is popped from the stack. + /// This also serves as a mark for whether the variable is + /// currently in the stack below. + name: Option, + /// If the variable is in a dependency cycle, lowlink represents + /// a smaller index which corresponds to a variable in the same + /// strong connected component, which is known to be accessible + /// from this variable. It is not necessarily the root, though. + lowlink: usize, + } + /// Context struct for traversing the variable graph, so that we can + /// avoid referencing all the fields multiple times. + #[derive(Debug)] + struct Context<'a> { + /// Number of variables visited. This is used as the order index + /// when we visit a new unresolved variable. + count: usize, + /// The map from custom property name to its order index. + index_map: PrecomputedHashMap, + /// Information of each variable indexed by the order index. + var_info: SmallVec<[VarInfo; 5]>, + /// The stack of order index of visited variables. It contains + /// all unfinished strong connected components. + stack: SmallVec<[usize; 5]>, + map: &'a mut CustomPropertiesMap, + /// To resolve the environment to substitute `env()` variables. + device: &'a Device, + } + + /// This function combines the traversal for cycle removal and value + /// substitution. It returns either a signal None if this variable + /// has been fully resolved (to either having no reference or being + /// marked invalid), or the order index for the given name. + /// + /// When it returns, the variable corresponds to the name would be + /// in one of the following states: + /// * It is still in context.stack, which means it is part of an + /// potentially incomplete dependency circle. + /// * It has been removed from the map. It can be either that the + /// substitution failed, or it is inside a dependency circle. + /// When this function removes a variable from the map because + /// of dependency circle, it would put all variables in the same + /// strong connected component to the set together. + /// * It doesn't have any reference, because either this variable + /// doesn't have reference at all in specified value, or it has + /// been completely resolved. + /// * There is no such variable at all. + fn traverse<'a>(name: &Name, context: &mut Context<'a>) -> Option { + // Some shortcut checks. + let (name, value) = { + let value = context.map.get(name)?; + + // Nothing to resolve. + if value.references.is_empty() { + debug_assert!( + !value.references_environment, + "Should've been handled earlier" + ); + return None; + } + + // Whether this variable has been visited in this traversal. + let key; + match context.index_map.entry(name.clone()) { + Entry::Occupied(entry) => { + return Some(*entry.get()); + }, + Entry::Vacant(entry) => { + key = entry.key().clone(); + entry.insert(context.count); + }, + } + + // Hold a strong reference to the value so that we don't + // need to keep reference to context.map. + (key, value.clone()) + }; + + // Add new entry to the information table. + let index = context.count; + context.count += 1; + debug_assert_eq!(index, context.var_info.len()); + context.var_info.push(VarInfo { + name: Some(name), + lowlink: index, + }); + context.stack.push(index); + + let mut self_ref = false; + let mut lowlink = index; + for next in value.references.iter() { + let next_index = match traverse(next, context) { + Some(index) => index, + // There is nothing to do if the next variable has been + // fully resolved at this point. + None => { + continue; + }, + }; + let next_info = &context.var_info[next_index]; + if next_index > index { + // The next variable has a larger index than us, so it + // must be inserted in the recursive call above. We want + // to get its lowlink. + lowlink = cmp::min(lowlink, next_info.lowlink); + } else if next_index == index { + self_ref = true; + } else if next_info.name.is_some() { + // The next variable has a smaller order index and it is + // in the stack, so we are at the same component. + lowlink = cmp::min(lowlink, next_index); + } + } + + context.var_info[index].lowlink = lowlink; + if lowlink != index { + // This variable is in a loop, but it is not the root of + // this strong connected component. We simply return for + // now, and the root would remove it from the map. + // + // This cannot be removed from the map here, because + // otherwise the shortcut check at the beginning of this + // function would return the wrong value. + return Some(index); + } + + // This is the root of a strong-connected component. + let mut in_loop = self_ref; + let name; + loop { + let var_index = context + .stack + .pop() + .expect("The current variable should still be in stack"); + let var_info = &mut context.var_info[var_index]; + // We should never visit the variable again, so it's safe + // to take the name away, so that we don't do additional + // reference count. + let var_name = var_info + .name + .take() + .expect("Variable should not be poped from stack twice"); + if var_index == index { + name = var_name; + break; + } + // Anything here is in a loop which can traverse to the + // variable we are handling, so remove it from the map, it's invalid + // at computed-value time. + context.map.remove(&var_name); + in_loop = true; + } + if in_loop { + // This variable is in loop. Resolve to invalid. + context.map.remove(&name); + return None; + } + + // Now we have shown that this variable is not in a loop, and all of its + // dependencies should have been resolved. We can start substitution + // now. + let result = substitute_references_in_value(&value, &context.map, &context.device); + match result { + Ok(computed_value) => { + context.map.insert(name, computed_value); + }, + Err(..) => { + // This is invalid, reset it to the guaranteed-invalid value. + context.map.remove(&name); + }, + } + + // All resolved, so return the signal value. + None + } + + // Note that `seen` doesn't contain names inherited from our parent, but + // those can't have variable references (since we inherit the computed + // variables) so we don't want to spend cycles traversing them anyway. + for name in seen { + let mut context = Context { + count: 0, + index_map: PrecomputedHashMap::default(), + stack: SmallVec::new(), + var_info: SmallVec::new(), + map: custom_properties_map, + device, + }; + traverse(name, &mut context); + } +} + +/// Replace `var()` and `env()` functions in a pre-existing variable value. +fn substitute_references_in_value<'i>( + value: &'i VariableValue, + custom_properties: &CustomPropertiesMap, + device: &Device, +) -> Result, ParseError<'i>> { + debug_assert!(!value.references.is_empty() || value.references_environment); + + let mut input = ParserInput::new(&value.css); + let mut input = Parser::new(&mut input); + let mut position = (input.position(), value.first_token_type); + let mut computed_value = ComputedValue::empty(); + + let last_token_type = substitute_block( + &mut input, + &mut position, + &mut computed_value, + custom_properties, + device, + )?; + + computed_value.push_from(&input, position, last_token_type)?; + computed_value.css.shrink_to_fit(); + Ok(Arc::new(computed_value)) +} + +/// Replace `var()` functions in an arbitrary bit of input. +/// +/// If the variable has its initial value, the callback should return `Err(())` +/// and leave `partial_computed_value` unchanged. +/// +/// Otherwise, it should push the value of the variable (with its own `var()` functions replaced) +/// to `partial_computed_value` and return `Ok(last_token_type of what was pushed)` +/// +/// Return `Err(())` if `input` is invalid at computed-value time. +/// or `Ok(last_token_type that was pushed to partial_computed_value)` otherwise. +fn substitute_block<'i>( + input: &mut Parser<'i, '_>, + position: &mut (SourcePosition, TokenSerializationType), + partial_computed_value: &mut ComputedValue, + custom_properties: &CustomPropertiesMap, + device: &Device, +) -> Result> { + let mut last_token_type = TokenSerializationType::nothing(); + let mut set_position_at_next_iteration = false; + loop { + let before_this_token = input.position(); + let next = input.next_including_whitespace_and_comments(); + if set_position_at_next_iteration { + *position = ( + before_this_token, + match next { + Ok(token) => token.serialization_type(), + Err(_) => TokenSerializationType::nothing(), + }, + ); + set_position_at_next_iteration = false; + } + let token = match next { + Ok(token) => token, + Err(..) => break, + }; + match token { + Token::Function(ref name) + if name.eq_ignore_ascii_case("var") || name.eq_ignore_ascii_case("env") => + { + let is_env = name.eq_ignore_ascii_case("env"); + + partial_computed_value.push( + input, + input.slice(position.0..before_this_token), + position.1, + last_token_type, + )?; + input.parse_nested_block(|input| { + // parse_var_function() / parse_env_function() ensure neither .unwrap() will fail. + let name = { + let name = input.expect_ident().unwrap(); + if is_env { + Atom::from(&**name) + } else { + Atom::from(parse_name(&name).unwrap()) + } + }; + + let env_value; + let value = if is_env { + if let Some(v) = device.environment().get(&name, device) { + env_value = v; + Some(&env_value) + } else { + None + } + } else { + custom_properties.get(&name).map(|v| &**v) + }; + + if let Some(v) = value { + last_token_type = v.last_token_type; + partial_computed_value.push_variable(input, v)?; + // Skip over the fallback, as `parse_nested_block` would return `Err` + // if we don't consume all of `input`. + // FIXME: Add a specialized method to cssparser to do this with less work. + while input.next().is_ok() {} + } else { + input.expect_comma()?; + input.skip_whitespace(); + let after_comma = input.state(); + let first_token_type = input + .next_including_whitespace_and_comments() + .ok() + .map_or_else(TokenSerializationType::nothing, |t| { + t.serialization_type() + }); + input.reset(&after_comma); + let mut position = (after_comma.position(), first_token_type); + last_token_type = substitute_block( + input, + &mut position, + partial_computed_value, + custom_properties, + device, + )?; + partial_computed_value.push_from(input, position, last_token_type)?; + } + Ok(()) + })?; + set_position_at_next_iteration = true + }, + Token::Function(_) | + Token::ParenthesisBlock | + Token::CurlyBracketBlock | + Token::SquareBracketBlock => { + input.parse_nested_block(|input| { + substitute_block( + input, + position, + partial_computed_value, + custom_properties, + device, + ) + })?; + // It's the same type for CloseCurlyBracket and CloseSquareBracket. + last_token_type = Token::CloseParenthesis.serialization_type(); + }, + + _ => last_token_type = token.serialization_type(), + } + } + // FIXME: deal with things being implicitly closed at the end of the input. E.g. + // ```html + //
+ //

+ //
+ // ``` + Ok(last_token_type) +} + +/// Replace `var()` and `env()` functions for a non-custom property. +/// +/// Return `Err(())` for invalid at computed time. +pub fn substitute<'i>( + input: &'i str, + first_token_type: TokenSerializationType, + computed_values_map: Option<&Arc>, + device: &Device, +) -> Result> { + let mut substituted = ComputedValue::empty(); + let mut input = ParserInput::new(input); + let mut input = Parser::new(&mut input); + let mut position = (input.position(), first_token_type); + let empty_map = CustomPropertiesMap::default(); + let custom_properties = match computed_values_map { + Some(m) => &**m, + None => &empty_map, + }; + let last_token_type = substitute_block( + &mut input, + &mut position, + &mut substituted, + &custom_properties, + device, + )?; + substituted.push_from(&input, position, last_token_type)?; + Ok(substituted.css) +} diff --git a/servo/components/style/data.rs b/servo/components/style/data.rs new file mode 100644 index 0000000000..62dff225f8 --- /dev/null +++ b/servo/components/style/data.rs @@ -0,0 +1,545 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Per-node data used in style calculation. + +use crate::computed_value_flags::ComputedValueFlags; +use crate::context::{SharedStyleContext, StackLimitChecker}; +use crate::dom::TElement; +use crate::invalidation::element::invalidator::InvalidationResult; +use crate::invalidation::element::restyle_hints::RestyleHint; +use crate::properties::ComputedValues; +use crate::selector_parser::{PseudoElement, RestyleDamage, EAGER_PSEUDO_COUNT}; +use crate::style_resolver::{PrimaryStyle, ResolvedElementStyles, ResolvedStyle}; +#[cfg(feature = "gecko")] +use malloc_size_of::MallocSizeOfOps; +use selectors::NthIndexCache; +use servo_arc::Arc; +use std::fmt; +use std::mem; +use std::ops::{Deref, DerefMut}; + +bitflags! { + /// Various flags stored on ElementData. + #[derive(Default)] + pub struct ElementDataFlags: u8 { + /// Whether the styles changed for this restyle. + const WAS_RESTYLED = 1 << 0; + /// Whether the last traversal of this element did not do + /// any style computation. This is not true during the initial + /// styling pass, nor is it true when we restyle (in which case + /// WAS_RESTYLED is set). + /// + /// This bit always corresponds to the last time the element was + /// traversed, so each traversal simply updates it with the appropriate + /// value. + const TRAVERSED_WITHOUT_STYLING = 1 << 1; + + /// Whether the primary style of this element data was reused from + /// another element via a rule node comparison. This allows us to + /// differentiate between elements that shared styles because they met + /// all the criteria of the style sharing cache, compared to elements + /// that reused style structs via rule node identity. + /// + /// The former gives us stronger transitive guarantees that allows us to + /// apply the style sharing cache to cousins. + const PRIMARY_STYLE_REUSED_VIA_RULE_NODE = 1 << 2; + } +} + +/// A lazily-allocated list of styles for eagerly-cascaded pseudo-elements. +/// +/// We use an Arc so that sharing these styles via the style sharing cache does +/// not require duplicate allocations. We leverage the copy-on-write semantics of +/// Arc::make_mut(), which is free (i.e. does not require atomic RMU operations) +/// in servo_arc. +#[derive(Clone, Debug, Default)] +pub struct EagerPseudoStyles(Option>); + +#[derive(Default)] +struct EagerPseudoArray(EagerPseudoArrayInner); +type EagerPseudoArrayInner = [Option>; EAGER_PSEUDO_COUNT]; + +impl Deref for EagerPseudoArray { + type Target = EagerPseudoArrayInner; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for EagerPseudoArray { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +// Manually implement `Clone` here because the derived impl of `Clone` for +// array types assumes the value inside is `Copy`. +impl Clone for EagerPseudoArray { + fn clone(&self) -> Self { + let mut clone = Self::default(); + for i in 0..EAGER_PSEUDO_COUNT { + clone[i] = self.0[i].clone(); + } + clone + } +} + +// Override Debug to print which pseudos we have, and substitute the rule node +// for the much-more-verbose ComputedValues stringification. +impl fmt::Debug for EagerPseudoArray { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "EagerPseudoArray {{ ")?; + for i in 0..EAGER_PSEUDO_COUNT { + if let Some(ref values) = self[i] { + write!( + f, + "{:?}: {:?}, ", + PseudoElement::from_eager_index(i), + &values.rules + )?; + } + } + write!(f, "}}") + } +} + +// Can't use [None; EAGER_PSEUDO_COUNT] here because it complains +// about Copy not being implemented for our Arc type. +#[cfg(feature = "gecko")] +const EMPTY_PSEUDO_ARRAY: &'static EagerPseudoArrayInner = &[None, None, None, None]; +#[cfg(feature = "servo")] +const EMPTY_PSEUDO_ARRAY: &'static EagerPseudoArrayInner = &[None, None, None]; + +impl EagerPseudoStyles { + /// Returns whether there are any pseudo styles. + pub fn is_empty(&self) -> bool { + self.0.is_none() + } + + /// Grabs a reference to the list of styles, if they exist. + pub fn as_optional_array(&self) -> Option<&EagerPseudoArrayInner> { + match self.0 { + None => None, + Some(ref x) => Some(&x.0), + } + } + + /// Grabs a reference to the list of styles or a list of None if + /// there are no styles to be had. + pub fn as_array(&self) -> &EagerPseudoArrayInner { + self.as_optional_array().unwrap_or(EMPTY_PSEUDO_ARRAY) + } + + /// Returns a reference to the style for a given eager pseudo, if it exists. + pub fn get(&self, pseudo: &PseudoElement) -> Option<&Arc> { + debug_assert!(pseudo.is_eager()); + self.0 + .as_ref() + .and_then(|p| p[pseudo.eager_index()].as_ref()) + } + + /// Sets the style for the eager pseudo. + pub fn set(&mut self, pseudo: &PseudoElement, value: Arc) { + if self.0.is_none() { + self.0 = Some(Arc::new(Default::default())); + } + let arr = Arc::make_mut(self.0.as_mut().unwrap()); + arr[pseudo.eager_index()] = Some(value); + } +} + +/// The styles associated with a node, including the styles for any +/// pseudo-elements. +#[derive(Clone, Default)] +pub struct ElementStyles { + /// The element's style. + pub primary: Option>, + /// A list of the styles for the element's eagerly-cascaded pseudo-elements. + pub pseudos: EagerPseudoStyles, +} + +// There's one of these per rendered elements so it better be small. +size_of_test!(ElementStyles, 16); + +/// Information on how this element uses viewport units. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum ViewportUnitUsage { + /// No viewport units are used. + None = 0, + /// There are viewport units used from regular style rules (which means we + /// should re-cascade). + FromDeclaration, + /// There are viewport units used from container queries (which means we + /// need to re-selector-match). + FromQuery, +} + +impl ElementStyles { + /// Returns the primary style. + pub fn get_primary(&self) -> Option<&Arc> { + self.primary.as_ref() + } + + /// Returns the primary style. Panic if no style available. + pub fn primary(&self) -> &Arc { + self.primary.as_ref().unwrap() + } + + /// Whether this element `display` value is `none`. + pub fn is_display_none(&self) -> bool { + self.primary().get_box().clone_display().is_none() + } + + /// Whether this element uses viewport units. + pub fn viewport_unit_usage(&self) -> ViewportUnitUsage { + fn usage_from_flags(flags: ComputedValueFlags) -> ViewportUnitUsage { + if flags.intersects(ComputedValueFlags::USES_VIEWPORT_UNITS_ON_CONTAINER_QUERIES) { + return ViewportUnitUsage::FromQuery; + } + if flags.intersects(ComputedValueFlags::USES_VIEWPORT_UNITS) { + return ViewportUnitUsage::FromDeclaration; + } + ViewportUnitUsage::None + } + + let mut usage = usage_from_flags(self.primary().flags); + for pseudo_style in self.pseudos.as_array() { + if let Some(ref pseudo_style) = pseudo_style { + usage = std::cmp::max(usage, usage_from_flags(pseudo_style.flags)); + } + } + + usage + } + + #[cfg(feature = "gecko")] + fn size_of_excluding_cvs(&self, _ops: &mut MallocSizeOfOps) -> usize { + // As the method name suggests, we don't measures the ComputedValues + // here, because they are measured on the C++ side. + + // XXX: measure the EagerPseudoArray itself, but not the ComputedValues + // within it. + + 0 + } +} + +// We manually implement Debug for ElementStyles so that we can avoid the +// verbose stringification of every property in the ComputedValues. We +// substitute the rule node instead. +impl fmt::Debug for ElementStyles { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "ElementStyles {{ primary: {:?}, pseudos: {:?} }}", + self.primary.as_ref().map(|x| &x.rules), + self.pseudos + ) + } +} + +/// Style system data associated with an Element. +/// +/// In Gecko, this hangs directly off the Element. Servo, this is embedded +/// inside of layout data, which itself hangs directly off the Element. In +/// both cases, it is wrapped inside an AtomicRefCell to ensure thread safety. +#[derive(Debug, Default)] +pub struct ElementData { + /// The styles for the element and its pseudo-elements. + pub styles: ElementStyles, + + /// The restyle damage, indicating what kind of layout changes are required + /// afte restyling. + pub damage: RestyleDamage, + + /// The restyle hint, which indicates whether selectors need to be rematched + /// for this element, its children, and its descendants. + pub hint: RestyleHint, + + /// Flags. + pub flags: ElementDataFlags, +} + +// There's one of these per rendered elements so it better be small. +size_of_test!(ElementData, 24); + +/// The kind of restyle that a single element should do. +#[derive(Debug)] +pub enum RestyleKind { + /// We need to run selector matching plus re-cascade, that is, a full + /// restyle. + MatchAndCascade, + /// We need to recascade with some replacement rule, such as the style + /// attribute, or animation rules. + CascadeWithReplacements(RestyleHint), + /// We only need to recascade, for example, because only inherited + /// properties in the parent changed. + CascadeOnly, +} + +impl ElementData { + /// Invalidates style for this element, its descendants, and later siblings, + /// based on the snapshot of the element that we took when attributes or + /// state changed. + pub fn invalidate_style_if_needed<'a, E: TElement>( + &mut self, + element: E, + shared_context: &SharedStyleContext, + stack_limit_checker: Option<&StackLimitChecker>, + nth_index_cache: &mut NthIndexCache, + ) -> InvalidationResult { + // In animation-only restyle we shouldn't touch snapshot at all. + if shared_context.traversal_flags.for_animation_only() { + return InvalidationResult::empty(); + } + + use crate::invalidation::element::invalidator::TreeStyleInvalidator; + use crate::invalidation::element::state_and_attributes::StateAndAttrInvalidationProcessor; + + debug!( + "invalidate_style_if_needed: {:?}, flags: {:?}, has_snapshot: {}, \ + handled_snapshot: {}, pseudo: {:?}", + element, + shared_context.traversal_flags, + element.has_snapshot(), + element.handled_snapshot(), + element.implemented_pseudo_element() + ); + + if !element.has_snapshot() || element.handled_snapshot() { + return InvalidationResult::empty(); + } + + let mut processor = + StateAndAttrInvalidationProcessor::new(shared_context, element, self, nth_index_cache); + + let invalidator = TreeStyleInvalidator::new(element, stack_limit_checker, &mut processor); + + let result = invalidator.invalidate(); + + unsafe { element.set_handled_snapshot() } + debug_assert!(element.handled_snapshot()); + + result + } + + /// Returns true if this element has styles. + #[inline] + pub fn has_styles(&self) -> bool { + self.styles.primary.is_some() + } + + /// Returns this element's styles as resolved styles to use for sharing. + pub fn share_styles(&self) -> ResolvedElementStyles { + ResolvedElementStyles { + primary: self.share_primary_style(), + pseudos: self.styles.pseudos.clone(), + } + } + + /// Returns this element's primary style as a resolved style to use for sharing. + pub fn share_primary_style(&self) -> PrimaryStyle { + let reused_via_rule_node = self + .flags + .contains(ElementDataFlags::PRIMARY_STYLE_REUSED_VIA_RULE_NODE); + + PrimaryStyle { + style: ResolvedStyle(self.styles.primary().clone()), + reused_via_rule_node, + } + } + + /// Sets a new set of styles, returning the old ones. + pub fn set_styles(&mut self, new_styles: ResolvedElementStyles) -> ElementStyles { + if new_styles.primary.reused_via_rule_node { + self.flags + .insert(ElementDataFlags::PRIMARY_STYLE_REUSED_VIA_RULE_NODE); + } else { + self.flags + .remove(ElementDataFlags::PRIMARY_STYLE_REUSED_VIA_RULE_NODE); + } + mem::replace(&mut self.styles, new_styles.into()) + } + + /// Returns the kind of restyling that we're going to need to do on this + /// element, based of the stored restyle hint. + pub fn restyle_kind(&self, shared_context: &SharedStyleContext) -> Option { + if shared_context.traversal_flags.for_animation_only() { + return self.restyle_kind_for_animation(shared_context); + } + + let style = match self.styles.primary { + Some(ref s) => s, + None => return Some(RestyleKind::MatchAndCascade), + }; + + let hint = self.hint; + if hint.is_empty() { + return None; + } + + let needs_to_match_self = hint.intersects(RestyleHint::RESTYLE_SELF) || + (hint.intersects(RestyleHint::RESTYLE_SELF_IF_PSEUDO) && style.is_pseudo_style()); + if needs_to_match_self { + return Some(RestyleKind::MatchAndCascade); + } + + if hint.has_replacements() { + debug_assert!( + !hint.has_animation_hint(), + "Animation only restyle hint should have already processed" + ); + return Some(RestyleKind::CascadeWithReplacements( + hint & RestyleHint::replacements(), + )); + } + + let needs_to_recascade_self = hint.intersects(RestyleHint::RECASCADE_SELF) || + (hint.intersects(RestyleHint::RECASCADE_SELF_IF_INHERIT_RESET_STYLE) && + style + .flags + .contains(ComputedValueFlags::INHERITS_RESET_STYLE)); + if needs_to_recascade_self { + return Some(RestyleKind::CascadeOnly); + } + + None + } + + /// Returns the kind of restyling for animation-only restyle. + fn restyle_kind_for_animation( + &self, + shared_context: &SharedStyleContext, + ) -> Option { + debug_assert!(shared_context.traversal_flags.for_animation_only()); + debug_assert!( + self.has_styles(), + "animation traversal doesn't care about unstyled elements" + ); + + // FIXME: We should ideally restyle here, but it is a hack to work around our weird + // animation-only traversal stuff: If we're display: none and the rules we could + // match could change, we consider our style up-to-date. This is because re-cascading with + // and old style doesn't guarantee returning the correct animation style (that's + // bug 1393323). So if our display changed, and it changed from display: none, we would + // incorrectly forget about it and wouldn't be able to correctly style our descendants + // later. + // XXX Figure out if this still makes sense. + let hint = self.hint; + if self.styles.is_display_none() && hint.intersects(RestyleHint::RESTYLE_SELF) { + return None; + } + + let style = self.styles.primary(); + // Return either CascadeWithReplacements or CascadeOnly in case of + // animation-only restyle. I.e. animation-only restyle never does + // selector matching. + if hint.has_animation_hint() { + return Some(RestyleKind::CascadeWithReplacements( + hint & RestyleHint::for_animations(), + )); + } + + let needs_to_recascade_self = hint.intersects(RestyleHint::RECASCADE_SELF) || + (hint.intersects(RestyleHint::RECASCADE_SELF_IF_INHERIT_RESET_STYLE) && + style + .flags + .contains(ComputedValueFlags::INHERITS_RESET_STYLE)); + if needs_to_recascade_self { + return Some(RestyleKind::CascadeOnly); + } + return None; + } + + /// Drops any restyle state from the element. + /// + /// FIXME(bholley): The only caller of this should probably just assert that + /// the hint is empty and call clear_flags_and_damage(). + #[inline] + pub fn clear_restyle_state(&mut self) { + self.hint = RestyleHint::empty(); + self.clear_restyle_flags_and_damage(); + } + + /// Drops restyle flags and damage from the element. + #[inline] + pub fn clear_restyle_flags_and_damage(&mut self) { + self.damage = RestyleDamage::empty(); + self.flags.remove(ElementDataFlags::WAS_RESTYLED); + } + + /// Mark this element as restyled, which is useful to know whether we need + /// to do a post-traversal. + pub fn set_restyled(&mut self) { + self.flags.insert(ElementDataFlags::WAS_RESTYLED); + self.flags + .remove(ElementDataFlags::TRAVERSED_WITHOUT_STYLING); + } + + /// Returns true if this element was restyled. + #[inline] + pub fn is_restyle(&self) -> bool { + self.flags.contains(ElementDataFlags::WAS_RESTYLED) + } + + /// Mark that we traversed this element without computing any style for it. + pub fn set_traversed_without_styling(&mut self) { + self.flags + .insert(ElementDataFlags::TRAVERSED_WITHOUT_STYLING); + } + + /// Returns whether this element has been part of a restyle. + #[inline] + pub fn contains_restyle_data(&self) -> bool { + self.is_restyle() || !self.hint.is_empty() || !self.damage.is_empty() + } + + /// Returns whether it is safe to perform cousin sharing based on the ComputedValues + /// identity of the primary style in this ElementData. There are a few subtle things + /// to check. + /// + /// First, if a parent element was already styled and we traversed past it without + /// restyling it, that may be because our clever invalidation logic was able to prove + /// that the styles of that element would remain unchanged despite changes to the id + /// or class attributes. However, style sharing relies on the strong guarantee that all + /// the classes and ids up the respective parent chains are identical. As such, if we + /// skipped styling for one (or both) of the parents on this traversal, we can't share + /// styles across cousins. Note that this is a somewhat conservative check. We could + /// tighten it by having the invalidation logic explicitly flag elements for which it + /// ellided styling. + /// + /// Second, we want to only consider elements whose ComputedValues match due to a hit + /// in the style sharing cache, rather than due to the rule-node-based reuse that + /// happens later in the styling pipeline. The former gives us the stronger guarantees + /// we need for style sharing, the latter does not. + pub fn safe_for_cousin_sharing(&self) -> bool { + if self.flags.intersects( + ElementDataFlags::TRAVERSED_WITHOUT_STYLING | + ElementDataFlags::PRIMARY_STYLE_REUSED_VIA_RULE_NODE, + ) { + return false; + } + if !self + .styles + .primary() + .get_box() + .clone_container_type() + .is_normal() + { + return false; + } + true + } + + /// Measures memory usage. + #[cfg(feature = "gecko")] + pub fn size_of_excluding_cvs(&self, ops: &mut MallocSizeOfOps) -> usize { + let n = self.styles.size_of_excluding_cvs(ops); + + // We may measure more fields in the future if DMD says it's worth it. + + n + } +} diff --git a/servo/components/style/dom.rs b/servo/components/style/dom.rs new file mode 100644 index 0000000000..cffc24f6cc --- /dev/null +++ b/servo/components/style/dom.rs @@ -0,0 +1,947 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Types and traits used to access the DOM from style calculation. + +#![allow(unsafe_code)] +#![deny(missing_docs)] + +use crate::applicable_declarations::ApplicableDeclarationBlock; +use crate::context::SharedStyleContext; +#[cfg(feature = "gecko")] +use crate::context::{PostAnimationTasks, UpdateAnimationsTasks}; +use crate::data::ElementData; +use crate::media_queries::Device; +use crate::properties::{AnimationDeclarations, ComputedValues, PropertyDeclarationBlock}; +use crate::selector_parser::{AttrValue, Lang, PseudoElement, SelectorImpl}; +use crate::shared_lock::{Locked, SharedRwLock}; +use crate::stylist::CascadeData; +use crate::values::computed::Display; +use crate::values::AtomIdent; +use crate::{LocalName, Namespace, WeakAtom}; +use atomic_refcell::{AtomicRef, AtomicRefMut}; +use dom::ElementState; +use selectors::matching::{QuirksMode, VisitedHandlingMode}; +use selectors::sink::Push; +use selectors::Element as SelectorsElement; +use servo_arc::{Arc, ArcBorrow}; +use std::fmt; +use std::fmt::Debug; +use std::hash::Hash; +use std::ops::Deref; + +pub use style_traits::dom::OpaqueNode; + +/// Simple trait to provide basic information about the type of an element. +/// +/// We avoid exposing the full type id, since computing it in the general case +/// would be difficult for Gecko nodes. +pub trait NodeInfo { + /// Whether this node is an element. + fn is_element(&self) -> bool; + /// Whether this node is a text node. + fn is_text_node(&self) -> bool; +} + +/// A node iterator that only returns node that don't need layout. +pub struct LayoutIterator(pub T); + +impl Iterator for LayoutIterator +where + T: Iterator, + N: NodeInfo, +{ + type Item = N; + + fn next(&mut self) -> Option { + loop { + let n = self.0.next()?; + // Filter out nodes that layout should ignore. + if n.is_text_node() || n.is_element() { + return Some(n); + } + } + } +} + +/// An iterator over the DOM children of a node. +pub struct DomChildren(Option); +impl Iterator for DomChildren +where + N: TNode, +{ + type Item = N; + + fn next(&mut self) -> Option { + let n = self.0.take()?; + self.0 = n.next_sibling(); + Some(n) + } +} + +/// An iterator over the DOM descendants of a node in pre-order. +pub struct DomDescendants { + previous: Option, + scope: N, +} + +impl Iterator for DomDescendants +where + N: TNode, +{ + type Item = N; + + #[inline] + fn next(&mut self) -> Option { + let prev = self.previous.take()?; + self.previous = prev.next_in_preorder(self.scope); + self.previous + } +} + +/// The `TDocument` trait, to represent a document node. +pub trait TDocument: Sized + Copy + Clone { + /// The concrete `TNode` type. + type ConcreteNode: TNode; + + /// Get this document as a `TNode`. + fn as_node(&self) -> Self::ConcreteNode; + + /// Returns whether this document is an HTML document. + fn is_html_document(&self) -> bool; + + /// Returns the quirks mode of this document. + fn quirks_mode(&self) -> QuirksMode; + + /// Get a list of elements with a given ID in this document, sorted by + /// tree position. + /// + /// Can return an error to signal that this list is not available, or also + /// return an empty slice. + fn elements_with_id<'a>( + &self, + _id: &AtomIdent, + ) -> Result<&'a [::ConcreteElement], ()> + where + Self: 'a, + { + Err(()) + } + + /// This document's shared lock. + fn shared_lock(&self) -> &SharedRwLock; +} + +/// The `TNode` trait. This is the main generic trait over which the style +/// system can be implemented. +pub trait TNode: Sized + Copy + Clone + Debug + NodeInfo + PartialEq { + /// The concrete `TElement` type. + type ConcreteElement: TElement; + + /// The concrete `TDocument` type. + type ConcreteDocument: TDocument; + + /// The concrete `TShadowRoot` type. + type ConcreteShadowRoot: TShadowRoot; + + /// Get this node's parent node. + fn parent_node(&self) -> Option; + + /// Get this node's first child. + fn first_child(&self) -> Option; + + /// Get this node's last child. + fn last_child(&self) -> Option; + + /// Get this node's previous sibling. + fn prev_sibling(&self) -> Option; + + /// Get this node's next sibling. + fn next_sibling(&self) -> Option; + + /// Get the owner document of this node. + fn owner_doc(&self) -> Self::ConcreteDocument; + + /// Iterate over the DOM children of a node. + #[inline(always)] + fn dom_children(&self) -> DomChildren { + DomChildren(self.first_child()) + } + + /// Returns whether the node is attached to a document. + fn is_in_document(&self) -> bool; + + /// Iterate over the DOM children of a node, in preorder. + #[inline(always)] + fn dom_descendants(&self) -> DomDescendants { + DomDescendants { + previous: Some(*self), + scope: *self, + } + } + + /// Returns the next node after this one, in a pre-order tree-traversal of + /// the subtree rooted at scoped_to. + #[inline] + fn next_in_preorder(&self, scoped_to: Self) -> Option { + if let Some(c) = self.first_child() { + return Some(c); + } + + let mut current = *self; + loop { + if current == scoped_to { + return None; + } + + if let Some(s) = current.next_sibling() { + return Some(s); + } + + debug_assert!( + current.parent_node().is_some(), + "Not a descendant of the scope?" + ); + current = current.parent_node()?; + } + } + + /// Get this node's parent element from the perspective of a restyle + /// traversal. + fn traversal_parent(&self) -> Option; + + /// Get this node's parent element if present. + fn parent_element(&self) -> Option { + self.parent_node().and_then(|n| n.as_element()) + } + + /// Get this node's parent element, or shadow host if it's a shadow root. + fn parent_element_or_host(&self) -> Option { + let parent = self.parent_node()?; + if let Some(e) = parent.as_element() { + return Some(e); + } + if let Some(root) = parent.as_shadow_root() { + return Some(root.host()); + } + None + } + + /// Converts self into an `OpaqueNode`. + fn opaque(&self) -> OpaqueNode; + + /// A debug id, only useful, mm... for debugging. + fn debug_id(self) -> usize; + + /// Get this node as an element, if it's one. + fn as_element(&self) -> Option; + + /// Get this node as a document, if it's one. + fn as_document(&self) -> Option; + + /// Get this node as a ShadowRoot, if it's one. + fn as_shadow_root(&self) -> Option; +} + +/// Wrapper to output the subtree rather than the single node when formatting +/// for Debug. +pub struct ShowSubtree(pub N); +impl Debug for ShowSubtree { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!(f, "DOM Subtree:")?; + fmt_subtree(f, &|f, n| write!(f, "{:?}", n), self.0, 1) + } +} + +/// Wrapper to output the subtree along with the ElementData when formatting +/// for Debug. +pub struct ShowSubtreeData(pub N); +impl Debug for ShowSubtreeData { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!(f, "DOM Subtree:")?; + fmt_subtree(f, &|f, n| fmt_with_data(f, n), self.0, 1) + } +} + +/// Wrapper to output the subtree along with the ElementData and primary +/// ComputedValues when formatting for Debug. This is extremely verbose. +#[cfg(feature = "servo")] +pub struct ShowSubtreeDataAndPrimaryValues(pub N); +#[cfg(feature = "servo")] +impl Debug for ShowSubtreeDataAndPrimaryValues { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!(f, "DOM Subtree:")?; + fmt_subtree(f, &|f, n| fmt_with_data_and_primary_values(f, n), self.0, 1) + } +} + +fn fmt_with_data(f: &mut fmt::Formatter, n: N) -> fmt::Result { + if let Some(el) = n.as_element() { + write!( + f, + "{:?} dd={} aodd={} data={:?}", + el, + el.has_dirty_descendants(), + el.has_animation_only_dirty_descendants(), + el.borrow_data(), + ) + } else { + write!(f, "{:?}", n) + } +} + +#[cfg(feature = "servo")] +fn fmt_with_data_and_primary_values(f: &mut fmt::Formatter, n: N) -> fmt::Result { + if let Some(el) = n.as_element() { + let dd = el.has_dirty_descendants(); + let aodd = el.has_animation_only_dirty_descendants(); + let data = el.borrow_data(); + let values = data.as_ref().and_then(|d| d.styles.get_primary()); + write!( + f, + "{:?} dd={} aodd={} data={:?} values={:?}", + el, dd, aodd, &data, values + ) + } else { + write!(f, "{:?}", n) + } +} + +fn fmt_subtree(f: &mut fmt::Formatter, stringify: &F, n: N, indent: u32) -> fmt::Result +where + F: Fn(&mut fmt::Formatter, N) -> fmt::Result, +{ + for _ in 0..indent { + write!(f, " ")?; + } + stringify(f, n)?; + if let Some(e) = n.as_element() { + for kid in e.traversal_children() { + writeln!(f, "")?; + fmt_subtree(f, stringify, kid, indent + 1)?; + } + } + + Ok(()) +} + +/// The ShadowRoot trait. +pub trait TShadowRoot: Sized + Copy + Clone + Debug + PartialEq { + /// The concrete node type. + type ConcreteNode: TNode; + + /// Get this ShadowRoot as a node. + fn as_node(&self) -> Self::ConcreteNode; + + /// Get the shadow host that hosts this ShadowRoot. + fn host(&self) -> ::ConcreteElement; + + /// Get the style data for this ShadowRoot. + fn style_data<'a>(&self) -> Option<&'a CascadeData> + where + Self: 'a; + + /// Get the list of shadow parts for this shadow root. + fn parts<'a>(&self) -> &[::ConcreteElement] + where + Self: 'a, + { + &[] + } + + /// Get a list of elements with a given ID in this shadow root, sorted by + /// tree position. + /// + /// Can return an error to signal that this list is not available, or also + /// return an empty slice. + fn elements_with_id<'a>( + &self, + _id: &AtomIdent, + ) -> Result<&'a [::ConcreteElement], ()> + where + Self: 'a, + { + Err(()) + } +} + +/// The element trait, the main abstraction the style crate acts over. +pub trait TElement: + Eq + PartialEq + Debug + Hash + Sized + Copy + Clone + SelectorsElement +{ + /// The concrete node type. + type ConcreteNode: TNode; + + /// A concrete children iterator type in order to iterate over the `Node`s. + /// + /// TODO(emilio): We should eventually replace this with the `impl Trait` + /// syntax. + type TraversalChildrenIterator: Iterator; + + /// Get this element as a node. + fn as_node(&self) -> Self::ConcreteNode; + + /// A debug-only check that the device's owner doc matches the actual doc + /// we're the root of. + /// + /// Otherwise we may set document-level state incorrectly, like the root + /// font-size used for rem units. + fn owner_doc_matches_for_testing(&self, _: &Device) -> bool { + true + } + + /// Whether this element should match user and content rules. + /// + /// We use this for Native Anonymous Content in Gecko. + fn matches_user_and_content_rules(&self) -> bool { + true + } + + /// Returns the depth of this element in the DOM. + fn depth(&self) -> usize { + let mut depth = 0; + let mut curr = *self; + while let Some(parent) = curr.traversal_parent() { + depth += 1; + curr = parent; + } + + depth + } + + /// Get this node's parent element from the perspective of a restyle + /// traversal. + fn traversal_parent(&self) -> Option { + self.as_node().traversal_parent() + } + + /// Get this node's children from the perspective of a restyle traversal. + fn traversal_children(&self) -> LayoutIterator; + + /// Returns the parent element we should inherit from. + /// + /// This is pretty much always the parent element itself, except in the case + /// of Gecko's Native Anonymous Content, which uses the traversal parent + /// (i.e. the flattened tree parent) and which also may need to find the + /// closest non-NAC ancestor. + fn inheritance_parent(&self) -> Option { + self.parent_element() + } + + /// The ::before pseudo-element of this element, if it exists. + fn before_pseudo_element(&self) -> Option { + None + } + + /// The ::after pseudo-element of this element, if it exists. + fn after_pseudo_element(&self) -> Option { + None + } + + /// The ::marker pseudo-element of this element, if it exists. + fn marker_pseudo_element(&self) -> Option { + None + } + + /// Execute `f` for each anonymous content child (apart from ::before and + /// ::after) whose originating element is `self`. + fn each_anonymous_content_child(&self, _f: F) + where + F: FnMut(Self), + { + } + + /// Return whether this element is an element in the HTML namespace. + fn is_html_element(&self) -> bool; + + /// Return whether this element is an element in the MathML namespace. + fn is_mathml_element(&self) -> bool; + + /// Return whether this element is an element in the SVG namespace. + fn is_svg_element(&self) -> bool; + + /// Return whether this element is an element in the XUL namespace. + fn is_xul_element(&self) -> bool { + false + } + + /// Return the list of slotted nodes of this node. + fn slotted_nodes(&self) -> &[Self::ConcreteNode] { + &[] + } + + /// Get this element's style attribute. + fn style_attribute(&self) -> Option>>; + + /// Unset the style attribute's dirty bit. + /// Servo doesn't need to manage ditry bit for style attribute. + fn unset_dirty_style_attribute(&self) {} + + /// Get this element's SMIL override declarations. + fn smil_override(&self) -> Option>> { + None + } + + /// Get the combined animation and transition rules. + /// + /// FIXME(emilio): Is this really useful? + fn animation_declarations(&self, context: &SharedStyleContext) -> AnimationDeclarations { + if !self.may_have_animations() { + return Default::default(); + } + + AnimationDeclarations { + animations: self.animation_rule(context), + transitions: self.transition_rule(context), + } + } + + /// Get this element's animation rule. + fn animation_rule( + &self, + _: &SharedStyleContext, + ) -> Option>>; + + /// Get this element's transition rule. + fn transition_rule( + &self, + context: &SharedStyleContext, + ) -> Option>>; + + /// Get this element's state, for non-tree-structural pseudos. + fn state(&self) -> ElementState; + + /// Whether this element has an attribute with a given namespace. + fn has_attr(&self, namespace: &Namespace, attr: &LocalName) -> bool; + + /// Returns whether this element has a `part` attribute. + fn has_part_attr(&self) -> bool; + + /// Returns whether this element exports any part from its shadow tree. + fn exports_any_part(&self) -> bool; + + /// The ID for this element. + fn id(&self) -> Option<&WeakAtom>; + + /// Internal iterator for the classes of this element. + fn each_class(&self, callback: F) + where + F: FnMut(&AtomIdent); + + /// Internal iterator for the part names of this element. + fn each_part(&self, _callback: F) + where + F: FnMut(&AtomIdent), + { + } + + /// Internal iterator for the attribute names of this element. + fn each_attr_name(&self, callback: F) + where + F: FnMut(&AtomIdent); + + /// Internal iterator for the part names that this element exports for a + /// given part name. + fn each_exported_part(&self, _name: &AtomIdent, _callback: F) + where + F: FnMut(&AtomIdent), + { + } + + /// Whether a given element may generate a pseudo-element. + /// + /// This is useful to avoid computing, for example, pseudo styles for + /// `::-first-line` or `::-first-letter`, when we know it won't affect us. + /// + /// TODO(emilio, bz): actually implement the logic for it. + fn may_generate_pseudo(&self, pseudo: &PseudoElement, _primary_style: &ComputedValues) -> bool { + // ::before/::after are always supported for now, though we could try to + // optimize out leaf elements. + + // ::first-letter and ::first-line are only supported for block-inside + // things, and only in Gecko, not Servo. Unfortunately, Gecko has + // block-inside things that might have any computed display value due to + // things like fieldsets, legends, etc. Need to figure out how this + // should work. + debug_assert!( + pseudo.is_eager(), + "Someone called may_generate_pseudo with a non-eager pseudo." + ); + true + } + + /// Returns true if this element may have a descendant needing style processing. + /// + /// Note that we cannot guarantee the existence of such an element, because + /// it may have been removed from the DOM between marking it for restyle and + /// the actual restyle traversal. + fn has_dirty_descendants(&self) -> bool; + + /// Returns whether state or attributes that may change style have changed + /// on the element, and thus whether the element has been snapshotted to do + /// restyle hint computation. + fn has_snapshot(&self) -> bool; + + /// Returns whether the current snapshot if present has been handled. + fn handled_snapshot(&self) -> bool; + + /// Flags this element as having handled already its snapshot. + unsafe fn set_handled_snapshot(&self); + + /// Returns whether the element's styles are up-to-date after traversal + /// (i.e. in post traversal). + fn has_current_styles(&self, data: &ElementData) -> bool { + if self.has_snapshot() && !self.handled_snapshot() { + return false; + } + + data.has_styles() && + // TODO(hiro): When an animating element moved into subtree of + // contenteditable element, there remains animation restyle hints in + // post traversal. It's generally harmless since the hints will be + // processed in a next styling but ideally it should be processed soon. + // + // Without this, we get failures in: + // layout/style/crashtests/1383319.html + // layout/style/crashtests/1383001.html + // + // https://bugzilla.mozilla.org/show_bug.cgi?id=1389675 tracks fixing + // this. + !data.hint.has_non_animation_invalidations() + } + + /// Flag that this element has a descendant for style processing. + /// + /// Only safe to call with exclusive access to the element. + unsafe fn set_dirty_descendants(&self); + + /// Flag that this element has no descendant for style processing. + /// + /// Only safe to call with exclusive access to the element. + unsafe fn unset_dirty_descendants(&self); + + /// Similar to the dirty_descendants but for representing a descendant of + /// the element needs to be updated in animation-only traversal. + fn has_animation_only_dirty_descendants(&self) -> bool { + false + } + + /// Flag that this element has a descendant for animation-only restyle + /// processing. + /// + /// Only safe to call with exclusive access to the element. + unsafe fn set_animation_only_dirty_descendants(&self) {} + + /// Flag that this element has no descendant for animation-only restyle processing. + /// + /// Only safe to call with exclusive access to the element. + unsafe fn unset_animation_only_dirty_descendants(&self) {} + + /// Clear all bits related describing the dirtiness of descendants. + /// + /// In Gecko, this corresponds to the regular dirty descendants bit, the + /// animation-only dirty descendants bit, and the lazy frame construction + /// descendants bit. + unsafe fn clear_descendant_bits(&self) { + self.unset_dirty_descendants(); + } + + /// Returns true if this element is a visited link. + /// + /// Servo doesn't support visited styles yet. + fn is_visited_link(&self) -> bool { + false + } + + /// Returns the pseudo-element implemented by this element, if any. + /// + /// Gecko traverses pseudo-elements during the style traversal, and we need + /// to know this so we can properly grab the pseudo-element style from the + /// parent element. + /// + /// Note that we still need to compute the pseudo-elements before-hand, + /// given otherwise we don't know if we need to create an element or not. + /// + /// Servo doesn't have to deal with this. + fn implemented_pseudo_element(&self) -> Option { + None + } + + /// Atomically stores the number of children of this node that we will + /// need to process during bottom-up traversal. + fn store_children_to_process(&self, n: isize); + + /// Atomically notes that a child has been processed during bottom-up + /// traversal. Returns the number of children left to process. + fn did_process_child(&self) -> isize; + + /// Gets a reference to the ElementData container, or creates one. + /// + /// Unsafe because it can race to allocate and leak if not used with + /// exclusive access to the element. + unsafe fn ensure_data(&self) -> AtomicRefMut; + + /// Clears the element data reference, if any. + /// + /// Unsafe following the same reasoning as ensure_data. + unsafe fn clear_data(&self); + + /// Whether there is an ElementData container. + fn has_data(&self) -> bool; + + /// Immutably borrows the ElementData. + fn borrow_data(&self) -> Option>; + + /// Mutably borrows the ElementData. + fn mutate_data(&self) -> Option>; + + /// Whether we should skip any root- or item-based display property + /// blockification on this element. (This function exists so that Gecko + /// native anonymous content can opt out of this style fixup.) + fn skip_item_display_fixup(&self) -> bool; + + /// In Gecko, element has a flag that represents the element may have + /// any type of animations or not to bail out animation stuff early. + /// Whereas Servo doesn't have such flag. + fn may_have_animations(&self) -> bool; + + /// Creates a task to update various animation state on a given (pseudo-)element. + #[cfg(feature = "gecko")] + fn update_animations( + &self, + before_change_style: Option>, + tasks: UpdateAnimationsTasks, + ); + + /// Creates a task to process post animation on a given element. + #[cfg(feature = "gecko")] + fn process_post_animation(&self, tasks: PostAnimationTasks); + + /// Returns true if the element has relevant animations. Relevant + /// animations are those animations that are affecting the element's style + /// or are scheduled to do so in the future. + fn has_animations(&self, context: &SharedStyleContext) -> bool; + + /// Returns true if the element has a CSS animation. The `context` and `pseudo_element` + /// arguments are only used by Servo, since it stores animations globally and pseudo-elements + /// are not in the DOM. + fn has_css_animations( + &self, + context: &SharedStyleContext, + pseudo_element: Option, + ) -> bool; + + /// Returns true if the element has a CSS transition (including running transitions and + /// completed transitions). The `context` and `pseudo_element` arguments are only used + /// by Servo, since it stores animations globally and pseudo-elements are not in the DOM. + fn has_css_transitions( + &self, + context: &SharedStyleContext, + pseudo_element: Option, + ) -> bool; + + /// Returns true if the element has animation restyle hints. + fn has_animation_restyle_hints(&self) -> bool { + let data = match self.borrow_data() { + Some(d) => d, + None => return false, + }; + return data.hint.has_animation_hint(); + } + + /// The shadow root this element is a host of. + fn shadow_root(&self) -> Option<::ConcreteShadowRoot>; + + /// The shadow root which roots the subtree this element is contained in. + fn containing_shadow(&self) -> Option<::ConcreteShadowRoot>; + + /// Return the element which we can use to look up rules in the selector + /// maps. + /// + /// This is always the element itself, except in the case where we are an + /// element-backed pseudo-element, in which case we return the originating + /// element. + fn rule_hash_target(&self) -> Self { + if self.is_pseudo_element() { + self.pseudo_element_originating_element() + .expect("Trying to collect rules for a detached pseudo-element") + } else { + *self + } + } + + /// Executes the callback for each applicable style rule data which isn't + /// the main document's data (which stores UA / author rules). + /// + /// The element passed to the callback is the containing shadow host for the + /// data if it comes from Shadow DOM. + /// + /// Returns whether normal document author rules should apply. + /// + /// TODO(emilio): We could separate the invalidation data for elements + /// matching in other scopes to avoid over-invalidation. + fn each_applicable_non_document_style_rule_data<'a, F>(&self, mut f: F) -> bool + where + Self: 'a, + F: FnMut(&'a CascadeData, Self), + { + use crate::rule_collector::containing_shadow_ignoring_svg_use; + + let target = self.rule_hash_target(); + let matches_user_and_content_rules = target.matches_user_and_content_rules(); + let mut doc_rules_apply = matches_user_and_content_rules; + + // Use the same rules to look for the containing host as we do for rule + // collection. + if let Some(shadow) = containing_shadow_ignoring_svg_use(target) { + doc_rules_apply = false; + if let Some(data) = shadow.style_data() { + f(data, shadow.host()); + } + } + + if let Some(shadow) = target.shadow_root() { + if let Some(data) = shadow.style_data() { + f(data, shadow.host()); + } + } + + let mut current = target.assigned_slot(); + while let Some(slot) = current { + // Slots can only have assigned nodes when in a shadow tree. + let shadow = slot.containing_shadow().unwrap(); + if let Some(data) = shadow.style_data() { + if data.any_slotted_rule() { + f(data, shadow.host()); + } + } + current = slot.assigned_slot(); + } + + if target.has_part_attr() { + if let Some(mut inner_shadow) = target.containing_shadow() { + loop { + let inner_shadow_host = inner_shadow.host(); + match inner_shadow_host.containing_shadow() { + Some(shadow) => { + if let Some(data) = shadow.style_data() { + if data.any_part_rule() { + f(data, shadow.host()) + } + } + // TODO: Could be more granular. + if !inner_shadow_host.exports_any_part() { + break; + } + inner_shadow = shadow; + }, + None => { + // TODO(emilio): Should probably distinguish with + // MatchesDocumentRules::{No,Yes,IfPart} or something so that we could + // skip some work. + doc_rules_apply = matches_user_and_content_rules; + break; + }, + } + } + } + } + + doc_rules_apply + } + + /// Returns true if one of the transitions needs to be updated on this element. We check all + /// the transition properties to make sure that updating transitions is necessary. + /// This method should only be called if might_needs_transitions_update returns true when + /// passed the same parameters. + #[cfg(feature = "gecko")] + fn needs_transitions_update( + &self, + before_change_style: &ComputedValues, + after_change_style: &ComputedValues, + ) -> bool; + + /// Returns the value of the `xml:lang=""` attribute (or, if appropriate, + /// the `lang=""` attribute) on this element. + fn lang_attr(&self) -> Option; + + /// Returns whether this element's language matches the language tag + /// `value`. If `override_lang` is not `None`, it specifies the value + /// of the `xml:lang=""` or `lang=""` attribute to use in place of + /// looking at the element and its ancestors. (This argument is used + /// to implement matching of `:lang()` against snapshots.) + fn match_element_lang(&self, override_lang: Option>, value: &Lang) -> bool; + + /// Returns whether this element is the main body element of the HTML + /// document it is on. + fn is_html_document_body_element(&self) -> bool; + + /// Generate the proper applicable declarations due to presentational hints, + /// and insert them into `hints`. + fn synthesize_presentational_hints_for_legacy_attributes( + &self, + visited_handling: VisitedHandlingMode, + hints: &mut V, + ) where + V: Push; + + /// Returns element's local name. + fn local_name(&self) -> &::BorrowedLocalName; + + /// Returns element's namespace. + fn namespace(&self) + -> &::BorrowedNamespaceUrl; + + /// Returns the size of the element to be used in container size queries. + /// This will usually be the size of the content area of the primary box, + /// but can be None if there is no box or if some axis lacks size containment. + fn query_container_size( + &self, + display: &Display, + ) -> euclid::default::Size2D>; + + /// Returns true if this element anchors a relative selector, now or after + /// a DOM mutation. + fn anchors_relative_selector(&self) -> bool; +} + +/// TNode and TElement aren't Send because we want to be careful and explicit +/// about our parallel traversal. However, there are certain situations +/// (including but not limited to the traversal) where we need to send DOM +/// objects to other threads. +/// +/// That's the reason why `SendNode` exists. +#[derive(Clone, Debug, PartialEq)] +pub struct SendNode(N); +unsafe impl Send for SendNode {} +impl SendNode { + /// Unsafely construct a SendNode. + pub unsafe fn new(node: N) -> Self { + SendNode(node) + } +} +impl Deref for SendNode { + type Target = N; + fn deref(&self) -> &N { + &self.0 + } +} + +/// Same reason as for the existence of SendNode, SendElement does the proper +/// things for a given `TElement`. +#[derive(Debug, Eq, Hash, PartialEq)] +pub struct SendElement(E); +unsafe impl Send for SendElement {} +impl SendElement { + /// Unsafely construct a SendElement. + pub unsafe fn new(el: E) -> Self { + SendElement(el) + } +} +impl Deref for SendElement { + type Target = E; + fn deref(&self) -> &E { + &self.0 + } +} diff --git a/servo/components/style/dom_apis.rs b/servo/components/style/dom_apis.rs new file mode 100644 index 0000000000..4afcc813ef --- /dev/null +++ b/servo/components/style/dom_apis.rs @@ -0,0 +1,723 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Generic implementations of some DOM APIs so they can be shared between Servo +//! and Gecko. + +use crate::context::QuirksMode; +use crate::dom::{TDocument, TElement, TNode, TShadowRoot}; +use crate::invalidation::element::invalidation_map::Dependency; +use crate::invalidation::element::invalidator::{DescendantInvalidationLists, Invalidation}; +use crate::invalidation::element::invalidator::{InvalidationProcessor, InvalidationVector}; +use crate::selector_parser::SelectorImpl; +use crate::values::AtomIdent; +use selectors::attr::CaseSensitivity; +use selectors::matching::{self, MatchingContext, MatchingMode, NeedsSelectorFlags}; +use selectors::parser::{Combinator, Component, LocalName}; +use selectors::{Element, SelectorList}; +use smallvec::SmallVec; + +/// +pub fn element_matches( + element: &E, + selector_list: &SelectorList, + quirks_mode: QuirksMode, +) -> bool +where + E: Element, +{ + let mut nth_index_cache = Default::default(); + + let mut context = MatchingContext::new( + MatchingMode::Normal, + None, + &mut nth_index_cache, + quirks_mode, + NeedsSelectorFlags::No, + ); + context.scope_element = Some(element.opaque()); + context.current_host = element.containing_shadow_host().map(|e| e.opaque()); + matching::matches_selector_list(selector_list, element, &mut context) +} + +/// +pub fn element_closest( + element: E, + selector_list: &SelectorList, + quirks_mode: QuirksMode, +) -> Option +where + E: Element, +{ + let mut nth_index_cache = Default::default(); + + let mut context = MatchingContext::new( + MatchingMode::Normal, + None, + &mut nth_index_cache, + quirks_mode, + NeedsSelectorFlags::No, + ); + context.scope_element = Some(element.opaque()); + context.current_host = element.containing_shadow_host().map(|e| e.opaque()); + + let mut current = Some(element); + while let Some(element) = current.take() { + if matching::matches_selector_list(selector_list, &element, &mut context) { + return Some(element); + } + current = element.parent_element(); + } + + return None; +} + +/// A selector query abstraction, in order to be generic over QuerySelector and +/// QuerySelectorAll. +pub trait SelectorQuery { + /// The output of the query. + type Output; + + /// Whether the query should stop after the first element has been matched. + fn should_stop_after_first_match() -> bool; + + /// Append an element matching after the first query. + fn append_element(output: &mut Self::Output, element: E); + + /// Returns true if the output is empty. + fn is_empty(output: &Self::Output) -> bool; +} + +/// The result of a querySelectorAll call. +pub type QuerySelectorAllResult = SmallVec<[E; 128]>; + +/// A query for all the elements in a subtree. +pub struct QueryAll; + +impl SelectorQuery for QueryAll { + type Output = QuerySelectorAllResult; + + fn should_stop_after_first_match() -> bool { + false + } + + fn append_element(output: &mut Self::Output, element: E) { + output.push(element); + } + + fn is_empty(output: &Self::Output) -> bool { + output.is_empty() + } +} + +/// A query for the first in-tree match of all the elements in a subtree. +pub struct QueryFirst; + +impl SelectorQuery for QueryFirst { + type Output = Option; + + fn should_stop_after_first_match() -> bool { + true + } + + fn append_element(output: &mut Self::Output, element: E) { + if output.is_none() { + *output = Some(element) + } + } + + fn is_empty(output: &Self::Output) -> bool { + output.is_none() + } +} + +struct QuerySelectorProcessor<'a, E, Q> +where + E: TElement + 'a, + Q: SelectorQuery, + Q::Output: 'a, +{ + results: &'a mut Q::Output, + matching_context: MatchingContext<'a, E::Impl>, + dependencies: &'a [Dependency], +} + +impl<'a, E, Q> InvalidationProcessor<'a, E> for QuerySelectorProcessor<'a, E, Q> +where + E: TElement + 'a, + Q: SelectorQuery, + Q::Output: 'a, +{ + fn light_tree_only(&self) -> bool { + true + } + + fn check_outer_dependency(&mut self, _: &Dependency, _: E) -> bool { + debug_assert!( + false, + "How? We should only have parent-less dependencies here!" + ); + true + } + + fn collect_invalidations( + &mut self, + element: E, + self_invalidations: &mut InvalidationVector<'a>, + descendant_invalidations: &mut DescendantInvalidationLists<'a>, + _sibling_invalidations: &mut InvalidationVector<'a>, + ) -> bool { + // TODO(emilio): If the element is not a root element, and + // selector_list has any descendant combinator, we need to do extra work + // in order to handle properly things like: + // + //
+ //
+ //
+ //
+ //
+ // + // b.querySelector('#a div'); // Should return "c". + // + // For now, assert it's a root element. + debug_assert!(element.parent_element().is_none()); + + let target_vector = if self.matching_context.scope_element.is_some() { + &mut descendant_invalidations.dom_descendants + } else { + self_invalidations + }; + + for dependency in self.dependencies.iter() { + target_vector.push(Invalidation::new( + dependency, + self.matching_context.current_host.clone(), + )) + } + + false + } + + fn matching_context(&mut self) -> &mut MatchingContext<'a, E::Impl> { + &mut self.matching_context + } + + fn should_process_descendants(&mut self, _: E) -> bool { + if Q::should_stop_after_first_match() { + return Q::is_empty(&self.results); + } + + true + } + + fn invalidated_self(&mut self, e: E) { + Q::append_element(self.results, e); + } + + fn invalidated_sibling(&mut self, e: E, _of: E) { + Q::append_element(self.results, e); + } + + fn recursion_limit_exceeded(&mut self, _e: E) {} + fn invalidated_descendants(&mut self, _e: E, _child: E) {} +} + +fn collect_all_elements(root: E::ConcreteNode, results: &mut Q::Output, mut filter: F) +where + E: TElement, + Q: SelectorQuery, + F: FnMut(E) -> bool, +{ + for node in root.dom_descendants() { + let element = match node.as_element() { + Some(e) => e, + None => continue, + }; + + if !filter(element) { + continue; + } + + Q::append_element(results, element); + if Q::should_stop_after_first_match() { + return; + } + } +} + +/// Returns whether a given element connected to `root` is descendant of `root`. +/// +/// NOTE(emilio): if root == element, this returns false. +fn connected_element_is_descendant_of(element: E, root: E::ConcreteNode) -> bool +where + E: TElement, +{ + // Optimize for when the root is a document or a shadow root and the element + // is connected to that root. + if root.as_document().is_some() { + debug_assert!(element.as_node().is_in_document(), "Not connected?"); + debug_assert_eq!( + root, + root.owner_doc().as_node(), + "Where did this element come from?", + ); + return true; + } + + if root.as_shadow_root().is_some() { + debug_assert_eq!( + element.containing_shadow().unwrap().as_node(), + root, + "Not connected?" + ); + return true; + } + + let mut current = element.as_node().parent_node(); + while let Some(n) = current.take() { + if n == root { + return true; + } + + current = n.parent_node(); + } + false +} + +/// Fast path for iterating over every element with a given id in the document +/// or shadow root that `root` is connected to. +fn fast_connected_elements_with_id<'a, N>( + root: N, + id: &AtomIdent, + quirks_mode: QuirksMode, +) -> Result<&'a [N::ConcreteElement], ()> +where + N: TNode + 'a, +{ + let case_sensitivity = quirks_mode.classes_and_ids_case_sensitivity(); + if case_sensitivity != CaseSensitivity::CaseSensitive { + return Err(()); + } + + if root.is_in_document() { + return root.owner_doc().elements_with_id(id); + } + + if let Some(shadow) = root.as_shadow_root() { + return shadow.elements_with_id(id); + } + + if let Some(shadow) = root.as_element().and_then(|e| e.containing_shadow()) { + return shadow.elements_with_id(id); + } + + Err(()) +} + +/// Collects elements with a given id under `root`, that pass `filter`. +fn collect_elements_with_id( + root: E::ConcreteNode, + id: &AtomIdent, + results: &mut Q::Output, + quirks_mode: QuirksMode, + mut filter: F, +) where + E: TElement, + Q: SelectorQuery, + F: FnMut(E) -> bool, +{ + let elements = match fast_connected_elements_with_id(root, id, quirks_mode) { + Ok(elements) => elements, + Err(()) => { + let case_sensitivity = quirks_mode.classes_and_ids_case_sensitivity(); + + collect_all_elements::(root, results, |e| { + e.has_id(id, case_sensitivity) && filter(e) + }); + + return; + }, + }; + + for element in elements { + // If the element is not an actual descendant of the root, even though + // it's connected, we don't really care about it. + if !connected_element_is_descendant_of(*element, root) { + continue; + } + + if !filter(*element) { + continue; + } + + Q::append_element(results, *element); + if Q::should_stop_after_first_match() { + break; + } + } +} + +#[inline(always)] +fn local_name_matches(element: E, local_name: &LocalName) -> bool +where + E: TElement, +{ + let LocalName { + ref name, + ref lower_name, + } = *local_name; + + let chosen_name = if name == lower_name || element.is_html_element_in_html_document() { + lower_name + } else { + name + }; + + element.local_name() == &**chosen_name +} + +fn get_id(component: &Component) -> Option<&AtomIdent> { + use selectors::attr::AttrSelectorOperator; + Some(match component { + Component::ID(ref id) => id, + Component::AttributeInNoNamespace { + ref operator, + ref local_name, + ref value, + .. + } => { + if *local_name != local_name!("id") { + return None; + } + if *operator != AttrSelectorOperator::Equal { + return None; + } + AtomIdent::cast(&value.0) + }, + _ => return None, + }) +} + +/// Fast paths for querySelector with a single simple selector. +fn query_selector_single_query( + root: E::ConcreteNode, + component: &Component, + results: &mut Q::Output, + quirks_mode: QuirksMode, +) -> Result<(), ()> +where + E: TElement, + Q: SelectorQuery, +{ + // TODO: Maybe we could implement a fast path for [name=""]? + match *component { + Component::ExplicitUniversalType => { + collect_all_elements::(root, results, |_| true) + }, + Component::Class(ref class) => { + let case_sensitivity = quirks_mode.classes_and_ids_case_sensitivity(); + collect_all_elements::(root, results, |element| { + element.has_class(class, case_sensitivity) + }) + }, + Component::LocalName(ref local_name) => { + collect_all_elements::(root, results, |element| { + local_name_matches(element, local_name) + }) + }, + ref other => { + let id = match get_id(other) { + Some(id) => id, + // TODO(emilio): More fast paths? + None => return Err(()), + }; + collect_elements_with_id::(root, id, results, quirks_mode, |_| true); + }, + } + + Ok(()) +} + +enum SimpleFilter<'a> { + Class(&'a AtomIdent), + LocalName(&'a LocalName), +} + +/// Fast paths for a given selector query. +/// +/// When there's only one component, we go directly to +/// `query_selector_single_query`, otherwise, we try to optimize by looking just +/// at the subtrees rooted at ids in the selector, and otherwise we try to look +/// up by class name or local name in the rightmost compound. +/// +/// FIXME(emilio, nbp): This may very well be a good candidate for code to be +/// replaced by HolyJit :) +fn query_selector_fast( + root: E::ConcreteNode, + selector_list: &SelectorList, + results: &mut Q::Output, + matching_context: &mut MatchingContext, +) -> Result<(), ()> +where + E: TElement, + Q: SelectorQuery, +{ + // We need to return elements in document order, and reordering them + // afterwards is kinda silly. + if selector_list.0.len() > 1 { + return Err(()); + } + + let selector = &selector_list.0[0]; + let quirks_mode = matching_context.quirks_mode(); + + // Let's just care about the easy cases for now. + if selector.len() == 1 { + return query_selector_single_query::( + root, + selector.iter().next().unwrap(), + results, + quirks_mode, + ); + } + + let mut iter = selector.iter(); + let mut combinator: Option = None; + + // We want to optimize some cases where there's no id involved whatsoever, + // like `.foo .bar`, but we don't want to make `#foo .bar` slower because of + // that. + let mut simple_filter = None; + + 'selector_loop: loop { + debug_assert!(combinator.map_or(true, |c| !c.is_sibling())); + + 'component_loop: for component in &mut iter { + match *component { + Component::Class(ref class) => { + if combinator.is_none() { + simple_filter = Some(SimpleFilter::Class(class)); + } + }, + Component::LocalName(ref local_name) => { + if combinator.is_none() { + // Prefer to look at class rather than local-name if + // both are present. + if let Some(SimpleFilter::Class(..)) = simple_filter { + continue; + } + simple_filter = Some(SimpleFilter::LocalName(local_name)); + } + }, + ref other => { + if let Some(id) = get_id(other) { + if combinator.is_none() { + // In the rightmost compound, just find descendants of root that match + // the selector list with that id. + collect_elements_with_id::( + root, + id, + results, + quirks_mode, + |e| { + matching::matches_selector_list( + selector_list, + &e, + matching_context, + ) + }, + ); + return Ok(()); + } + + let elements = fast_connected_elements_with_id(root, id, quirks_mode)?; + if elements.is_empty() { + return Ok(()); + } + + // Results need to be in document order. Let's not bother + // reordering or deduplicating nodes, which we would need to + // do if one element with the given id were a descendant of + // another element with that given id. + if !Q::should_stop_after_first_match() && elements.len() > 1 { + continue; + } + + for element in elements { + // If the element is not a descendant of the root, then + // it may have descendants that match our selector that + // _are_ descendants of the root, and other descendants + // that match our selector that are _not_. + // + // So we can't just walk over the element's descendants + // and match the selector against all of them, nor can + // we skip looking at this element's descendants. + // + // Give up on trying to optimize based on this id and + // keep walking our selector. + if !connected_element_is_descendant_of(*element, root) { + continue 'component_loop; + } + + query_selector_slow::( + element.as_node(), + selector_list, + results, + matching_context, + ); + + if Q::should_stop_after_first_match() && !Q::is_empty(&results) { + break; + } + } + + return Ok(()); + } + }, + } + } + + loop { + let next_combinator = match iter.next_sequence() { + None => break 'selector_loop, + Some(c) => c, + }; + + // We don't want to scan stuff affected by sibling combinators, + // given we scan the subtree of elements with a given id (and we + // don't want to care about scanning the siblings' subtrees). + if next_combinator.is_sibling() { + // Advance to the next combinator. + for _ in &mut iter {} + continue; + } + + combinator = Some(next_combinator); + break; + } + } + + // We got here without finding any ID or such that we could handle. Try to + // use one of the simple filters. + let simple_filter = match simple_filter { + Some(f) => f, + None => return Err(()), + }; + + match simple_filter { + SimpleFilter::Class(ref class) => { + let case_sensitivity = quirks_mode.classes_and_ids_case_sensitivity(); + collect_all_elements::(root, results, |element| { + element.has_class(class, case_sensitivity) && + matching::matches_selector_list(selector_list, &element, matching_context) + }); + }, + SimpleFilter::LocalName(ref local_name) => { + collect_all_elements::(root, results, |element| { + local_name_matches(element, local_name) && + matching::matches_selector_list(selector_list, &element, matching_context) + }); + }, + } + + Ok(()) +} + +// Slow path for a given selector query. +fn query_selector_slow( + root: E::ConcreteNode, + selector_list: &SelectorList, + results: &mut Q::Output, + matching_context: &mut MatchingContext, +) where + E: TElement, + Q: SelectorQuery, +{ + collect_all_elements::(root, results, |element| { + matching::matches_selector_list(selector_list, &element, matching_context) + }); +} + +/// Whether the invalidation machinery should be used for this query. +#[derive(PartialEq)] +pub enum MayUseInvalidation { + /// We may use it if we deem it useful. + Yes, + /// Don't use it. + No, +} + +/// +pub fn query_selector( + root: E::ConcreteNode, + selector_list: &SelectorList, + results: &mut Q::Output, + may_use_invalidation: MayUseInvalidation, +) where + E: TElement, + Q: SelectorQuery, +{ + use crate::invalidation::element::invalidator::TreeStyleInvalidator; + + let mut nth_index_cache = Default::default(); + let quirks_mode = root.owner_doc().quirks_mode(); + + let mut matching_context = MatchingContext::new( + MatchingMode::Normal, + None, + &mut nth_index_cache, + quirks_mode, + NeedsSelectorFlags::No, + ); + let root_element = root.as_element(); + matching_context.scope_element = root_element.map(|e| e.opaque()); + matching_context.current_host = match root_element { + Some(root) => root.containing_shadow_host().map(|host| host.opaque()), + None => root.as_shadow_root().map(|root| root.host().opaque()), + }; + + let fast_result = + query_selector_fast::(root, selector_list, results, &mut matching_context); + + if fast_result.is_ok() { + return; + } + + // Slow path: Use the invalidation machinery if we're a root, and tree + // traversal otherwise. + // + // See the comment in collect_invalidations to see why only if we're a root. + // + // The invalidation mechanism is only useful in presence of combinators. + // + // We could do that check properly here, though checking the length of the + // selectors is a good heuristic. + // + // A selector with a combinator needs to have a length of at least 3: A + // simple selector, a combinator, and another simple selector. + let invalidation_may_be_useful = may_use_invalidation == MayUseInvalidation::Yes && + selector_list.0.iter().any(|s| s.len() > 2); + + if root_element.is_some() || !invalidation_may_be_useful { + query_selector_slow::(root, selector_list, results, &mut matching_context); + } else { + let dependencies = selector_list + .0 + .iter() + .map(|selector| Dependency::for_full_selector_invalidation(selector.clone())) + .collect::>(); + let mut processor = QuerySelectorProcessor:: { + results, + matching_context, + dependencies: &dependencies, + }; + + for node in root.dom_children() { + if let Some(e) = node.as_element() { + TreeStyleInvalidator::new(e, /* stack_limit_checker = */ None, &mut processor) + .invalidate(); + } + } + } +} diff --git a/servo/components/style/driver.rs b/servo/components/style/driver.rs new file mode 100644 index 0000000000..7b80a20e40 --- /dev/null +++ b/servo/components/style/driver.rs @@ -0,0 +1,195 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Implements traversal over the DOM tree. The traversal starts in sequential +//! mode, and optionally parallelizes as it discovers work. + +#![deny(missing_docs)] + +use crate::context::{PerThreadTraversalStatistics, StyleContext}; +use crate::context::{ThreadLocalStyleContext, TraversalStatistics}; +use crate::dom::{SendNode, TElement, TNode}; +use crate::parallel; +use crate::parallel::{work_unit_max, DispatchMode}; +use crate::scoped_tls::ScopedTLS; +use crate::traversal::{DomTraversal, PerLevelTraversalData, PreTraverseToken}; +use rayon; +use std::collections::VecDeque; +use std::mem; +use time; + +#[cfg(feature = "servo")] +fn should_report_statistics() -> bool { + false +} + +#[cfg(feature = "gecko")] +fn should_report_statistics() -> bool { + unsafe { crate::gecko_bindings::structs::ServoTraversalStatistics_sActive } +} + +#[cfg(feature = "servo")] +fn report_statistics(_stats: &PerThreadTraversalStatistics) { + unreachable!("Servo never report stats"); +} + +#[cfg(feature = "gecko")] +fn report_statistics(stats: &PerThreadTraversalStatistics) { + // This should only be called in the main thread, or it may be racy + // to update the statistics in a global variable. + debug_assert!(unsafe { crate::gecko_bindings::bindings::Gecko_IsMainThread() }); + let gecko_stats = + unsafe { &mut crate::gecko_bindings::structs::ServoTraversalStatistics_sSingleton }; + gecko_stats.mElementsTraversed += stats.elements_traversed; + gecko_stats.mElementsStyled += stats.elements_styled; + gecko_stats.mElementsMatched += stats.elements_matched; + gecko_stats.mStylesShared += stats.styles_shared; + gecko_stats.mStylesReused += stats.styles_reused; +} + +fn parallelism_threshold() -> usize { + static_prefs::pref!("layout.css.stylo-parallelism-threshold") as usize +} + +/// Do a DOM traversal for top-down and (optionally) bottom-up processing, generic over `D`. +/// +/// We use an adaptive traversal strategy. We start out with simple sequential processing, until we +/// arrive at a wide enough level in the DOM that the parallel traversal would parallelize it. +/// If a thread pool is provided, we then transfer control over to the parallel traversal. +/// +/// Returns true if the traversal was parallel, and also returns the statistics object containing +/// information on nodes traversed (on nightly only). Not all of its fields will be initialized +/// since we don't call finish(). +pub fn traverse_dom( + traversal: &D, + token: PreTraverseToken, + pool: Option<&rayon::ThreadPool>, +) -> E +where + E: TElement, + D: DomTraversal, +{ + let root = token + .traversal_root() + .expect("Should've ensured we needed to traverse"); + + let report_stats = should_report_statistics(); + let dump_stats = traversal.shared_context().options.dump_style_statistics; + let start_time = if dump_stats { + Some(time::precise_time_s()) + } else { + None + }; + + // Declare the main-thread context, as well as the worker-thread contexts, + // which we may or may not instantiate. It's important to declare the worker- + // thread contexts first, so that they get dropped second. This matters because: + // * ThreadLocalContexts borrow AtomicRefCells in TLS. + // * Dropping a ThreadLocalContext can run SequentialTasks. + // * Sequential tasks may call into functions like + // Servo_StyleSet_GetBaseComputedValuesForElement, which instantiate a + // ThreadLocalStyleContext on the main thread. If the main thread + // ThreadLocalStyleContext has not released its TLS borrow by that point, + // we'll panic on double-borrow. + let mut tls_slots = None; + let mut tlc = ThreadLocalStyleContext::new(); + let mut context = StyleContext { + shared: traversal.shared_context(), + thread_local: &mut tlc, + }; + + // Process the nodes breadth-first, just like the parallel traversal does. + // This helps keep similar traversal characteristics for the style sharing + // cache. + let work_unit_max = work_unit_max(); + let parallelism_threshold = parallelism_threshold(); + let mut discovered = VecDeque::>::with_capacity(work_unit_max * 2); + let mut depth = root.depth(); + let mut nodes_remaining_at_current_depth = 1; + discovered.push_back(unsafe { SendNode::new(root.as_node()) }); + while let Some(node) = discovered.pop_front() { + let mut children_to_process = 0isize; + let traversal_data = PerLevelTraversalData { + current_dom_depth: depth, + }; + traversal.process_preorder(&traversal_data, &mut context, *node, |n| { + children_to_process += 1; + discovered.push_back(unsafe { SendNode::new(n) }); + }); + + traversal.handle_postorder_traversal( + &mut context, + root.as_node().opaque(), + *node, + children_to_process, + ); + + nodes_remaining_at_current_depth -= 1; + + // If there is enough work to parallelize over, and the caller allows parallelism, switch + // to the parallel driver. We do this only when moving to the next level in the dom so that + // we can pass the same depth for all the children. + if nodes_remaining_at_current_depth != 0 { + continue; + } + depth += 1; + if pool.is_some() && discovered.len() > parallelism_threshold && parallelism_threshold > 0 { + let pool = pool.unwrap(); + let tls = ScopedTLS::>::new(pool); + let root_opaque = root.as_node().opaque(); + pool.scope_fifo(|scope| { + // Enable a breadth-first rayon traversal. This causes the work + // queue to be always FIFO, rather than FIFO for stealers and + // FILO for the owner (which is what rayon does by default). This + // ensures that we process all the elements at a given depth before + // proceeding to the next depth, which is important for style sharing. + gecko_profiler_label!(Layout, StyleComputation); + parallel::traverse_nodes( + discovered.make_contiguous(), + DispatchMode::TailCall, + /* recursion_ok = */ true, + root_opaque, + PerLevelTraversalData { + current_dom_depth: depth, + }, + scope, + pool, + traversal, + &tls, + ); + }); + + tls_slots = Some(tls.into_slots()); + break; + } + nodes_remaining_at_current_depth = discovered.len(); + } + + // Collect statistics from thread-locals if requested. + if dump_stats || report_stats { + let mut aggregate = mem::replace(&mut context.thread_local.statistics, Default::default()); + let parallel = tls_slots.is_some(); + if let Some(ref mut tls) = tls_slots { + for slot in tls.iter_mut() { + if let Some(cx) = slot.get_mut() { + aggregate += cx.statistics.clone(); + } + } + } + + if report_stats { + report_statistics(&aggregate); + } + // dump statistics to stdout if requested + if dump_stats { + let stats = + TraversalStatistics::new(aggregate, traversal, parallel, start_time.unwrap()); + if stats.is_large { + println!("{}", stats); + } + } + } + + root +} diff --git a/servo/components/style/encoding_support.rs b/servo/components/style/encoding_support.rs new file mode 100644 index 0000000000..c144ad0b3b --- /dev/null +++ b/servo/components/style/encoding_support.rs @@ -0,0 +1,105 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Parsing stylesheets from bytes (not `&str`). + +use crate::context::QuirksMode; +use crate::error_reporting::ParseErrorReporter; +use crate::media_queries::MediaList; +use crate::shared_lock::SharedRwLock; +use crate::stylesheets::{AllowImportRules, Origin, Stylesheet, StylesheetLoader, UrlExtraData}; +use cssparser::{stylesheet_encoding, EncodingSupport}; +use servo_arc::Arc; +use std::borrow::Cow; +use std::str; + +struct EncodingRs; + +impl EncodingSupport for EncodingRs { + type Encoding = &'static encoding_rs::Encoding; + + fn utf8() -> Self::Encoding { + encoding_rs::UTF_8 + } + + fn is_utf16_be_or_le(encoding: &Self::Encoding) -> bool { + *encoding == encoding_rs::UTF_16LE || *encoding == encoding_rs::UTF_16BE + } + + fn from_label(ascii_label: &[u8]) -> Option { + encoding_rs::Encoding::for_label(ascii_label) + } +} + +fn decode_stylesheet_bytes<'a>( + css: &'a [u8], + protocol_encoding_label: Option<&str>, + environment_encoding: Option<&'static encoding_rs::Encoding>, +) -> Cow<'a, str> { + let fallback_encoding = stylesheet_encoding::( + css, + protocol_encoding_label.map(str::as_bytes), + environment_encoding, + ); + let (result, _used_encoding, _) = fallback_encoding.decode(&css); + // FIXME record used encoding for environment encoding of @import + result +} + +impl Stylesheet { + /// Parse a stylesheet from a set of bytes, potentially received over the + /// network. + /// + /// Takes care of decoding the network bytes and forwards the resulting + /// string to `Stylesheet::from_str`. + pub fn from_bytes( + bytes: &[u8], + url_data: UrlExtraData, + protocol_encoding_label: Option<&str>, + environment_encoding: Option<&'static encoding_rs::Encoding>, + origin: Origin, + media: MediaList, + shared_lock: SharedRwLock, + stylesheet_loader: Option<&dyn StylesheetLoader>, + error_reporter: Option<&dyn ParseErrorReporter>, + quirks_mode: QuirksMode, + ) -> Stylesheet { + let string = decode_stylesheet_bytes(bytes, protocol_encoding_label, environment_encoding); + Stylesheet::from_str( + &string, + url_data, + origin, + Arc::new(shared_lock.wrap(media)), + shared_lock, + stylesheet_loader, + error_reporter, + quirks_mode, + 0, + AllowImportRules::Yes, + ) + } + + /// Updates an empty stylesheet with a set of bytes that reached over the + /// network. + pub fn update_from_bytes( + existing: &Stylesheet, + bytes: &[u8], + protocol_encoding_label: Option<&str>, + environment_encoding: Option<&'static encoding_rs::Encoding>, + url_data: UrlExtraData, + stylesheet_loader: Option<&dyn StylesheetLoader>, + error_reporter: Option<&dyn ParseErrorReporter>, + ) { + let string = decode_stylesheet_bytes(bytes, protocol_encoding_label, environment_encoding); + Self::update_from_str( + existing, + &string, + url_data, + stylesheet_loader, + error_reporter, + 0, + AllowImportRules::Yes, + ) + } +} diff --git a/servo/components/style/error_reporting.rs b/servo/components/style/error_reporting.rs new file mode 100644 index 0000000000..258c7c44ef --- /dev/null +++ b/servo/components/style/error_reporting.rs @@ -0,0 +1,283 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Types used to report parsing errors. + +#![deny(missing_docs)] + +use crate::selector_parser::SelectorImpl; +use crate::stylesheets::UrlExtraData; +use cssparser::{BasicParseErrorKind, ParseErrorKind, SourceLocation, Token}; +use selectors::SelectorList; +use std::fmt; +use style_traits::ParseError; + +/// Errors that can be encountered while parsing CSS. +#[derive(Debug)] +pub enum ContextualParseError<'a> { + /// A property declaration was not recognized. + UnsupportedPropertyDeclaration( + &'a str, + ParseError<'a>, + Option<&'a SelectorList>, + ), + /// A property descriptor was not recognized. + UnsupportedPropertyDescriptor(&'a str, ParseError<'a>), + /// A font face descriptor was not recognized. + UnsupportedFontFaceDescriptor(&'a str, ParseError<'a>), + /// A font feature values descriptor was not recognized. + UnsupportedFontFeatureValuesDescriptor(&'a str, ParseError<'a>), + /// A font palette values descriptor was not recognized. + UnsupportedFontPaletteValuesDescriptor(&'a str, ParseError<'a>), + /// A keyframe rule was not valid. + InvalidKeyframeRule(&'a str, ParseError<'a>), + /// A font feature values rule was not valid. + InvalidFontFeatureValuesRule(&'a str, ParseError<'a>), + /// A keyframe property declaration was not recognized. + UnsupportedKeyframePropertyDeclaration(&'a str, ParseError<'a>), + /// A rule was invalid for some reason. + InvalidRule(&'a str, ParseError<'a>), + /// A rule was not recognized. + UnsupportedRule(&'a str, ParseError<'a>), + /// A viewport descriptor declaration was not recognized. + UnsupportedViewportDescriptorDeclaration(&'a str, ParseError<'a>), + /// A counter style descriptor declaration was not recognized. + UnsupportedCounterStyleDescriptorDeclaration(&'a str, ParseError<'a>), + /// A counter style rule had no symbols. + InvalidCounterStyleWithoutSymbols(String), + /// A counter style rule had less than two symbols. + InvalidCounterStyleNotEnoughSymbols(String), + /// A counter style rule did not have additive-symbols. + InvalidCounterStyleWithoutAdditiveSymbols, + /// A counter style rule had extends with symbols. + InvalidCounterStyleExtendsWithSymbols, + /// A counter style rule had extends with additive-symbols. + InvalidCounterStyleExtendsWithAdditiveSymbols, + /// A media rule was invalid for some reason. + InvalidMediaRule(&'a str, ParseError<'a>), + /// A value was not recognized. + UnsupportedValue(&'a str, ParseError<'a>), + /// A never-matching `:host` selector was found. + NeverMatchingHostSelector(String), +} + +impl<'a> fmt::Display for ContextualParseError<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn token_to_str(t: &Token, f: &mut fmt::Formatter) -> fmt::Result { + match *t { + Token::Ident(ref i) => write!(f, "identifier {}", i), + Token::AtKeyword(ref kw) => write!(f, "keyword @{}", kw), + Token::Hash(ref h) => write!(f, "hash #{}", h), + Token::IDHash(ref h) => write!(f, "id selector #{}", h), + Token::QuotedString(ref s) => write!(f, "quoted string \"{}\"", s), + Token::UnquotedUrl(ref u) => write!(f, "url {}", u), + Token::Delim(ref d) => write!(f, "delimiter {}", d), + Token::Number { + int_value: Some(i), .. + } => write!(f, "number {}", i), + Token::Number { value, .. } => write!(f, "number {}", value), + Token::Percentage { + int_value: Some(i), .. + } => write!(f, "percentage {}", i), + Token::Percentage { unit_value, .. } => { + write!(f, "percentage {}", unit_value * 100.) + }, + Token::Dimension { + value, ref unit, .. + } => write!(f, "dimension {}{}", value, unit), + Token::WhiteSpace(_) => write!(f, "whitespace"), + Token::Comment(_) => write!(f, "comment"), + Token::Colon => write!(f, "colon (:)"), + Token::Semicolon => write!(f, "semicolon (;)"), + Token::Comma => write!(f, "comma (,)"), + Token::IncludeMatch => write!(f, "include match (~=)"), + Token::DashMatch => write!(f, "dash match (|=)"), + Token::PrefixMatch => write!(f, "prefix match (^=)"), + Token::SuffixMatch => write!(f, "suffix match ($=)"), + Token::SubstringMatch => write!(f, "substring match (*=)"), + Token::CDO => write!(f, "CDO ()"), + Token::Function(ref name) => write!(f, "function {}", name), + Token::ParenthesisBlock => write!(f, "parenthesis ("), + Token::SquareBracketBlock => write!(f, "square bracket ["), + Token::CurlyBracketBlock => write!(f, "curly bracket {{"), + Token::BadUrl(ref _u) => write!(f, "bad url parse error"), + Token::BadString(ref _s) => write!(f, "bad string parse error"), + Token::CloseParenthesis => write!(f, "unmatched close parenthesis"), + Token::CloseSquareBracket => write!(f, "unmatched close square bracket"), + Token::CloseCurlyBracket => write!(f, "unmatched close curly bracket"), + } + } + + fn parse_error_to_str(err: &ParseError, f: &mut fmt::Formatter) -> fmt::Result { + match err.kind { + ParseErrorKind::Basic(BasicParseErrorKind::UnexpectedToken(ref t)) => { + write!(f, "found unexpected ")?; + token_to_str(t, f) + }, + ParseErrorKind::Basic(BasicParseErrorKind::EndOfInput) => { + write!(f, "unexpected end of input") + }, + ParseErrorKind::Basic(BasicParseErrorKind::AtRuleInvalid(ref i)) => { + write!(f, "@ rule invalid: {}", i) + }, + ParseErrorKind::Basic(BasicParseErrorKind::AtRuleBodyInvalid) => { + write!(f, "@ rule invalid") + }, + ParseErrorKind::Basic(BasicParseErrorKind::QualifiedRuleInvalid) => { + write!(f, "qualified rule invalid") + }, + ParseErrorKind::Custom(ref err) => write!(f, "{:?}", err), + } + } + + match *self { + ContextualParseError::UnsupportedPropertyDeclaration(decl, ref err, _selectors) => { + write!(f, "Unsupported property declaration: '{}', ", decl)?; + parse_error_to_str(err, f) + }, + ContextualParseError::UnsupportedPropertyDescriptor(decl, ref err) => { + write!( + f, + "Unsupported @property descriptor declaration: '{}', ", + decl + )?; + parse_error_to_str(err, f) + }, + ContextualParseError::UnsupportedFontFaceDescriptor(decl, ref err) => { + write!( + f, + "Unsupported @font-face descriptor declaration: '{}', ", + decl + )?; + parse_error_to_str(err, f) + }, + ContextualParseError::UnsupportedFontFeatureValuesDescriptor(decl, ref err) => { + write!( + f, + "Unsupported @font-feature-values descriptor declaration: '{}', ", + decl + )?; + parse_error_to_str(err, f) + }, + ContextualParseError::UnsupportedFontPaletteValuesDescriptor(decl, ref err) => { + write!( + f, + "Unsupported @font-palette-values descriptor declaration: '{}', ", + decl + )?; + parse_error_to_str(err, f) + }, + ContextualParseError::InvalidKeyframeRule(rule, ref err) => { + write!(f, "Invalid keyframe rule: '{}', ", rule)?; + parse_error_to_str(err, f) + }, + ContextualParseError::InvalidFontFeatureValuesRule(rule, ref err) => { + write!(f, "Invalid font feature value rule: '{}', ", rule)?; + parse_error_to_str(err, f) + }, + ContextualParseError::UnsupportedKeyframePropertyDeclaration(decl, ref err) => { + write!(f, "Unsupported keyframe property declaration: '{}', ", decl)?; + parse_error_to_str(err, f) + }, + ContextualParseError::InvalidRule(rule, ref err) => { + write!(f, "Invalid rule: '{}', ", rule)?; + parse_error_to_str(err, f) + }, + ContextualParseError::UnsupportedRule(rule, ref err) => { + write!(f, "Unsupported rule: '{}', ", rule)?; + parse_error_to_str(err, f) + }, + ContextualParseError::UnsupportedViewportDescriptorDeclaration(decl, ref err) => { + write!( + f, + "Unsupported @viewport descriptor declaration: '{}', ", + decl + )?; + parse_error_to_str(err, f) + }, + ContextualParseError::UnsupportedCounterStyleDescriptorDeclaration(decl, ref err) => { + write!( + f, + "Unsupported @counter-style descriptor declaration: '{}', ", + decl + )?; + parse_error_to_str(err, f) + }, + ContextualParseError::InvalidCounterStyleWithoutSymbols(ref system) => write!( + f, + "Invalid @counter-style rule: 'system: {}' without 'symbols'", + system + ), + ContextualParseError::InvalidCounterStyleNotEnoughSymbols(ref system) => write!( + f, + "Invalid @counter-style rule: 'system: {}' less than two 'symbols'", + system + ), + ContextualParseError::InvalidCounterStyleWithoutAdditiveSymbols => write!( + f, + "Invalid @counter-style rule: 'system: additive' without 'additive-symbols'" + ), + ContextualParseError::InvalidCounterStyleExtendsWithSymbols => write!( + f, + "Invalid @counter-style rule: 'system: extends …' with 'symbols'" + ), + ContextualParseError::InvalidCounterStyleExtendsWithAdditiveSymbols => write!( + f, + "Invalid @counter-style rule: 'system: extends …' with 'additive-symbols'" + ), + ContextualParseError::InvalidMediaRule(media_rule, ref err) => { + write!(f, "Invalid media rule: {}, ", media_rule)?; + parse_error_to_str(err, f) + }, + ContextualParseError::UnsupportedValue(_value, ref err) => parse_error_to_str(err, f), + ContextualParseError::NeverMatchingHostSelector(ref selector) => { + write!(f, ":host selector is not featureless: {}", selector) + }, + } + } +} + +/// A generic trait for an error reporter. +pub trait ParseErrorReporter { + /// Called when the style engine detects an error. + /// + /// Returns the current input being parsed, the source location it was + /// reported from, and a message. + fn report_error( + &self, + url: &UrlExtraData, + location: SourceLocation, + error: ContextualParseError, + ); +} + +/// An error reporter that uses [the `log` crate](https://github.com/rust-lang-nursery/log) +/// at `info` level. +/// +/// This logging is silent by default, and can be enabled with a `RUST_LOG=style=info` +/// environment variable. +/// (See [`env_logger`](https://rust-lang-nursery.github.io/log/env_logger/).) +#[cfg(feature = "servo")] +pub struct RustLogReporter; + +#[cfg(feature = "servo")] +impl ParseErrorReporter for RustLogReporter { + fn report_error( + &self, + url: &UrlExtraData, + location: SourceLocation, + error: ContextualParseError, + ) { + if log_enabled!(log::Level::Info) { + info!( + "Url:\t{}\n{}:{} {}", + url.as_str(), + location.line, + location.column, + error + ) + } + } +} diff --git a/servo/components/style/font_face.rs b/servo/components/style/font_face.rs new file mode 100644 index 0000000000..a4e919ef89 --- /dev/null +++ b/servo/components/style/font_face.rs @@ -0,0 +1,818 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! The [`@font-face`][ff] at-rule. +//! +//! [ff]: https://drafts.csswg.org/css-fonts/#at-font-face-rule + +use crate::error_reporting::ContextualParseError; +use crate::parser::{Parse, ParserContext}; +#[cfg(feature = "gecko")] +use crate::properties::longhands::font_language_override; +use crate::shared_lock::{SharedRwLockReadGuard, ToCssWithGuard}; +use crate::str::CssStringWriter; +use crate::values::computed::font::{FamilyName, FontStretch}; +use crate::values::generics::font::FontStyle as GenericFontStyle; +use crate::values::specified::font::SpecifiedFontStyle; +use crate::values::specified::font::{ + AbsoluteFontWeight, FontStretch as SpecifiedFontStretch, MetricsOverride, +}; +#[cfg(feature = "gecko")] +use crate::values::specified::font::{FontFeatureSettings, FontVariationSettings}; +use crate::values::specified::url::SpecifiedUrl; +use crate::values::specified::{Angle, NonNegativePercentage}; +#[cfg(feature = "gecko")] +use cssparser::UnicodeRange; +use cssparser::{ + AtRuleParser, CowRcStr, DeclarationParser, Parser, QualifiedRuleParser, RuleBodyItemParser, + RuleBodyParser, SourceLocation, +}; +use selectors::parser::SelectorParseErrorKind; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError}; +use style_traits::{StyleParseErrorKind, ToCss}; + +/// A source for a font-face rule. +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive(Clone, Debug, Eq, PartialEq, ToCss, ToShmem)] +pub enum Source { + /// A `url()` source. + Url(UrlSource), + /// A `local()` source. + #[css(function)] + Local(FamilyName), +} + +/// A list of sources for the font-face src descriptor. +#[derive(Clone, Debug, Eq, PartialEq, ToCss, ToShmem)] +#[css(comma)] +pub struct SourceList(#[css(iterable)] pub Vec); + +// We can't just use OneOrMoreSeparated to derive Parse for the Source list, +// because we want to filter out components that parsed as None, then fail if no +// valid components remain. So we provide our own implementation here. +impl Parse for SourceList { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + // Parse the comma-separated list, then let filter_map discard any None items. + let list = input + .parse_comma_separated(|input| { + let s = input.parse_entirely(|input| Source::parse(context, input)); + while input.next().is_ok() {} + Ok(s.ok()) + })? + .into_iter() + .filter_map(|s| s) + .collect::>(); + if list.is_empty() { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } else { + Ok(SourceList(list)) + } + } +} + +/// Keywords for the font-face src descriptor's format() function. +/// ('None' and 'Unknown' are for internal use in gfx, not exposed to CSS.) +#[derive(Clone, Copy, Debug, Eq, Parse, PartialEq, ToCss, ToShmem)] +#[repr(u8)] +#[allow(missing_docs)] +pub enum FontFaceSourceFormatKeyword { + #[css(skip)] + None, + Collection, + EmbeddedOpentype, + Opentype, + Svg, + Truetype, + Woff, + Woff2, + #[css(skip)] + Unknown, +} + +bitflags! { + /// Flags for the @font-face tech() function, indicating font technologies + /// required by the resource. + #[derive(Clone, Copy, Debug, Eq, PartialEq, ToShmem)] + #[repr(C)] + pub struct FontFaceSourceTechFlags: u16 { + /// Font requires OpenType feature support. + const FEATURES_OPENTYPE = 1 << 0; + /// Font requires Apple Advanced Typography support. + const FEATURES_AAT = 1 << 1; + /// Font requires Graphite shaping support. + const FEATURES_GRAPHITE = 1 << 2; + /// Font requires COLRv0 rendering support (simple list of colored layers). + const COLOR_COLRV0 = 1 << 3; + /// Font requires COLRv1 rendering support (graph of paint operations). + const COLOR_COLRV1 = 1 << 4; + /// Font requires SVG glyph rendering support. + const COLOR_SVG = 1 << 5; + /// Font has bitmap glyphs in 'sbix' format. + const COLOR_SBIX = 1 << 6; + /// Font has bitmap glyphs in 'CBDT' format. + const COLOR_CBDT = 1 << 7; + /// Font requires OpenType Variations support. + const VARIATIONS = 1 << 8; + /// Font requires CPAL palette selection support. + const PALETTES = 1 << 9; + /// Font requires support for incremental downloading. + const INCREMENTAL = 1 << 10; + } +} + +impl FontFaceSourceTechFlags { + /// Parse a single font-technology keyword and return its flag. + pub fn parse_one<'i, 't>(input: &mut Parser<'i, 't>) -> Result> { + Ok(try_match_ident_ignore_ascii_case! { input, + "features-opentype" => Self::FEATURES_OPENTYPE, + "features-aat" => Self::FEATURES_AAT, + "features-graphite" => Self::FEATURES_GRAPHITE, + "color-colrv0" => Self::COLOR_COLRV0, + "color-colrv1" => Self::COLOR_COLRV1, + "color-svg" => Self::COLOR_SVG, + "color-sbix" => Self::COLOR_SBIX, + "color-cbdt" => Self::COLOR_CBDT, + "variations" => Self::VARIATIONS, + "palettes" => Self::PALETTES, + "incremental" => Self::INCREMENTAL, + }) + } +} + +impl Parse for FontFaceSourceTechFlags { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + let location = input.current_source_location(); + // We don't actually care about the return value of parse_comma_separated, + // because we insert the flags into result as we go. + let mut result = Self::empty(); + input.parse_comma_separated(|input| { + let flag = Self::parse_one(input)?; + result.insert(flag); + Ok(()) + })?; + if !result.is_empty() { + Ok(result) + } else { + Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } +} + +#[allow(unused_assignments)] +impl ToCss for FontFaceSourceTechFlags { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: fmt::Write, + { + let mut first = true; + + macro_rules! write_if_flag { + ($s:expr => $f:ident) => { + if self.contains(Self::$f) { + if first { + first = false; + } else { + dest.write_str(", ")?; + } + dest.write_str($s)?; + } + }; + } + + write_if_flag!("features-opentype" => FEATURES_OPENTYPE); + write_if_flag!("features-aat" => FEATURES_AAT); + write_if_flag!("features-graphite" => FEATURES_GRAPHITE); + write_if_flag!("color-colrv0" => COLOR_COLRV0); + write_if_flag!("color-colrv1" => COLOR_COLRV1); + write_if_flag!("color-svg" => COLOR_SVG); + write_if_flag!("color-sbix" => COLOR_SBIX); + write_if_flag!("color-cbdt" => COLOR_CBDT); + write_if_flag!("variations" => VARIATIONS); + write_if_flag!("palettes" => PALETTES); + write_if_flag!("incremental" => INCREMENTAL); + + Ok(()) + } +} + +/// A POD representation for Gecko. All pointers here are non-owned and as such +/// can't outlive the rule they came from, but we can't enforce that via C++. +/// +/// All the strings are of course utf8. +#[cfg(feature = "gecko")] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(u8)] +#[allow(missing_docs)] +pub enum FontFaceSourceListComponent { + Url(*const crate::gecko::url::CssUrl), + Local(*mut crate::gecko_bindings::structs::nsAtom), + FormatHintKeyword(FontFaceSourceFormatKeyword), + FormatHintString { + length: usize, + utf8_bytes: *const u8, + }, + TechFlags(FontFaceSourceTechFlags), +} + +#[derive(Clone, Debug, Eq, PartialEq, ToCss, ToShmem)] +#[repr(u8)] +#[allow(missing_docs)] +pub enum FontFaceSourceFormat { + Keyword(FontFaceSourceFormatKeyword), + String(String), +} + +/// A `UrlSource` represents a font-face source that has been specified with a +/// `url()` function. +/// +/// +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive(Clone, Debug, Eq, PartialEq, ToShmem)] +pub struct UrlSource { + /// The specified url. + pub url: SpecifiedUrl, + /// The format hint specified with the `format()` function, if present. + pub format_hint: Option, + /// The font technology flags specified with the `tech()` function, if any. + pub tech_flags: FontFaceSourceTechFlags, +} + +impl ToCss for UrlSource { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: fmt::Write, + { + self.url.to_css(dest)?; + if let Some(hint) = &self.format_hint { + dest.write_str(" format(")?; + hint.to_css(dest)?; + dest.write_char(')')?; + } + if !self.tech_flags.is_empty() { + dest.write_str(" tech(")?; + self.tech_flags.to_css(dest)?; + dest.write_char(')')?; + } + Ok(()) + } +} + +/// A font-display value for a @font-face rule. +/// The font-display descriptor determines how a font face is displayed based +/// on whether and when it is downloaded and ready to use. +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, ToComputedValue, ToCss, ToShmem, +)] +#[repr(u8)] +pub enum FontDisplay { + Auto, + Block, + Swap, + Fallback, + Optional, +} + +macro_rules! impl_range { + ($range:ident, $component:ident) => { + impl Parse for $range { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + let first = $component::parse(context, input)?; + let second = input + .try_parse(|input| $component::parse(context, input)) + .unwrap_or_else(|_| first.clone()); + Ok($range(first, second)) + } + } + impl ToCss for $range { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: fmt::Write, + { + self.0.to_css(dest)?; + if self.0 != self.1 { + dest.write_char(' ')?; + self.1.to_css(dest)?; + } + Ok(()) + } + } + }; +} + +/// The font-weight descriptor: +/// +/// https://drafts.csswg.org/css-fonts-4/#descdef-font-face-font-weight +#[derive(Clone, Debug, PartialEq, ToShmem)] +pub struct FontWeightRange(pub AbsoluteFontWeight, pub AbsoluteFontWeight); +impl_range!(FontWeightRange, AbsoluteFontWeight); + +/// The computed representation of the above so Gecko can read them easily. +/// +/// This one is needed because cbindgen doesn't know how to generate +/// specified::Number. +#[repr(C)] +#[allow(missing_docs)] +pub struct ComputedFontWeightRange(f32, f32); + +#[inline] +fn sort_range(a: T, b: T) -> (T, T) { + if a > b { + (b, a) + } else { + (a, b) + } +} + +impl FontWeightRange { + /// Returns a computed font-stretch range. + pub fn compute(&self) -> ComputedFontWeightRange { + let (min, max) = sort_range(self.0.compute().value(), self.1.compute().value()); + ComputedFontWeightRange(min, max) + } +} + +/// The font-stretch descriptor: +/// +/// https://drafts.csswg.org/css-fonts-4/#descdef-font-face-font-stretch +#[derive(Clone, Debug, PartialEq, ToShmem)] +pub struct FontStretchRange(pub SpecifiedFontStretch, pub SpecifiedFontStretch); +impl_range!(FontStretchRange, SpecifiedFontStretch); + +/// The computed representation of the above, so that Gecko can read them +/// easily. +#[repr(C)] +#[allow(missing_docs)] +pub struct ComputedFontStretchRange(FontStretch, FontStretch); + +impl FontStretchRange { + /// Returns a computed font-stretch range. + pub fn compute(&self) -> ComputedFontStretchRange { + fn compute_stretch(s: &SpecifiedFontStretch) -> FontStretch { + match *s { + SpecifiedFontStretch::Keyword(ref kw) => kw.compute(), + SpecifiedFontStretch::Stretch(ref p) => FontStretch::from_percentage(p.0.get()), + SpecifiedFontStretch::System(..) => unreachable!(), + } + } + + let (min, max) = sort_range(compute_stretch(&self.0), compute_stretch(&self.1)); + ComputedFontStretchRange(min, max) + } +} + +/// The font-style descriptor: +/// +/// https://drafts.csswg.org/css-fonts-4/#descdef-font-face-font-style +#[derive(Clone, Debug, PartialEq, ToShmem)] +#[allow(missing_docs)] +pub enum FontStyle { + Normal, + Italic, + Oblique(Angle, Angle), +} + +/// The computed representation of the above, with angles in degrees, so that +/// Gecko can read them easily. +#[repr(u8)] +#[allow(missing_docs)] +pub enum ComputedFontStyleDescriptor { + Normal, + Italic, + Oblique(f32, f32), +} + +impl Parse for FontStyle { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + let style = SpecifiedFontStyle::parse(context, input)?; + Ok(match style { + GenericFontStyle::Normal => FontStyle::Normal, + GenericFontStyle::Italic => FontStyle::Italic, + GenericFontStyle::Oblique(angle) => { + let second_angle = input + .try_parse(|input| SpecifiedFontStyle::parse_angle(context, input)) + .unwrap_or_else(|_| angle.clone()); + + FontStyle::Oblique(angle, second_angle) + }, + }) + } +} + +impl ToCss for FontStyle { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: fmt::Write, + { + match *self { + FontStyle::Normal => dest.write_str("normal"), + FontStyle::Italic => dest.write_str("italic"), + FontStyle::Oblique(ref first, ref second) => { + dest.write_str("oblique")?; + if *first != SpecifiedFontStyle::default_angle() || first != second { + dest.write_char(' ')?; + first.to_css(dest)?; + } + if first != second { + dest.write_char(' ')?; + second.to_css(dest)?; + } + Ok(()) + }, + } + } +} + +impl FontStyle { + /// Returns a computed font-style descriptor. + pub fn compute(&self) -> ComputedFontStyleDescriptor { + match *self { + FontStyle::Normal => ComputedFontStyleDescriptor::Normal, + FontStyle::Italic => ComputedFontStyleDescriptor::Italic, + FontStyle::Oblique(ref first, ref second) => { + let (min, max) = sort_range( + SpecifiedFontStyle::compute_angle_degrees(first), + SpecifiedFontStyle::compute_angle_degrees(second), + ); + ComputedFontStyleDescriptor::Oblique(min, max) + }, + } + } +} + +/// Parse the block inside a `@font-face` rule. +/// +/// Note that the prelude parsing code lives in the `stylesheets` module. +pub fn parse_font_face_block( + context: &ParserContext, + input: &mut Parser, + location: SourceLocation, +) -> FontFaceRuleData { + let mut rule = FontFaceRuleData::empty(location); + { + let mut parser = FontFaceRuleParser { + context, + rule: &mut rule, + }; + let mut iter = RuleBodyParser::new(input, &mut parser); + while let Some(declaration) = iter.next() { + if let Err((error, slice)) = declaration { + let location = error.location; + let error = ContextualParseError::UnsupportedFontFaceDescriptor(slice, error); + context.log_css_error(location, error) + } + } + } + rule +} + +/// A @font-face rule that is known to have font-family and src declarations. +#[cfg(feature = "servo")] +pub struct FontFace<'a>(&'a FontFaceRuleData); + +/// A list of effective sources that we send over through IPC to the font cache. +#[cfg(feature = "servo")] +#[derive(Clone, Debug)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +pub struct EffectiveSources(SourceList); + +#[cfg(feature = "servo")] +impl<'a> FontFace<'a> { + /// Returns the list of effective sources for that font-face, that is the + /// sources which don't list any format hint, or the ones which list at + /// least "truetype" or "opentype". + pub fn effective_sources(&self) -> EffectiveSources { + EffectiveSources( + self.sources() + .iter() + .rev() + .filter(|source| { + if let Source::Url(ref url_source) = **source { + // We support only opentype fonts and truetype is an alias for + // that format. Sources without format hints need to be + // downloaded in case we support them. + url_source.format_hint.as_ref().map_or(true, |hint| { + hint == "truetype" || hint == "opentype" || hint == "woff" + }) + } else { + true + } + }) + .cloned() + .collect(), + ) + } +} + +#[cfg(feature = "servo")] +impl Iterator for EffectiveSources { + type Item = Source; + fn next(&mut self) -> Option { + self.0.pop() + } + + fn size_hint(&self) -> (usize, Option) { + (self.0.len(), Some(self.0.len())) + } +} + +struct FontFaceRuleParser<'a, 'b: 'a> { + context: &'a ParserContext<'b>, + rule: &'a mut FontFaceRuleData, +} + +/// Default methods reject all at rules. +impl<'a, 'b, 'i> AtRuleParser<'i> for FontFaceRuleParser<'a, 'b> { + type Prelude = (); + type AtRule = (); + type Error = StyleParseErrorKind<'i>; +} + +impl<'a, 'b, 'i> QualifiedRuleParser<'i> for FontFaceRuleParser<'a, 'b> { + type Prelude = (); + type QualifiedRule = (); + type Error = StyleParseErrorKind<'i>; +} + +impl<'a, 'b, 'i> RuleBodyItemParser<'i, (), StyleParseErrorKind<'i>> + for FontFaceRuleParser<'a, 'b> +{ + fn parse_qualified(&self) -> bool { + false + } + fn parse_declarations(&self) -> bool { + true + } +} + +impl Parse for Source { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + if input + .try_parse(|input| input.expect_function_matching("local")) + .is_ok() + { + return input + .parse_nested_block(|input| FamilyName::parse(context, input)) + .map(Source::Local); + } + + let url = SpecifiedUrl::parse(context, input)?; + + // Parsing optional format() + let format_hint = if input + .try_parse(|input| input.expect_function_matching("format")) + .is_ok() + { + input.parse_nested_block(|input| { + if let Ok(kw) = input.try_parse(FontFaceSourceFormatKeyword::parse) { + Ok(Some(FontFaceSourceFormat::Keyword(kw))) + } else { + let s = input.expect_string()?.as_ref().to_owned(); + Ok(Some(FontFaceSourceFormat::String(s))) + } + })? + } else { + None + }; + + // Parse optional tech() + let tech_flags = if static_prefs::pref!("layout.css.font-tech.enabled") && + input + .try_parse(|input| input.expect_function_matching("tech")) + .is_ok() + { + input.parse_nested_block(|input| FontFaceSourceTechFlags::parse(context, input))? + } else { + FontFaceSourceTechFlags::empty() + }; + + Ok(Source::Url(UrlSource { + url, + format_hint, + tech_flags, + })) + } +} + +macro_rules! is_descriptor_enabled { + ("font-display") => { + static_prefs::pref!("layout.css.font-display.enabled") + }; + ("font-variation-settings") => { + static_prefs::pref!("layout.css.font-variations.enabled") + }; + ("ascent-override") => { + static_prefs::pref!("layout.css.font-metrics-overrides.enabled") + }; + ("descent-override") => { + static_prefs::pref!("layout.css.font-metrics-overrides.enabled") + }; + ("line-gap-override") => { + static_prefs::pref!("layout.css.font-metrics-overrides.enabled") + }; + ("size-adjust") => { + static_prefs::pref!("layout.css.size-adjust.enabled") + }; + ($name:tt) => { + true + }; +} + +macro_rules! font_face_descriptors_common { + ( + $( #[$doc: meta] $name: tt $ident: ident / $gecko_ident: ident: $ty: ty, )* + ) => { + /// Data inside a `@font-face` rule. + /// + /// + #[derive(Clone, Debug, PartialEq, ToShmem)] + pub struct FontFaceRuleData { + $( + #[$doc] + pub $ident: Option<$ty>, + )* + /// Line and column of the @font-face rule source code. + pub source_location: SourceLocation, + } + + impl FontFaceRuleData { + /// Create an empty font-face rule + pub fn empty(location: SourceLocation) -> Self { + FontFaceRuleData { + $( + $ident: None, + )* + source_location: location, + } + } + + /// Serialization of declarations in the FontFaceRule + pub fn decl_to_css(&self, dest: &mut CssStringWriter) -> fmt::Result { + $( + if let Some(ref value) = self.$ident { + dest.write_str(concat!($name, ": "))?; + value.to_css(&mut CssWriter::new(dest))?; + dest.write_str("; ")?; + } + )* + Ok(()) + } + } + + impl<'a, 'b, 'i> DeclarationParser<'i> for FontFaceRuleParser<'a, 'b> { + type Declaration = (); + type Error = StyleParseErrorKind<'i>; + + fn parse_value<'t>( + &mut self, + name: CowRcStr<'i>, + input: &mut Parser<'i, 't>, + ) -> Result<(), ParseError<'i>> { + match_ignore_ascii_case! { &*name, + $( + $name if is_descriptor_enabled!($name) => { + // DeclarationParser also calls parse_entirely + // so we’d normally not need to, + // but in this case we do because we set the value as a side effect + // rather than returning it. + let value = input.parse_entirely(|i| Parse::parse(self.context, i))?; + self.rule.$ident = Some(value) + }, + )* + _ => return Err(input.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))), + } + Ok(()) + } + } + } +} + +impl ToCssWithGuard for FontFaceRuleData { + // Serialization of FontFaceRule is not specced. + fn to_css(&self, _guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result { + dest.write_str("@font-face { ")?; + self.decl_to_css(dest)?; + dest.write_char('}') + } +} + +macro_rules! font_face_descriptors { + ( + mandatory descriptors = [ + $( #[$m_doc: meta] $m_name: tt $m_ident: ident / $m_gecko_ident: ident: $m_ty: ty, )* + ] + optional descriptors = [ + $( #[$o_doc: meta] $o_name: tt $o_ident: ident / $o_gecko_ident: ident: $o_ty: ty, )* + ] + ) => { + font_face_descriptors_common! { + $( #[$m_doc] $m_name $m_ident / $m_gecko_ident: $m_ty, )* + $( #[$o_doc] $o_name $o_ident / $o_gecko_ident: $o_ty, )* + } + + impl FontFaceRuleData { + /// Per https://github.com/w3c/csswg-drafts/issues/1133 an @font-face rule + /// is valid as far as the CSS parser is concerned even if it doesn’t have + /// a font-family or src declaration. + /// + /// However both are required for the rule to represent an actual font face. + #[cfg(feature = "servo")] + pub fn font_face(&self) -> Option { + if $( self.$m_ident.is_some() )&&* { + Some(FontFace(self)) + } else { + None + } + } + } + + #[cfg(feature = "servo")] + impl<'a> FontFace<'a> { + $( + #[$m_doc] + pub fn $m_ident(&self) -> &$m_ty { + self.0 .$m_ident.as_ref().unwrap() + } + )* + } + } +} + +#[cfg(feature = "gecko")] +font_face_descriptors! { + mandatory descriptors = [ + /// The name of this font face + "font-family" family / mFamily: FamilyName, + + /// The alternative sources for this font face. + "src" sources / mSrc: SourceList, + ] + optional descriptors = [ + /// The style of this font face. + "font-style" style / mStyle: FontStyle, + + /// The weight of this font face. + "font-weight" weight / mWeight: FontWeightRange, + + /// The stretch of this font face. + "font-stretch" stretch / mStretch: FontStretchRange, + + /// The display of this font face. + "font-display" display / mDisplay: FontDisplay, + + /// The ranges of code points outside of which this font face should not be used. + "unicode-range" unicode_range / mUnicodeRange: Vec, + + /// The feature settings of this font face. + "font-feature-settings" feature_settings / mFontFeatureSettings: FontFeatureSettings, + + /// The variation settings of this font face. + "font-variation-settings" variation_settings / mFontVariationSettings: FontVariationSettings, + + /// The language override of this font face. + "font-language-override" language_override / mFontLanguageOverride: font_language_override::SpecifiedValue, + + /// The ascent override for this font face. + "ascent-override" ascent_override / mAscentOverride: MetricsOverride, + + /// The descent override for this font face. + "descent-override" descent_override / mDescentOverride: MetricsOverride, + + /// The line-gap override for this font face. + "line-gap-override" line_gap_override / mLineGapOverride: MetricsOverride, + + /// The size adjustment for this font face. + "size-adjust" size_adjust / mSizeAdjust: NonNegativePercentage, + ] +} + +#[cfg(feature = "servo")] +font_face_descriptors! { + mandatory descriptors = [ + /// The name of this font face + "font-family" family / mFamily: FamilyName, + + /// The alternative sources for this font face. + "src" sources / mSrc: SourceList, + ] + optional descriptors = [ + ] +} diff --git a/servo/components/style/font_metrics.rs b/servo/components/style/font_metrics.rs new file mode 100644 index 0000000000..391d3653ee --- /dev/null +++ b/servo/components/style/font_metrics.rs @@ -0,0 +1,58 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Access to font metrics from the style system. + +#![deny(missing_docs)] + +use crate::values::computed::Length; + +/// Represents the font metrics that style needs from a font to compute the +/// value of certain CSS units like `ex`. +#[derive(Clone, Debug, PartialEq)] +pub struct FontMetrics { + /// The x-height of the font. + pub x_height: Option, + /// The zero advance. This is usually writing mode dependent + pub zero_advance_measure: Option, + /// The cap-height of the font. + pub cap_height: Option, + /// The ideographic-width of the font. + pub ic_width: Option, + /// The ascent of the font (a value is always available for this). + pub ascent: Length, + /// Script scale down factor for math-depth 1. + /// https://w3c.github.io/mathml-core/#dfn-scriptpercentscaledown + pub script_percent_scale_down: Option, + /// Script scale down factor for math-depth 2. + /// https://w3c.github.io/mathml-core/#dfn-scriptscriptpercentscaledown + pub script_script_percent_scale_down: Option, +} + +impl Default for FontMetrics { + fn default() -> Self { + FontMetrics { + x_height: None, + zero_advance_measure: None, + cap_height: None, + ic_width: None, + ascent: Length::new(0.0), + script_percent_scale_down: None, + script_script_percent_scale_down: None, + } + } +} + +/// Type of font metrics to retrieve. +#[derive(Clone, Debug, PartialEq)] +pub enum FontMetricsOrientation { + /// Get metrics for horizontal or vertical according to the Context's + /// writing mode, using horizontal metrics for vertical/mixed + MatchContextPreferHorizontal, + /// Get metrics for horizontal or vertical according to the Context's + /// writing mode, using vertical metrics for vertical/mixed + MatchContextPreferVertical, + /// Force getting horizontal metrics. + Horizontal, +} diff --git a/servo/components/style/gecko/arc_types.rs b/servo/components/style/gecko/arc_types.rs new file mode 100644 index 0000000000..24bf22d69a --- /dev/null +++ b/servo/components/style/gecko/arc_types.rs @@ -0,0 +1,171 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! This file lists all arc FFI types and defines corresponding addref and release functions. This +//! list loosely corresponds to ServoLockedArcTypeList.h file in Gecko. + +#![allow(non_snake_case, missing_docs)] + +use crate::gecko::url::CssUrlData; +use crate::media_queries::MediaList; +use crate::properties::animated_properties::AnimationValue; +use crate::properties::{ComputedValues, PropertyDeclarationBlock}; +use crate::shared_lock::Locked; +use crate::stylesheets::keyframes_rule::Keyframe; +use crate::stylesheets::{ + ContainerRule, CounterStyleRule, CssRules, DocumentRule, FontFaceRule, FontFeatureValuesRule, + FontPaletteValuesRule, ImportRule, KeyframesRule, LayerBlockRule, LayerStatementRule, + MediaRule, NamespaceRule, PageRule, PropertyRule, StyleRule, StylesheetContents, SupportsRule, +}; +use servo_arc::Arc; + +macro_rules! impl_simple_arc_ffi { + ($ty:ty, $addref:ident, $release:ident) => { + #[no_mangle] + pub unsafe extern "C" fn $addref(obj: *const $ty) { + std::mem::forget(Arc::from_raw_addrefed(obj)); + } + + #[no_mangle] + pub unsafe extern "C" fn $release(obj: *const $ty) { + let _ = Arc::from_raw(obj); + } + }; +} + +macro_rules! impl_locked_arc_ffi { + ($servo_type:ty, $alias:ident, $addref:ident, $release:ident) => { + /// A simple alias for a locked type. + pub type $alias = Locked<$servo_type>; + impl_simple_arc_ffi!($alias, $addref, $release); + }; +} + +impl_locked_arc_ffi!( + CssRules, + LockedCssRules, + Servo_CssRules_AddRef, + Servo_CssRules_Release +); +impl_locked_arc_ffi!( + PropertyDeclarationBlock, + LockedDeclarationBlock, + Servo_DeclarationBlock_AddRef, + Servo_DeclarationBlock_Release +); +impl_locked_arc_ffi!( + StyleRule, + LockedStyleRule, + Servo_StyleRule_AddRef, + Servo_StyleRule_Release +); +impl_locked_arc_ffi!( + ImportRule, + LockedImportRule, + Servo_ImportRule_AddRef, + Servo_ImportRule_Release +); +impl_locked_arc_ffi!( + Keyframe, + LockedKeyframe, + Servo_Keyframe_AddRef, + Servo_Keyframe_Release +); +impl_locked_arc_ffi!( + KeyframesRule, + LockedKeyframesRule, + Servo_KeyframesRule_AddRef, + Servo_KeyframesRule_Release +); +impl_simple_arc_ffi!( + LayerBlockRule, + Servo_LayerBlockRule_AddRef, + Servo_LayerBlockRule_Release +); +impl_simple_arc_ffi!( + LayerStatementRule, + Servo_LayerStatementRule_AddRef, + Servo_LayerStatementRule_Release +); +impl_locked_arc_ffi!( + MediaList, + LockedMediaList, + Servo_MediaList_AddRef, + Servo_MediaList_Release +); +impl_simple_arc_ffi!(MediaRule, Servo_MediaRule_AddRef, Servo_MediaRule_Release); +impl_simple_arc_ffi!( + NamespaceRule, + Servo_NamespaceRule_AddRef, + Servo_NamespaceRule_Release +); +impl_locked_arc_ffi!( + PageRule, + LockedPageRule, + Servo_PageRule_AddRef, + Servo_PageRule_Release +); +impl_simple_arc_ffi!( + PropertyRule, + Servo_PropertyRule_AddRef, + Servo_PropertyRule_Release +); +impl_simple_arc_ffi!( + SupportsRule, + Servo_SupportsRule_AddRef, + Servo_SupportsRule_Release +); +impl_simple_arc_ffi!( + ContainerRule, + Servo_ContainerRule_AddRef, + Servo_ContainerRule_Release +); +impl_simple_arc_ffi!( + DocumentRule, + Servo_DocumentRule_AddRef, + Servo_DocumentRule_Release +); +impl_simple_arc_ffi!( + FontFeatureValuesRule, + Servo_FontFeatureValuesRule_AddRef, + Servo_FontFeatureValuesRule_Release +); +impl_simple_arc_ffi!( + FontPaletteValuesRule, + Servo_FontPaletteValuesRule_AddRef, + Servo_FontPaletteValuesRule_Release +); +impl_locked_arc_ffi!( + FontFaceRule, + LockedFontFaceRule, + Servo_FontFaceRule_AddRef, + Servo_FontFaceRule_Release +); +impl_locked_arc_ffi!( + CounterStyleRule, + LockedCounterStyleRule, + Servo_CounterStyleRule_AddRef, + Servo_CounterStyleRule_Release +); + +impl_simple_arc_ffi!( + StylesheetContents, + Servo_StyleSheetContents_AddRef, + Servo_StyleSheetContents_Release +); +impl_simple_arc_ffi!( + CssUrlData, + Servo_CssUrlData_AddRef, + Servo_CssUrlData_Release +); +impl_simple_arc_ffi!( + ComputedValues, + Servo_ComputedStyle_AddRef, + Servo_ComputedStyle_Release +); +impl_simple_arc_ffi!( + AnimationValue, + Servo_AnimationValue_AddRef, + Servo_AnimationValue_Release +); diff --git a/servo/components/style/gecko/conversions.rs b/servo/components/style/gecko/conversions.rs new file mode 100644 index 0000000000..ea3700a323 --- /dev/null +++ b/servo/components/style/gecko/conversions.rs @@ -0,0 +1,59 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! This module contains conversion helpers between Servo and Gecko types +//! Ideally, it would be in geckolib itself, but coherence +//! forces us to keep the traits and implementations here +//! +//! FIXME(emilio): This file should generally just die. + +#![allow(unsafe_code)] + +use crate::gecko_bindings::structs::{nsresult, Matrix4x4Components}; +use crate::stylesheets::RulesMutateError; +use crate::values::computed::transform::Matrix3D; + +impl From for nsresult { + fn from(other: RulesMutateError) -> Self { + match other { + RulesMutateError::Syntax => nsresult::NS_ERROR_DOM_SYNTAX_ERR, + RulesMutateError::IndexSize => nsresult::NS_ERROR_DOM_INDEX_SIZE_ERR, + RulesMutateError::HierarchyRequest => nsresult::NS_ERROR_DOM_HIERARCHY_REQUEST_ERR, + RulesMutateError::InvalidState => nsresult::NS_ERROR_DOM_INVALID_STATE_ERR, + } + } +} + +impl<'a> From<&'a Matrix4x4Components> for Matrix3D { + fn from(m: &'a Matrix4x4Components) -> Matrix3D { + Matrix3D { + m11: m[0], + m12: m[1], + m13: m[2], + m14: m[3], + m21: m[4], + m22: m[5], + m23: m[6], + m24: m[7], + m31: m[8], + m32: m[9], + m33: m[10], + m34: m[11], + m41: m[12], + m42: m[13], + m43: m[14], + m44: m[15], + } + } +} + +impl From for Matrix4x4Components { + fn from(matrix: Matrix3D) -> Self { + [ + matrix.m11, matrix.m12, matrix.m13, matrix.m14, matrix.m21, matrix.m22, matrix.m23, + matrix.m24, matrix.m31, matrix.m32, matrix.m33, matrix.m34, matrix.m41, matrix.m42, + matrix.m43, matrix.m44, + ] + } +} diff --git a/servo/components/style/gecko/data.rs b/servo/components/style/gecko/data.rs new file mode 100644 index 0000000000..c4a5554c5e --- /dev/null +++ b/servo/components/style/gecko/data.rs @@ -0,0 +1,198 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Data needed to style a Gecko document. + +use crate::dom::TElement; +use crate::gecko_bindings::bindings; +use crate::gecko_bindings::structs::{ + self, ServoStyleSetSizes, StyleSheet as DomStyleSheet, StyleSheetInfo, +}; +use crate::invalidation::media_queries::{MediaListKey, ToMediaListKey}; +use crate::media_queries::{Device, MediaList}; +use crate::properties::ComputedValues; +use crate::selector_parser::SnapshotMap; +use crate::shared_lock::{SharedRwLockReadGuard, StylesheetGuards}; +use crate::stylesheets::{StylesheetContents, StylesheetInDocument}; +use crate::stylist::Stylist; +use atomic_refcell::{AtomicRef, AtomicRefCell, AtomicRefMut}; +use malloc_size_of::MallocSizeOfOps; +use servo_arc::Arc; +use std::fmt; + +/// Little wrapper to a Gecko style sheet. +#[derive(Eq, PartialEq)] +pub struct GeckoStyleSheet(*const DomStyleSheet); + +// NOTE(emilio): These are kind of a lie. We allow to make these Send + Sync so that other data +// structures can also be Send and Sync, but Gecko's stylesheets are main-thread-reference-counted. +// +// We assert that we reference-count in the right thread (in the Addref/Release implementations). +// Sending these to a different thread can't really happen (it could theoretically really happen if +// we allowed @import rules inside a nested style rule, but that can't happen per spec and would be +// a parser bug, caught by the asserts). +unsafe impl Send for GeckoStyleSheet {} +unsafe impl Sync for GeckoStyleSheet {} + +impl fmt::Debug for GeckoStyleSheet { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + let contents = self.contents(); + formatter + .debug_struct("GeckoStyleSheet") + .field("origin", &contents.origin) + .field("url_data", &*contents.url_data.read()) + .finish() + } +} + +impl ToMediaListKey for crate::gecko::data::GeckoStyleSheet { + fn to_media_list_key(&self) -> MediaListKey { + use std::mem; + unsafe { MediaListKey::from_raw(mem::transmute(self.0)) } + } +} + +impl GeckoStyleSheet { + /// Create a `GeckoStyleSheet` from a raw `DomStyleSheet` pointer. + #[inline] + pub unsafe fn new(s: *const DomStyleSheet) -> Self { + debug_assert!(!s.is_null()); + bindings::Gecko_StyleSheet_AddRef(s); + Self::from_addrefed(s) + } + + /// Create a `GeckoStyleSheet` from a raw `DomStyleSheet` pointer that + /// already holds a strong reference. + #[inline] + pub unsafe fn from_addrefed(s: *const DomStyleSheet) -> Self { + assert!(!s.is_null()); + GeckoStyleSheet(s) + } + + /// HACK(emilio): This is so that we can avoid crashing release due to + /// bug 1719963 and can hopefully get a useful report from fuzzers. + #[inline] + pub fn hack_is_null(&self) -> bool { + self.0.is_null() + } + + /// Get the raw `StyleSheet` that we're wrapping. + pub fn raw(&self) -> &DomStyleSheet { + unsafe { &*self.0 } + } + + fn inner(&self) -> &StyleSheetInfo { + unsafe { &*(self.raw().mInner as *const StyleSheetInfo) } + } +} + +impl Drop for GeckoStyleSheet { + fn drop(&mut self) { + unsafe { bindings::Gecko_StyleSheet_Release(self.0) }; + } +} + +impl Clone for GeckoStyleSheet { + fn clone(&self) -> Self { + unsafe { bindings::Gecko_StyleSheet_AddRef(self.0) }; + GeckoStyleSheet(self.0) + } +} + +impl StylesheetInDocument for GeckoStyleSheet { + fn media<'a>(&'a self, guard: &'a SharedRwLockReadGuard) -> Option<&'a MediaList> { + use crate::gecko_bindings::structs::mozilla::dom::MediaList as DomMediaList; + unsafe { + let dom_media_list = self.raw().mMedia.mRawPtr as *const DomMediaList; + if dom_media_list.is_null() { + return None; + } + let list = &*(*dom_media_list).mRawList.mRawPtr; + Some(list.read_with(guard)) + } + } + + // All the stylesheets Servo knows about are enabled, because that state is + // handled externally by Gecko. + #[inline] + fn enabled(&self) -> bool { + true + } + + #[inline] + fn contents(&self) -> &StylesheetContents { + debug_assert!(!self.inner().mContents.mRawPtr.is_null()); + unsafe { &*self.inner().mContents.mRawPtr } + } +} + +/// The container for data that a Servo-backed Gecko document needs to style +/// itself. +pub struct PerDocumentStyleDataImpl { + /// Rule processor. + pub stylist: Stylist, + + /// A cache from element to resolved style. + pub undisplayed_style_cache: crate::traversal::UndisplayedStyleCache, + + /// The generation for which our cache is valid. + pub undisplayed_style_cache_generation: u64, +} + +/// The data itself is an `AtomicRefCell`, which guarantees the proper semantics +/// and unexpected races while trying to mutate it. +pub struct PerDocumentStyleData(AtomicRefCell); + +impl PerDocumentStyleData { + /// Create a `PerDocumentStyleData`. + pub fn new(document: *const structs::Document) -> Self { + let device = Device::new(document); + let quirks_mode = device.document().mCompatMode; + + PerDocumentStyleData(AtomicRefCell::new(PerDocumentStyleDataImpl { + stylist: Stylist::new(device, quirks_mode.into()), + undisplayed_style_cache: Default::default(), + undisplayed_style_cache_generation: 0, + })) + } + + /// Get an immutable reference to this style data. + pub fn borrow(&self) -> AtomicRef { + self.0.borrow() + } + + /// Get an mutable reference to this style data. + pub fn borrow_mut(&self) -> AtomicRefMut { + self.0.borrow_mut() + } +} + +impl PerDocumentStyleDataImpl { + /// Recreate the style data if the stylesheets have changed. + pub fn flush_stylesheets( + &mut self, + guard: &SharedRwLockReadGuard, + document_element: Option, + snapshots: Option<&SnapshotMap>, + ) -> bool + where + E: TElement, + { + self.stylist + .flush(&StylesheetGuards::same(guard), document_element, snapshots) + } + + /// Get the default computed values for this document. + pub fn default_computed_values(&self) -> &Arc { + self.stylist.device().default_computed_values_arc() + } + + /// Measure heap usage. + pub fn add_size_of(&self, ops: &mut MallocSizeOfOps, sizes: &mut ServoStyleSetSizes) { + self.stylist.add_size_of(ops, sizes); + } +} + +/// The gecko-specific AuthorStyles instantiation. +pub type AuthorStyles = crate::author_styles::AuthorStyles; diff --git a/servo/components/style/gecko/media_features.rs b/servo/components/style/gecko/media_features.rs new file mode 100644 index 0000000000..4ca746ea84 --- /dev/null +++ b/servo/components/style/gecko/media_features.rs @@ -0,0 +1,1028 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Gecko's media feature list and evaluator. + +use crate::gecko_bindings::bindings; +use crate::gecko_bindings::structs; +use crate::gecko_bindings::structs::ScreenColorGamut; +use crate::media_queries::{Device, MediaType}; +use crate::queries::feature::{AllowsRanges, Evaluator, FeatureFlags, QueryFeatureDescription}; +use crate::queries::values::Orientation; +use crate::values::computed::{CSSPixelLength, Context, Ratio, Resolution}; +use app_units::Au; +use euclid::default::Size2D; + +fn device_size(device: &Device) -> Size2D { + let mut width = 0; + let mut height = 0; + unsafe { + bindings::Gecko_MediaFeatures_GetDeviceSize(device.document(), &mut width, &mut height); + } + Size2D::new(Au(width), Au(height)) +} + +/// https://drafts.csswg.org/mediaqueries-4/#width +fn eval_width(context: &Context) -> CSSPixelLength { + CSSPixelLength::new(context.device().au_viewport_size().width.to_f32_px()) +} + +/// https://drafts.csswg.org/mediaqueries-4/#device-width +fn eval_device_width(context: &Context) -> CSSPixelLength { + CSSPixelLength::new(device_size(context.device()).width.to_f32_px()) +} + +/// https://drafts.csswg.org/mediaqueries-4/#height +fn eval_height(context: &Context) -> CSSPixelLength { + CSSPixelLength::new(context.device().au_viewport_size().height.to_f32_px()) +} + +/// https://drafts.csswg.org/mediaqueries-4/#device-height +fn eval_device_height(context: &Context) -> CSSPixelLength { + CSSPixelLength::new(device_size(context.device()).height.to_f32_px()) +} + +fn eval_aspect_ratio_for(context: &Context, get_size: F) -> Ratio +where + F: FnOnce(&Device) -> Size2D, +{ + let size = get_size(context.device()); + Ratio::new(size.width.0 as f32, size.height.0 as f32) +} + +/// https://drafts.csswg.org/mediaqueries-4/#aspect-ratio +fn eval_aspect_ratio(context: &Context) -> Ratio { + eval_aspect_ratio_for(context, Device::au_viewport_size) +} + +/// https://drafts.csswg.org/mediaqueries-4/#device-aspect-ratio +fn eval_device_aspect_ratio(context: &Context) -> Ratio { + eval_aspect_ratio_for(context, device_size) +} + +/// https://compat.spec.whatwg.org/#css-media-queries-webkit-device-pixel-ratio +fn eval_device_pixel_ratio(context: &Context) -> f32 { + eval_resolution(context).dppx() +} + +/// https://drafts.csswg.org/mediaqueries-4/#orientation +fn eval_orientation(context: &Context, value: Option) -> bool { + Orientation::eval(context.device().au_viewport_size(), value) +} + +/// FIXME: There's no spec for `-moz-device-orientation`. +fn eval_device_orientation(context: &Context, value: Option) -> bool { + Orientation::eval(device_size(context.device()), value) +} + +/// Values for the display-mode media feature. +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, PartialEq, ToCss)] +#[repr(u8)] +#[allow(missing_docs)] +pub enum DisplayMode { + Browser = 0, + MinimalUi, + Standalone, + Fullscreen, +} + +/// https://w3c.github.io/manifest/#the-display-mode-media-feature +fn eval_display_mode(context: &Context, query_value: Option) -> bool { + match query_value { + Some(v) => { + v == unsafe { + bindings::Gecko_MediaFeatures_GetDisplayMode(context.device().document()) + } + }, + None => true, + } +} + +/// https://drafts.csswg.org/mediaqueries-4/#grid +fn eval_grid(_: &Context) -> bool { + // Gecko doesn't support grid devices (e.g., ttys), so the 'grid' feature + // is always 0. + false +} + +/// https://compat.spec.whatwg.org/#css-media-queries-webkit-transform-3d +fn eval_transform_3d(_: &Context) -> bool { + true +} + +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] +#[repr(u8)] +enum Scan { + Progressive, + Interlace, +} + +/// https://drafts.csswg.org/mediaqueries-4/#scan +fn eval_scan(_: &Context, _: Option) -> bool { + // Since Gecko doesn't support the 'tv' media type, the 'scan' feature never + // matches. + false +} + +/// https://drafts.csswg.org/mediaqueries-4/#color +fn eval_color(context: &Context) -> i32 { + unsafe { bindings::Gecko_MediaFeatures_GetColorDepth(context.device().document()) } +} + +/// https://drafts.csswg.org/mediaqueries-4/#color-index +fn eval_color_index(_: &Context) -> i32 { + // We should return zero if the device does not use a color lookup table. + 0 +} + +/// https://drafts.csswg.org/mediaqueries-4/#monochrome +fn eval_monochrome(context: &Context) -> i32 { + // For color devices we should return 0. + unsafe { bindings::Gecko_MediaFeatures_GetMonochromeBitsPerPixel(context.device().document()) } +} + +/// Values for the color-gamut media feature. +/// This implements PartialOrd so that lower values will correctly match +/// higher capabilities. +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, PartialEq, PartialOrd, ToCss)] +#[repr(u8)] +enum ColorGamut { + /// The sRGB gamut. + Srgb, + /// The gamut specified by the Display P3 Color Space. + P3, + /// The gamut specified by the ITU-R Recommendation BT.2020 Color Space. + Rec2020, +} + +/// https://drafts.csswg.org/mediaqueries-4/#color-gamut +fn eval_color_gamut(context: &Context, query_value: Option) -> bool { + let query_value = match query_value { + Some(v) => v, + None => return false, + }; + let color_gamut = + unsafe { bindings::Gecko_MediaFeatures_ColorGamut(context.device().document()) }; + // Match if our color gamut is at least as wide as the query value + query_value <= + match color_gamut { + // EndGuard_ is not a valid color gamut, so the default color-gamut is used. + ScreenColorGamut::Srgb | ScreenColorGamut::EndGuard_ => ColorGamut::Srgb, + ScreenColorGamut::P3 => ColorGamut::P3, + ScreenColorGamut::Rec2020 => ColorGamut::Rec2020, + } +} + +/// https://drafts.csswg.org/mediaqueries-4/#resolution +fn eval_resolution(context: &Context) -> Resolution { + let resolution_dppx = + unsafe { bindings::Gecko_MediaFeatures_GetResolution(context.device().document()) }; + Resolution::from_dppx(resolution_dppx) +} + +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] +#[repr(u8)] +enum PrefersReducedMotion { + NoPreference, + Reduce, +} + +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] +#[repr(u8)] +enum PrefersReducedTransparency { + NoPreference, + Reduce, +} + +/// Values for the prefers-color-scheme media feature. +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, PartialEq, ToCss)] +#[repr(u8)] +#[allow(missing_docs)] +pub enum PrefersColorScheme { + Light, + Dark, +} + +/// Values for the dynamic-range and video-dynamic-range media features. +/// https://drafts.csswg.org/mediaqueries-5/#dynamic-range +/// This implements PartialOrd so that lower values will correctly match +/// higher capabilities. +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, PartialEq, PartialOrd, ToCss)] +#[repr(u8)] +#[allow(missing_docs)] +pub enum DynamicRange { + Standard, + High, +} + +/// https://drafts.csswg.org/mediaqueries-5/#prefers-reduced-motion +fn eval_prefers_reduced_motion( + context: &Context, + query_value: Option, +) -> bool { + let prefers_reduced = + unsafe { bindings::Gecko_MediaFeatures_PrefersReducedMotion(context.device().document()) }; + let query_value = match query_value { + Some(v) => v, + None => return prefers_reduced, + }; + + match query_value { + PrefersReducedMotion::NoPreference => !prefers_reduced, + PrefersReducedMotion::Reduce => prefers_reduced, + } +} + +/// https://drafts.csswg.org/mediaqueries-5/#prefers-reduced-transparency +fn eval_prefers_reduced_transparency( + context: &Context, + query_value: Option, +) -> bool { + let prefers_reduced = unsafe { + bindings::Gecko_MediaFeatures_PrefersReducedTransparency(context.device().document()) + }; + let query_value = match query_value { + Some(v) => v, + None => return prefers_reduced, + }; + + match query_value { + PrefersReducedTransparency::NoPreference => !prefers_reduced, + PrefersReducedTransparency::Reduce => prefers_reduced, + } +} + +/// Possible values for prefers-contrast media query. +/// https://drafts.csswg.org/mediaqueries-5/#prefers-contrast +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, PartialEq, ToCss)] +#[repr(u8)] +pub enum PrefersContrast { + /// More contrast is preferred. + More, + /// Low contrast is preferred. + Less, + /// Custom (not more, not less). + Custom, + /// The default value if neither high or low contrast is enabled. + NoPreference, +} + +/// https://drafts.csswg.org/mediaqueries-5/#prefers-contrast +fn eval_prefers_contrast(context: &Context, query_value: Option) -> bool { + let prefers_contrast = + unsafe { bindings::Gecko_MediaFeatures_PrefersContrast(context.device().document()) }; + match query_value { + Some(v) => v == prefers_contrast, + None => prefers_contrast != PrefersContrast::NoPreference, + } +} + +/// Possible values for the forced-colors media query. +/// https://drafts.csswg.org/mediaqueries-5/#forced-colors +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, PartialEq, ToCss)] +#[repr(u8)] +pub enum ForcedColors { + /// Page colors are not being forced. + None, + /// Page colors are being forced. + Active, +} + +/// https://drafts.csswg.org/mediaqueries-5/#forced-colors +fn eval_forced_colors(context: &Context, query_value: Option) -> bool { + let forced = !context.device().use_document_colors(); + match query_value { + Some(query_value) => forced == (query_value == ForcedColors::Active), + None => forced, + } +} + +/// Possible values for the inverted-colors media query. +/// https://drafts.csswg.org/mediaqueries-5/#inverted +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] +#[repr(u8)] +enum InvertedColors { + /// Colors are displayed normally. + None, + /// All pixels within the displayed area have been inverted. + Inverted, +} + +/// https://drafts.csswg.org/mediaqueries-5/#inverted +fn eval_inverted_colors(context: &Context, query_value: Option) -> bool { + let inverted_colors = + unsafe { bindings::Gecko_MediaFeatures_InvertedColors(context.device().document()) }; + let query_value = match query_value { + Some(v) => v, + None => return inverted_colors, + }; + + match query_value { + InvertedColors::None => !inverted_colors, + InvertedColors::Inverted => inverted_colors, + } +} + +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] +#[repr(u8)] +enum OverflowBlock { + None, + Scroll, + Paged, +} + +/// https://drafts.csswg.org/mediaqueries-4/#mf-overflow-block +fn eval_overflow_block(context: &Context, query_value: Option) -> bool { + // For the time being, assume that printing (including previews) + // is the only time when we paginate, and we are otherwise always + // scrolling. This is true at the moment in Firefox, but may need + // updating in the future (e.g., ebook readers built with Stylo, a + // billboard mode that doesn't support overflow at all). + // + // If this ever changes, don't forget to change eval_overflow_inline too. + let scrolling = context.device().media_type() != MediaType::print(); + let query_value = match query_value { + Some(v) => v, + None => return true, + }; + + match query_value { + OverflowBlock::None => false, + OverflowBlock::Scroll => scrolling, + OverflowBlock::Paged => !scrolling, + } +} + +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] +#[repr(u8)] +enum OverflowInline { + None, + Scroll, +} + +/// https://drafts.csswg.org/mediaqueries-4/#mf-overflow-inline +fn eval_overflow_inline(context: &Context, query_value: Option) -> bool { + // See the note in eval_overflow_block. + let scrolling = context.device().media_type() != MediaType::print(); + let query_value = match query_value { + Some(v) => v, + None => return scrolling, + }; + + match query_value { + OverflowInline::None => !scrolling, + OverflowInline::Scroll => scrolling, + } +} + +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] +#[repr(u8)] +enum Update { + None, + Slow, + Fast, +} + +/// https://drafts.csswg.org/mediaqueries-4/#update +fn eval_update(context: &Context, query_value: Option) -> bool { + // This has similar caveats to those described in eval_overflow_block. + // For now, we report that print (incl. print media simulation, + // which can in fact update but is limited to the developer tools) + // is `update: none` and that all other contexts are `update: fast`, + // which may not be true for future platforms, like e-ink devices. + let can_update = context.device().media_type() != MediaType::print(); + let query_value = match query_value { + Some(v) => v, + None => return can_update, + }; + + match query_value { + Update::None => !can_update, + Update::Slow => false, + Update::Fast => can_update, + } +} + +fn do_eval_prefers_color_scheme( + context: &Context, + use_content: bool, + query_value: Option, +) -> bool { + let prefers_color_scheme = unsafe { + bindings::Gecko_MediaFeatures_PrefersColorScheme(context.device().document(), use_content) + }; + match query_value { + Some(v) => prefers_color_scheme == v, + None => true, + } +} + +/// https://drafts.csswg.org/mediaqueries-5/#prefers-color-scheme +fn eval_prefers_color_scheme(context: &Context, query_value: Option) -> bool { + do_eval_prefers_color_scheme(context, /* use_content = */ false, query_value) +} + +fn eval_content_prefers_color_scheme( + context: &Context, + query_value: Option, +) -> bool { + do_eval_prefers_color_scheme(context, /* use_content = */ true, query_value) +} + +/// https://drafts.csswg.org/mediaqueries-5/#dynamic-range +fn eval_dynamic_range(context: &Context, query_value: Option) -> bool { + let dynamic_range = + unsafe { bindings::Gecko_MediaFeatures_DynamicRange(context.device().document()) }; + match query_value { + Some(v) => dynamic_range >= v, + None => false, + } +} +/// https://drafts.csswg.org/mediaqueries-5/#video-dynamic-range +fn eval_video_dynamic_range(context: &Context, query_value: Option) -> bool { + let dynamic_range = + unsafe { bindings::Gecko_MediaFeatures_VideoDynamicRange(context.device().document()) }; + match query_value { + Some(v) => dynamic_range >= v, + None => false, + } +} + +bitflags! { + /// https://drafts.csswg.org/mediaqueries-4/#mf-interaction + struct PointerCapabilities: u8 { + const COARSE = structs::PointerCapabilities_Coarse; + const FINE = structs::PointerCapabilities_Fine; + const HOVER = structs::PointerCapabilities_Hover; + } +} + +fn primary_pointer_capabilities(context: &Context) -> PointerCapabilities { + PointerCapabilities::from_bits_truncate(unsafe { + bindings::Gecko_MediaFeatures_PrimaryPointerCapabilities(context.device().document()) + }) +} + +fn all_pointer_capabilities(context: &Context) -> PointerCapabilities { + PointerCapabilities::from_bits_truncate(unsafe { + bindings::Gecko_MediaFeatures_AllPointerCapabilities(context.device().document()) + }) +} + +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] +#[repr(u8)] +enum Pointer { + None, + Coarse, + Fine, +} + +fn eval_pointer_capabilities( + query_value: Option, + pointer_capabilities: PointerCapabilities, +) -> bool { + let query_value = match query_value { + Some(v) => v, + None => return !pointer_capabilities.is_empty(), + }; + + match query_value { + Pointer::None => pointer_capabilities.is_empty(), + Pointer::Coarse => pointer_capabilities.intersects(PointerCapabilities::COARSE), + Pointer::Fine => pointer_capabilities.intersects(PointerCapabilities::FINE), + } +} + +/// https://drafts.csswg.org/mediaqueries-4/#pointer +fn eval_pointer(context: &Context, query_value: Option) -> bool { + eval_pointer_capabilities(query_value, primary_pointer_capabilities(context)) +} + +/// https://drafts.csswg.org/mediaqueries-4/#descdef-media-any-pointer +fn eval_any_pointer(context: &Context, query_value: Option) -> bool { + eval_pointer_capabilities(query_value, all_pointer_capabilities(context)) +} + +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] +#[repr(u8)] +enum Hover { + None, + Hover, +} + +fn eval_hover_capabilities( + query_value: Option, + pointer_capabilities: PointerCapabilities, +) -> bool { + let can_hover = pointer_capabilities.intersects(PointerCapabilities::HOVER); + let query_value = match query_value { + Some(v) => v, + None => return can_hover, + }; + + match query_value { + Hover::None => !can_hover, + Hover::Hover => can_hover, + } +} + +/// https://drafts.csswg.org/mediaqueries-4/#hover +fn eval_hover(context: &Context, query_value: Option) -> bool { + eval_hover_capabilities(query_value, primary_pointer_capabilities(context)) +} + +/// https://drafts.csswg.org/mediaqueries-4/#descdef-media-any-hover +fn eval_any_hover(context: &Context, query_value: Option) -> bool { + eval_hover_capabilities(query_value, all_pointer_capabilities(context)) +} + +fn eval_moz_is_glyph(context: &Context) -> bool { + context.device().document().mIsSVGGlyphsDocument() +} + +fn eval_moz_print_preview(context: &Context) -> bool { + let is_print_preview = context.device().is_print_preview(); + if is_print_preview { + debug_assert_eq!(context.device().media_type(), MediaType::print()); + } + is_print_preview +} + +fn eval_moz_non_native_content_theme(context: &Context) -> bool { + unsafe { bindings::Gecko_MediaFeatures_ShouldAvoidNativeTheme(context.device().document()) } +} + +fn eval_moz_is_resource_document(context: &Context) -> bool { + unsafe { bindings::Gecko_MediaFeatures_IsResourceDocument(context.device().document()) } +} + +/// Allows front-end CSS to discern platform via media queries. +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] +#[repr(u8)] +pub enum Platform { + /// Matches any Android version. + Android, + /// For our purposes here, "linux" is just "gtk" (so unix-but-not-mac). + /// There's no need for our front-end code to differentiate between those + /// platforms and they already use the "linux" string elsewhere (e.g., + /// toolkit/themes/linux). + Linux, + /// Matches any macOS version. + Macos, + /// Matches any Windows version. + Windows, + /// Matches only Windows 7. + WindowsWin7, + /// Matches only Windows 8. + WindowsWin8, + /// Matches windows 10 and actually matches windows 11 too, as of right now. + WindowsWin10, +} + +fn eval_moz_platform(_: &Context, query_value: Option) -> bool { + let query_value = match query_value { + Some(v) => v, + None => return false, + }; + + unsafe { bindings::Gecko_MediaFeatures_MatchesPlatform(query_value) } +} + +/// Values for the scripting media feature. +/// https://drafts.csswg.org/mediaqueries-5/#scripting +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, PartialEq, ToCss)] +#[repr(u8)] +pub enum Scripting { + /// Scripting is not supported or not enabled + None, + /// Scripting is supported and enabled, but only for initial page load + /// We will never match this value as it is intended for non-browser user agents, + /// but it is part of the spec so we should still parse it. + /// See: https://github.com/w3c/csswg-drafts/issues/8621 + InitialOnly, + /// Scripting is supported and enabled + Enabled, +} + +/// https://drafts.csswg.org/mediaqueries-5/#scripting +fn eval_scripting(context: &Context, query_value: Option) -> bool { + let scripting = unsafe { bindings::Gecko_MediaFeatures_Scripting(context.device().document()) }; + match query_value { + Some(v) => v == scripting, + None => scripting != Scripting::None, + } +} + +fn eval_moz_windows_non_native_menus(context: &Context) -> bool { + unsafe { bindings::Gecko_MediaFeatures_WindowsNonNativeMenus(context.device().document()) } +} + +fn eval_moz_overlay_scrollbars(context: &Context) -> bool { + unsafe { bindings::Gecko_MediaFeatures_UseOverlayScrollbars(context.device().document()) } +} + +fn get_lnf_int(int_id: i32) -> i32 { + unsafe { bindings::Gecko_GetLookAndFeelInt(int_id) } +} + +fn get_lnf_int_as_bool(int_id: i32) -> bool { + get_lnf_int(int_id) != 0 +} + +fn get_scrollbar_start_backward(int_id: i32) -> bool { + (get_lnf_int(int_id) & bindings::LookAndFeel_eScrollArrow_StartBackward as i32) != 0 +} + +fn get_scrollbar_start_forward(int_id: i32) -> bool { + (get_lnf_int(int_id) & bindings::LookAndFeel_eScrollArrow_StartForward as i32) != 0 +} + +fn get_scrollbar_end_backward(int_id: i32) -> bool { + (get_lnf_int(int_id) & bindings::LookAndFeel_eScrollArrow_EndBackward as i32) != 0 +} + +fn get_scrollbar_end_forward(int_id: i32) -> bool { + (get_lnf_int(int_id) & bindings::LookAndFeel_eScrollArrow_EndForward as i32) != 0 +} + +macro_rules! lnf_int_feature { + ($feature_name:expr, $int_id:ident, $get_value:ident) => {{ + fn __eval(_: &Context) -> bool { + $get_value(bindings::LookAndFeel_IntID::$int_id as i32) + } + + feature!( + $feature_name, + AllowsRanges::No, + Evaluator::BoolInteger(__eval), + FeatureFlags::CHROME_AND_UA_ONLY, + ) + }}; + ($feature_name:expr, $int_id:ident) => {{ + lnf_int_feature!($feature_name, $int_id, get_lnf_int_as_bool) + }}; +} + +/// bool pref-based features are an slightly less convenient to start using +/// version of @supports -moz-bool-pref, but with some benefits, mainly that +/// they can support dynamic changes, and don't require a pref lookup every time +/// they're used. +/// +/// In order to use them you need to make sure that the pref defined as a static +/// pref, with `rust: true`. The feature name needs to be defined in +/// `StaticAtoms.py` just like the others. In order to support dynamic changes, +/// you also need to add them to kMediaQueryPrefs in nsXPLookAndFeel.cpp +#[allow(unused)] +macro_rules! bool_pref_feature { + ($feature_name:expr, $pref:tt) => {{ + fn __eval(_: &Context) -> bool { + static_prefs::pref!($pref) + } + + feature!( + $feature_name, + AllowsRanges::No, + Evaluator::BoolInteger(__eval), + FeatureFlags::CHROME_AND_UA_ONLY, + ) + }}; +} + +/// Adding new media features requires (1) adding the new feature to this +/// array, with appropriate entries (and potentially any new code needed +/// to support new types in these entries and (2) ensuring that either +/// nsPresContext::MediaFeatureValuesChanged is called when the value that +/// would be returned by the evaluator function could change. +pub static MEDIA_FEATURES: [QueryFeatureDescription; 67] = [ + feature!( + atom!("width"), + AllowsRanges::Yes, + Evaluator::Length(eval_width), + FeatureFlags::VIEWPORT_DEPENDENT, + ), + feature!( + atom!("height"), + AllowsRanges::Yes, + Evaluator::Length(eval_height), + FeatureFlags::VIEWPORT_DEPENDENT, + ), + feature!( + atom!("aspect-ratio"), + AllowsRanges::Yes, + Evaluator::NumberRatio(eval_aspect_ratio), + FeatureFlags::VIEWPORT_DEPENDENT, + ), + feature!( + atom!("orientation"), + AllowsRanges::No, + keyword_evaluator!(eval_orientation, Orientation), + FeatureFlags::VIEWPORT_DEPENDENT, + ), + feature!( + atom!("device-width"), + AllowsRanges::Yes, + Evaluator::Length(eval_device_width), + FeatureFlags::empty(), + ), + feature!( + atom!("device-height"), + AllowsRanges::Yes, + Evaluator::Length(eval_device_height), + FeatureFlags::empty(), + ), + feature!( + atom!("device-aspect-ratio"), + AllowsRanges::Yes, + Evaluator::NumberRatio(eval_device_aspect_ratio), + FeatureFlags::empty(), + ), + feature!( + atom!("-moz-device-orientation"), + AllowsRanges::No, + keyword_evaluator!(eval_device_orientation, Orientation), + FeatureFlags::empty(), + ), + // Webkit extensions that we support for de-facto web compatibility. + // -webkit-{min|max}-device-pixel-ratio (controlled with its own pref): + feature!( + atom!("device-pixel-ratio"), + AllowsRanges::Yes, + Evaluator::Float(eval_device_pixel_ratio), + FeatureFlags::WEBKIT_PREFIX, + ), + // -webkit-transform-3d. + feature!( + atom!("transform-3d"), + AllowsRanges::No, + Evaluator::BoolInteger(eval_transform_3d), + FeatureFlags::WEBKIT_PREFIX, + ), + feature!( + atom!("-moz-device-pixel-ratio"), + AllowsRanges::Yes, + Evaluator::Float(eval_device_pixel_ratio), + FeatureFlags::empty(), + ), + feature!( + atom!("resolution"), + AllowsRanges::Yes, + Evaluator::Resolution(eval_resolution), + FeatureFlags::empty(), + ), + feature!( + atom!("display-mode"), + AllowsRanges::No, + keyword_evaluator!(eval_display_mode, DisplayMode), + FeatureFlags::empty(), + ), + feature!( + atom!("grid"), + AllowsRanges::No, + Evaluator::BoolInteger(eval_grid), + FeatureFlags::empty(), + ), + feature!( + atom!("scan"), + AllowsRanges::No, + keyword_evaluator!(eval_scan, Scan), + FeatureFlags::empty(), + ), + feature!( + atom!("color"), + AllowsRanges::Yes, + Evaluator::Integer(eval_color), + FeatureFlags::empty(), + ), + feature!( + atom!("color-index"), + AllowsRanges::Yes, + Evaluator::Integer(eval_color_index), + FeatureFlags::empty(), + ), + feature!( + atom!("monochrome"), + AllowsRanges::Yes, + Evaluator::Integer(eval_monochrome), + FeatureFlags::empty(), + ), + feature!( + atom!("color-gamut"), + AllowsRanges::No, + keyword_evaluator!(eval_color_gamut, ColorGamut), + FeatureFlags::empty(), + ), + feature!( + atom!("prefers-reduced-motion"), + AllowsRanges::No, + keyword_evaluator!(eval_prefers_reduced_motion, PrefersReducedMotion), + FeatureFlags::empty(), + ), + feature!( + atom!("prefers-reduced-transparency"), + AllowsRanges::No, + keyword_evaluator!( + eval_prefers_reduced_transparency, + PrefersReducedTransparency + ), + FeatureFlags::empty(), + ), + feature!( + atom!("prefers-contrast"), + AllowsRanges::No, + keyword_evaluator!(eval_prefers_contrast, PrefersContrast), + // Note: by default this is only enabled in browser chrome and + // ua. It can be enabled on the web via the + // layout.css.prefers-contrast.enabled preference. See + // disabed_by_pref in media_feature_expression.rs for how that + // is done. + FeatureFlags::empty(), + ), + feature!( + atom!("forced-colors"), + AllowsRanges::No, + keyword_evaluator!(eval_forced_colors, ForcedColors), + FeatureFlags::empty(), + ), + feature!( + atom!("inverted-colors"), + AllowsRanges::No, + keyword_evaluator!(eval_inverted_colors, InvertedColors), + FeatureFlags::empty(), + ), + feature!( + atom!("overflow-block"), + AllowsRanges::No, + keyword_evaluator!(eval_overflow_block, OverflowBlock), + FeatureFlags::empty(), + ), + feature!( + atom!("overflow-inline"), + AllowsRanges::No, + keyword_evaluator!(eval_overflow_inline, OverflowInline), + FeatureFlags::empty(), + ), + feature!( + atom!("update"), + AllowsRanges::No, + keyword_evaluator!(eval_update, Update), + FeatureFlags::empty(), + ), + feature!( + atom!("prefers-color-scheme"), + AllowsRanges::No, + keyword_evaluator!(eval_prefers_color_scheme, PrefersColorScheme), + FeatureFlags::empty(), + ), + feature!( + atom!("dynamic-range"), + AllowsRanges::No, + keyword_evaluator!(eval_dynamic_range, DynamicRange), + FeatureFlags::empty(), + ), + feature!( + atom!("video-dynamic-range"), + AllowsRanges::No, + keyword_evaluator!(eval_video_dynamic_range, DynamicRange), + FeatureFlags::empty(), + ), + feature!( + atom!("scripting"), + AllowsRanges::No, + keyword_evaluator!(eval_scripting, Scripting), + FeatureFlags::empty(), + ), + // Evaluates to the preferred color scheme for content. Only useful in + // chrome context, where the chrome color-scheme and the content + // color-scheme might differ. + feature!( + atom!("-moz-content-prefers-color-scheme"), + AllowsRanges::No, + keyword_evaluator!(eval_content_prefers_color_scheme, PrefersColorScheme), + FeatureFlags::CHROME_AND_UA_ONLY, + ), + feature!( + atom!("pointer"), + AllowsRanges::No, + keyword_evaluator!(eval_pointer, Pointer), + FeatureFlags::empty(), + ), + feature!( + atom!("any-pointer"), + AllowsRanges::No, + keyword_evaluator!(eval_any_pointer, Pointer), + FeatureFlags::empty(), + ), + feature!( + atom!("hover"), + AllowsRanges::No, + keyword_evaluator!(eval_hover, Hover), + FeatureFlags::empty(), + ), + feature!( + atom!("any-hover"), + AllowsRanges::No, + keyword_evaluator!(eval_any_hover, Hover), + FeatureFlags::empty(), + ), + // Internal -moz-is-glyph media feature: applies only inside SVG glyphs. + // Internal because it is really only useful in the user agent anyway + // and therefore not worth standardizing. + feature!( + atom!("-moz-is-glyph"), + AllowsRanges::No, + Evaluator::BoolInteger(eval_moz_is_glyph), + FeatureFlags::CHROME_AND_UA_ONLY, + ), + feature!( + atom!("-moz-is-resource-document"), + AllowsRanges::No, + Evaluator::BoolInteger(eval_moz_is_resource_document), + FeatureFlags::CHROME_AND_UA_ONLY, + ), + feature!( + atom!("-moz-platform"), + AllowsRanges::No, + keyword_evaluator!(eval_moz_platform, Platform), + FeatureFlags::CHROME_AND_UA_ONLY, + ), + feature!( + atom!("-moz-print-preview"), + AllowsRanges::No, + Evaluator::BoolInteger(eval_moz_print_preview), + FeatureFlags::CHROME_AND_UA_ONLY, + ), + feature!( + atom!("-moz-non-native-content-theme"), + AllowsRanges::No, + Evaluator::BoolInteger(eval_moz_non_native_content_theme), + FeatureFlags::CHROME_AND_UA_ONLY, + ), + feature!( + atom!("-moz-windows-non-native-menus"), + AllowsRanges::No, + Evaluator::BoolInteger(eval_moz_windows_non_native_menus), + FeatureFlags::CHROME_AND_UA_ONLY, + ), + feature!( + atom!("-moz-overlay-scrollbars"), + AllowsRanges::No, + Evaluator::BoolInteger(eval_moz_overlay_scrollbars), + FeatureFlags::CHROME_AND_UA_ONLY, + ), + lnf_int_feature!( + atom!("-moz-scrollbar-start-backward"), + ScrollArrowStyle, + get_scrollbar_start_backward + ), + lnf_int_feature!( + atom!("-moz-scrollbar-start-forward"), + ScrollArrowStyle, + get_scrollbar_start_forward + ), + lnf_int_feature!( + atom!("-moz-scrollbar-end-backward"), + ScrollArrowStyle, + get_scrollbar_end_backward + ), + lnf_int_feature!( + atom!("-moz-scrollbar-end-forward"), + ScrollArrowStyle, + get_scrollbar_end_forward + ), + lnf_int_feature!(atom!("-moz-menubar-drag"), MenuBarDrag), + lnf_int_feature!(atom!("-moz-windows-default-theme"), WindowsDefaultTheme), + lnf_int_feature!(atom!("-moz-mac-graphite-theme"), MacGraphiteTheme), + lnf_int_feature!(atom!("-moz-mac-big-sur-theme"), MacBigSurTheme), + lnf_int_feature!(atom!("-moz-mac-rtl"), MacRTL), + lnf_int_feature!( + atom!("-moz-windows-accent-color-in-titlebar"), + WindowsAccentColorInTitlebar + ), + lnf_int_feature!(atom!("-moz-windows-compositor"), DWMCompositor), + lnf_int_feature!(atom!("-moz-windows-classic"), WindowsClassic), + lnf_int_feature!(atom!("-moz-windows-glass"), WindowsGlass), + lnf_int_feature!(atom!("-moz-swipe-animation-enabled"), SwipeAnimationEnabled), + lnf_int_feature!(atom!("-moz-gtk-csd-available"), GTKCSDAvailable), + lnf_int_feature!(atom!("-moz-gtk-csd-minimize-button"), GTKCSDMinimizeButton), + lnf_int_feature!(atom!("-moz-gtk-csd-maximize-button"), GTKCSDMaximizeButton), + lnf_int_feature!(atom!("-moz-gtk-csd-close-button"), GTKCSDCloseButton), + lnf_int_feature!( + atom!("-moz-gtk-csd-reversed-placement"), + GTKCSDReversedPlacement + ), + lnf_int_feature!(atom!("-moz-system-dark-theme"), SystemUsesDarkTheme), + lnf_int_feature!(atom!("-moz-panel-animations"), PanelAnimations), + // media query for MathML Core's implementation of maction/semantics + bool_pref_feature!( + atom!("-moz-mathml-core-maction-and-semantics"), + "mathml.legacy_maction_and_semantics_implementations.disabled" + ), + // media query for MathML Core's implementation of ms + bool_pref_feature!( + atom!("-moz-mathml-core-ms"), + "mathml.ms_lquote_rquote_attributes.disabled" + ), + // media query for popover attribute + bool_pref_feature!(atom!("-moz-popover-enabled"), "dom.element.popover.enabled"), +]; diff --git a/servo/components/style/gecko/media_queries.rs b/servo/components/style/gecko/media_queries.rs new file mode 100644 index 0000000000..f27d78fd93 --- /dev/null +++ b/servo/components/style/gecko/media_queries.rs @@ -0,0 +1,567 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Gecko's media-query device and expression representation. + +use crate::color::AbsoluteColor; +use crate::context::QuirksMode; +use crate::custom_properties::CssEnvironment; +use crate::font_metrics::FontMetrics; +use crate::gecko::values::{convert_absolute_color_to_nscolor, convert_nscolor_to_absolute_color}; +use crate::gecko_bindings::bindings; +use crate::gecko_bindings::structs; +use crate::media_queries::MediaType; +use crate::properties::ComputedValues; +use crate::string_cache::Atom; +use crate::values::computed::font::GenericFontFamily; +use crate::values::computed::{ColorScheme, Length, NonNegativeLength}; +use crate::values::specified::color::SystemColor; +use crate::values::specified::font::FONT_MEDIUM_PX; +use crate::values::specified::ViewportVariant; +use crate::values::{CustomIdent, KeyframesName}; +use app_units::{Au, AU_PER_PX}; +use euclid::default::Size2D; +use euclid::{Scale, SideOffsets2D}; +use servo_arc::Arc; +use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering}; +use std::{cmp, fmt}; +use style_traits::viewport::ViewportConstraints; +use style_traits::{CSSPixel, DevicePixel}; + +/// The `Device` in Gecko wraps a pres context, has a default values computed, +/// and contains all the viewport rule state. +pub struct Device { + /// NB: The document owns the styleset, who owns the stylist, and thus the + /// `Device`, so having a raw document pointer here is fine. + document: *const structs::Document, + default_values: Arc, + /// The font size of the root element. + /// + /// This is set when computing the style of the root element, and used for + /// rem units in other elements. + /// + /// When computing the style of the root element, there can't be any other + /// style being computed at the same time, given we need the style of the + /// parent to compute everything else. So it is correct to just use a + /// relaxed atomic here. + root_font_size: AtomicU32, + /// The body text color, stored as an `nscolor`, used for the "tables + /// inherit from body" quirk. + /// + /// + body_text_color: AtomicUsize, + /// Whether any styles computed in the document relied on the root font-size + /// by using rem units. + used_root_font_size: AtomicBool, + /// Whether any styles computed in the document relied on font metrics. + used_font_metrics: AtomicBool, + /// Whether any styles computed in the document relied on the viewport size + /// by using vw/vh/vmin/vmax units. + used_viewport_size: AtomicBool, + /// Whether any styles computed in the document relied on the viewport size + /// by using dvw/dvh/dvmin/dvmax units. + used_dynamic_viewport_size: AtomicBool, + /// The CssEnvironment object responsible of getting CSS environment + /// variables. + environment: CssEnvironment, +} + +impl fmt::Debug for Device { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use nsstring::nsCString; + + let mut doc_uri = nsCString::new(); + unsafe { + bindings::Gecko_nsIURI_Debug((*self.document()).mDocumentURI.raw(), &mut doc_uri) + }; + + f.debug_struct("Device") + .field("document_url", &doc_uri) + .finish() + } +} + +unsafe impl Sync for Device {} +unsafe impl Send for Device {} + +impl Device { + /// Trivially constructs a new `Device`. + pub fn new(document: *const structs::Document) -> Self { + assert!(!document.is_null()); + let doc = unsafe { &*document }; + let prefs = unsafe { &*bindings::Gecko_GetPrefSheetPrefs(doc) }; + Device { + document, + default_values: ComputedValues::default_values(doc), + root_font_size: AtomicU32::new(FONT_MEDIUM_PX.to_bits()), + // This gets updated when we see the , so it doesn't really + // matter which color-scheme we look at here. + body_text_color: AtomicUsize::new(prefs.mLightColors.mDefault as usize), + used_root_font_size: AtomicBool::new(false), + used_font_metrics: AtomicBool::new(false), + used_viewport_size: AtomicBool::new(false), + used_dynamic_viewport_size: AtomicBool::new(false), + environment: CssEnvironment, + } + } + + /// Get the relevant environment to resolve `env()` functions. + #[inline] + pub fn environment(&self) -> &CssEnvironment { + &self.environment + } + + /// Returns the computed line-height for the font in a given computed values instance. + /// + /// If you pass down an element, then the used line-height is returned. + pub fn calc_line_height( + &self, + line_height: &crate::values::computed::LineHeight, + vertical: bool, + font: &crate::properties::style_structs::Font, + element: Option, + ) -> NonNegativeLength { + let pres_context = self.pres_context(); + let au = Au(unsafe { + bindings::Gecko_CalcLineHeight( + line_height, + pres_context.map_or(std::ptr::null(), |pc| pc), + vertical, + &**font, + element.map_or(std::ptr::null(), |e| e.0), + ) + }); + NonNegativeLength::new(au.to_f32_px()) + } + + /// Tells the device that a new viewport rule has been found, and stores the + /// relevant viewport constraints. + pub fn account_for_viewport_rule(&mut self, _constraints: &ViewportConstraints) { + unreachable!("Gecko doesn't support @viewport"); + } + + /// Whether any animation name may be referenced from the style of any + /// element. + pub fn animation_name_may_be_referenced(&self, name: &KeyframesName) -> bool { + let pc = match self.pres_context() { + Some(pc) => pc, + None => return false, + }; + + unsafe { + bindings::Gecko_AnimationNameMayBeReferencedFromStyle(pc, name.as_atom().as_ptr()) + } + } + + /// Returns the default computed values as a reference, in order to match + /// Servo. + pub fn default_computed_values(&self) -> &ComputedValues { + &self.default_values + } + + /// Returns the default computed values as an `Arc`. + pub fn default_computed_values_arc(&self) -> &Arc { + &self.default_values + } + + /// Get the font size of the root element (for rem) + pub fn root_font_size(&self) -> Length { + self.used_root_font_size.store(true, Ordering::Relaxed); + Length::new(f32::from_bits(self.root_font_size.load(Ordering::Relaxed))) + } + + /// Set the font size of the root element (for rem) + pub fn set_root_font_size(&self, size: Length) { + self.root_font_size + .store(size.px().to_bits(), Ordering::Relaxed) + } + + /// The quirks mode of the document. + pub fn quirks_mode(&self) -> QuirksMode { + self.document().mCompatMode.into() + } + + /// Sets the body text color for the "inherit color from body" quirk. + /// + /// + pub fn set_body_text_color(&self, color: AbsoluteColor) { + self.body_text_color.store( + convert_absolute_color_to_nscolor(&color) as usize, + Ordering::Relaxed, + ) + } + + /// Gets the base size given a generic font family and a language. + pub fn base_size_for_generic(&self, language: &Atom, generic: GenericFontFamily) -> Length { + unsafe { bindings::Gecko_GetBaseSize(self.document(), language.as_ptr(), generic) } + } + + /// Gets the size of the scrollbar in CSS pixels. + pub fn scrollbar_inline_size(&self) -> Length { + let pc = match self.pres_context() { + Some(pc) => pc, + // XXX: we could have a more reasonable default perhaps. + None => return Length::new(0.0), + }; + Length::new(unsafe { bindings::Gecko_GetScrollbarInlineSize(pc) }) + } + + /// Queries font metrics + pub fn query_font_metrics( + &self, + vertical: bool, + font: &crate::properties::style_structs::Font, + base_size: Length, + in_media_query: bool, + retrieve_math_scales: bool, + ) -> FontMetrics { + self.used_font_metrics.store(true, Ordering::Relaxed); + let pc = match self.pres_context() { + Some(pc) => pc, + None => return Default::default(), + }; + let gecko_metrics = unsafe { + bindings::Gecko_GetFontMetrics( + pc, + vertical, + &**font, + base_size, + // we don't use the user font set in a media query + !in_media_query, + retrieve_math_scales, + ) + }; + FontMetrics { + x_height: Some(gecko_metrics.mXSize), + zero_advance_measure: if gecko_metrics.mChSize.px() >= 0. { + Some(gecko_metrics.mChSize) + } else { + None + }, + cap_height: if gecko_metrics.mCapHeight.px() >= 0. { + Some(gecko_metrics.mCapHeight) + } else { + None + }, + ic_width: if gecko_metrics.mIcWidth.px() >= 0. { + Some(gecko_metrics.mIcWidth) + } else { + None + }, + ascent: gecko_metrics.mAscent, + script_percent_scale_down: if gecko_metrics.mScriptPercentScaleDown > 0. { + Some(gecko_metrics.mScriptPercentScaleDown) + } else { + None + }, + script_script_percent_scale_down: if gecko_metrics.mScriptScriptPercentScaleDown > 0. { + Some(gecko_metrics.mScriptScriptPercentScaleDown) + } else { + None + }, + } + } + + /// Returns the body text color. + pub fn body_text_color(&self) -> AbsoluteColor { + convert_nscolor_to_absolute_color(self.body_text_color.load(Ordering::Relaxed) as u32) + } + + /// Gets the document pointer. + #[inline] + pub fn document(&self) -> &structs::Document { + unsafe { &*self.document } + } + + /// Gets the pres context associated with this document. + #[inline] + pub fn pres_context(&self) -> Option<&structs::nsPresContext> { + unsafe { + self.document() + .mPresShell + .as_ref()? + .mPresContext + .mRawPtr + .as_ref() + } + } + + /// Gets the preference stylesheet prefs for our document. + #[inline] + pub fn pref_sheet_prefs(&self) -> &structs::PreferenceSheet_Prefs { + unsafe { &*bindings::Gecko_GetPrefSheetPrefs(self.document()) } + } + + /// Recreates the default computed values. + pub fn reset_computed_values(&mut self) { + self.default_values = ComputedValues::default_values(self.document()); + } + + /// Rebuild all the cached data. + pub fn rebuild_cached_data(&mut self) { + self.reset_computed_values(); + self.used_root_font_size.store(false, Ordering::Relaxed); + self.used_font_metrics.store(false, Ordering::Relaxed); + self.used_viewport_size.store(false, Ordering::Relaxed); + self.used_dynamic_viewport_size + .store(false, Ordering::Relaxed); + } + + /// Returns whether we ever looked up the root font size of the Device. + pub fn used_root_font_size(&self) -> bool { + self.used_root_font_size.load(Ordering::Relaxed) + } + + /// Recreates all the temporary state that the `Device` stores. + /// + /// This includes the viewport override from `@viewport` rules, and also the + /// default computed values. + pub fn reset(&mut self) { + self.reset_computed_values(); + } + + /// Returns whether this document is in print preview. + pub fn is_print_preview(&self) -> bool { + let pc = match self.pres_context() { + Some(pc) => pc, + None => return false, + }; + pc.mType == structs::nsPresContext_nsPresContextType_eContext_PrintPreview + } + + /// Returns the current media type of the device. + pub fn media_type(&self) -> MediaType { + let pc = match self.pres_context() { + Some(pc) => pc, + None => return MediaType::screen(), + }; + + // Gecko allows emulating random media with mMediaEmulationData.mMedium. + let medium_to_use = if !pc.mMediaEmulationData.mMedium.mRawPtr.is_null() { + pc.mMediaEmulationData.mMedium.mRawPtr + } else { + pc.mMedium as *const structs::nsAtom as *mut _ + }; + + MediaType(CustomIdent(unsafe { Atom::from_raw(medium_to_use) })) + } + + // It may make sense to account for @page rule margins here somehow, however + // it's not clear how that'd work, see: + // https://github.com/w3c/csswg-drafts/issues/5437 + fn page_size_minus_default_margin(&self, pc: &structs::nsPresContext) -> Size2D { + debug_assert!(pc.mIsRootPaginatedDocument() != 0); + let area = &pc.mPageSize; + let margin = &pc.mDefaultPageMargin; + let width = area.width - margin.left - margin.right; + let height = area.height - margin.top - margin.bottom; + Size2D::new(Au(cmp::max(width, 0)), Au(cmp::max(height, 0))) + } + + /// Returns the current viewport size in app units. + pub fn au_viewport_size(&self) -> Size2D { + let pc = match self.pres_context() { + Some(pc) => pc, + None => return Size2D::new(Au(0), Au(0)), + }; + + if pc.mIsRootPaginatedDocument() != 0 { + return self.page_size_minus_default_margin(pc); + } + + let area = &pc.mVisibleArea; + Size2D::new(Au(area.width), Au(area.height)) + } + + /// Returns the current viewport size in app units, recording that it's been + /// used for viewport unit resolution. + pub fn au_viewport_size_for_viewport_unit_resolution( + &self, + variant: ViewportVariant, + ) -> Size2D { + self.used_viewport_size.store(true, Ordering::Relaxed); + let pc = match self.pres_context() { + Some(pc) => pc, + None => return Size2D::new(Au(0), Au(0)), + }; + + if pc.mIsRootPaginatedDocument() != 0 { + return self.page_size_minus_default_margin(pc); + } + + match variant { + ViewportVariant::UADefault => { + let size = &pc.mSizeForViewportUnits; + Size2D::new(Au(size.width), Au(size.height)) + }, + ViewportVariant::Small => { + let size = &pc.mVisibleArea; + Size2D::new(Au(size.width), Au(size.height)) + }, + ViewportVariant::Large => { + let size = &pc.mVisibleArea; + // Looks like IntCoordTyped is treated as if it's u32 in Rust. + debug_assert!( + /* pc.mDynamicToolbarMaxHeight >=0 && */ + pc.mDynamicToolbarMaxHeight < i32::MAX as u32 + ); + Size2D::new( + Au(size.width), + Au(size.height + + pc.mDynamicToolbarMaxHeight as i32 * pc.mCurAppUnitsPerDevPixel), + ) + }, + ViewportVariant::Dynamic => { + self.used_dynamic_viewport_size + .store(true, Ordering::Relaxed); + let size = &pc.mVisibleArea; + // Looks like IntCoordTyped is treated as if it's u32 in Rust. + debug_assert!( + /* pc.mDynamicToolbarHeight >=0 && */ + pc.mDynamicToolbarHeight < i32::MAX as u32 + ); + Size2D::new( + Au(size.width), + Au(size.height + + (pc.mDynamicToolbarMaxHeight - pc.mDynamicToolbarHeight) as i32 * + pc.mCurAppUnitsPerDevPixel), + ) + }, + } + } + + /// Returns whether we ever looked up the viewport size of the Device. + pub fn used_viewport_size(&self) -> bool { + self.used_viewport_size.load(Ordering::Relaxed) + } + + /// Returns whether we ever looked up the dynamic viewport size of the Device. + pub fn used_dynamic_viewport_size(&self) -> bool { + self.used_dynamic_viewport_size.load(Ordering::Relaxed) + } + + /// Returns whether font metrics have been queried. + pub fn used_font_metrics(&self) -> bool { + self.used_font_metrics.load(Ordering::Relaxed) + } + + /// Returns whether visited styles are enabled. + pub fn visited_styles_enabled(&self) -> bool { + unsafe { bindings::Gecko_VisitedStylesEnabled(self.document()) } + } + + /// Returns the number of app units per device pixel we're using currently. + pub fn app_units_per_device_pixel(&self) -> i32 { + match self.pres_context() { + Some(pc) => pc.mCurAppUnitsPerDevPixel, + None => AU_PER_PX, + } + } + + /// Returns the device pixel ratio. + pub fn device_pixel_ratio(&self) -> Scale { + let pc = match self.pres_context() { + Some(pc) => pc, + None => return Scale::new(1.), + }; + + if pc.mMediaEmulationData.mDPPX > 0.0 { + return Scale::new(pc.mMediaEmulationData.mDPPX); + } + + let au_per_dpx = pc.mCurAppUnitsPerDevPixel as f32; + let au_per_px = AU_PER_PX as f32; + Scale::new(au_per_px / au_per_dpx) + } + + /// Returns whether document colors are enabled. + #[inline] + pub fn use_document_colors(&self) -> bool { + let doc = self.document(); + if doc.mIsBeingUsedAsImage() { + return true; + } + self.pref_sheet_prefs().mUseDocumentColors + } + + /// Computes a system color and returns it as an nscolor. + pub(crate) fn system_nscolor( + &self, + system_color: SystemColor, + color_scheme: &ColorScheme, + ) -> u32 { + unsafe { bindings::Gecko_ComputeSystemColor(system_color, self.document(), color_scheme) } + } + + /// Returns the default background color. + /// + /// This is only for forced-colors/high-contrast, so looking at light colors + /// is ok. + pub fn default_background_color(&self) -> AbsoluteColor { + let normal = ColorScheme::normal(); + convert_nscolor_to_absolute_color(self.system_nscolor(SystemColor::Canvas, &normal)) + } + + /// Returns the default foreground color. + /// + /// See above for looking at light colors only. + pub fn default_color(&self) -> AbsoluteColor { + let normal = ColorScheme::normal(); + convert_nscolor_to_absolute_color(self.system_nscolor(SystemColor::Canvastext, &normal)) + } + + /// Returns the current effective text zoom. + #[inline] + fn text_zoom(&self) -> f32 { + let pc = match self.pres_context() { + Some(pc) => pc, + None => return 1., + }; + pc.mTextZoom + } + + /// Applies text zoom to a font-size or line-height value (see nsStyleFont::ZoomText). + #[inline] + pub fn zoom_text(&self, size: Length) -> Length { + size.scale_by(self.text_zoom()) + } + + /// Un-apply text zoom. + #[inline] + pub fn unzoom_text(&self, size: Length) -> Length { + size.scale_by(1. / self.text_zoom()) + } + + /// Returns safe area insets + pub fn safe_area_insets(&self) -> SideOffsets2D { + let pc = match self.pres_context() { + Some(pc) => pc, + None => return SideOffsets2D::zero(), + }; + let mut top = 0.0; + let mut right = 0.0; + let mut bottom = 0.0; + let mut left = 0.0; + unsafe { + bindings::Gecko_GetSafeAreaInsets(pc, &mut top, &mut right, &mut bottom, &mut left) + }; + SideOffsets2D::new(top, right, bottom, left) + } + + /// Returns true if the given MIME type is supported + pub fn is_supported_mime_type(&self, mime_type: &str) -> bool { + unsafe { + bindings::Gecko_IsSupportedImageMimeType(mime_type.as_ptr(), mime_type.len() as u32) + } + } + + /// Return whether the document is a chrome document. + /// + /// This check is consistent with how we enable chrome rules for chrome:// and resource:// + /// stylesheets (and thus chrome:// documents). + #[inline] + pub fn chrome_rules_enabled_for_document(&self) -> bool { + self.document().mChromeRulesEnabled() + } +} diff --git a/servo/components/style/gecko/mod.rs b/servo/components/style/gecko/mod.rs new file mode 100644 index 0000000000..c32ded14f3 --- /dev/null +++ b/servo/components/style/gecko/mod.rs @@ -0,0 +1,23 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Gecko-specific style-system bits. + +#[macro_use] +mod non_ts_pseudo_class_list; + +pub mod arc_types; +pub mod conversions; +pub mod data; +pub mod media_features; +pub mod media_queries; +pub mod pseudo_element; +pub mod restyle_damage; +pub mod selector_parser; +pub mod snapshot; +pub mod snapshot_helpers; +pub mod traversal; +pub mod url; +pub mod values; +pub mod wrapper; diff --git a/servo/components/style/gecko/non_ts_pseudo_class_list.rs b/servo/components/style/gecko/non_ts_pseudo_class_list.rs new file mode 100644 index 0000000000..e494082047 --- /dev/null +++ b/servo/components/style/gecko/non_ts_pseudo_class_list.rs @@ -0,0 +1,100 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +/* + * This file contains a helper macro includes all supported non-tree-structural + * pseudo-classes. + * + * FIXME: Find a way to autogenerate this file. + * + * Expected usage is as follows: + * ``` + * macro_rules! pseudo_class_macro{ + * ([$(($css:expr, $name:ident, $gecko_type:tt, $state:tt, $flags:tt),)*]) => { + * // do stuff + * } + * } + * apply_non_ts_list!(pseudo_class_macro) + * ``` + * + * $gecko_type can be either "_" or an ident in Gecko's CSSPseudoClassType. + * $state can be either "_" or an expression of type ElementState. If present, + * the semantics are that the pseudo-class matches if any of the bits in + * $state are set on the element. + * $flags can be either "_" or an expression of type NonTSPseudoClassFlag, + * see selector_parser.rs for more details. + */ + +macro_rules! apply_non_ts_list { + ($apply_macro:ident) => { + $apply_macro! { + [ + ("-moz-table-border-nonzero", MozTableBorderNonzero, _, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), + ("-moz-browser-frame", MozBrowserFrame, _, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS_AND_CHROME), + ("-moz-select-list-box", MozSelectListBox, _, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), + ("link", Link, UNVISITED, _), + ("any-link", AnyLink, VISITED_OR_UNVISITED, _), + ("visited", Visited, VISITED, _), + ("active", Active, ACTIVE, _), + ("autofill", Autofill, AUTOFILL, _), + ("checked", Checked, CHECKED, _), + ("defined", Defined, DEFINED, _), + ("disabled", Disabled, DISABLED, _), + ("enabled", Enabled, ENABLED, _), + ("focus", Focus, FOCUS, _), + ("focus-within", FocusWithin, FOCUS_WITHIN, _), + ("focus-visible", FocusVisible, FOCUSRING, _), + ("hover", Hover, HOVER, _), + ("-moz-drag-over", MozDragOver, DRAGOVER, _), + ("target", Target, URLTARGET, _), + ("indeterminate", Indeterminate, INDETERMINATE, _), + ("-moz-inert", MozInert, INERT, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), + ("-moz-devtools-highlighted", MozDevtoolsHighlighted, DEVTOOLS_HIGHLIGHTED, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), + ("-moz-styleeditor-transitioning", MozStyleeditorTransitioning, STYLEEDITOR_TRANSITIONING, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), + ("fullscreen", Fullscreen, FULLSCREEN, _), + ("modal", Modal, MODAL, _), + ("-moz-topmost-modal", MozTopmostModal, TOPMOST_MODAL, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), + ("-moz-broken", MozBroken, BROKEN, _), + ("-moz-loading", MozLoading, LOADING, _), + ("-moz-has-dir-attr", MozHasDirAttr, HAS_DIR_ATTR, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), + ("-moz-dir-attr-ltr", MozDirAttrLTR, HAS_DIR_ATTR_LTR, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), + ("-moz-dir-attr-rtl", MozDirAttrRTL, HAS_DIR_ATTR_RTL, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), + ("-moz-dir-attr-like-auto", MozDirAttrLikeAuto, HAS_DIR_ATTR_LIKE_AUTO, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), + + ("-moz-autofill-preview", MozAutofillPreview, AUTOFILL_PREVIEW, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS_AND_CHROME), + ("-moz-value-empty", MozValueEmpty, VALUE_EMPTY, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), + ("-moz-revealed", MozRevealed, REVEALED, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), + + ("-moz-math-increment-script-level", MozMathIncrementScriptLevel, INCREMENT_SCRIPT_LEVEL, _), + + ("required", Required, REQUIRED, _), + ("popover-open", PopoverOpen, POPOVER_OPEN, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS_AND_CHROME), + ("optional", Optional, OPTIONAL_, _), + ("valid", Valid, VALID, _), + ("invalid", Invalid, INVALID, _), + ("in-range", InRange, INRANGE, _), + ("out-of-range", OutOfRange, OUTOFRANGE, _), + ("default", Default, DEFAULT, _), + ("placeholder-shown", PlaceholderShown, PLACEHOLDER_SHOWN, _), + ("read-only", ReadOnly, READONLY, _), + ("read-write", ReadWrite, READWRITE, _), + ("user-valid", UserValid, USER_VALID, _), + ("user-invalid", UserInvalid, USER_INVALID, _), + ("-moz-meter-optimum", MozMeterOptimum, OPTIMUM, _), + ("-moz-meter-sub-optimum", MozMeterSubOptimum, SUB_OPTIMUM, _), + ("-moz-meter-sub-sub-optimum", MozMeterSubSubOptimum, SUB_SUB_OPTIMUM, _), + + ("-moz-first-node", MozFirstNode, _, _), + ("-moz-last-node", MozLastNode, _, _), + ("-moz-only-whitespace", MozOnlyWhitespace, _, _), + ("-moz-native-anonymous", MozNativeAnonymous, _, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), + ("-moz-use-shadow-tree-root", MozUseShadowTreeRoot, _, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), + ("-moz-is-html", MozIsHTML, _, _), + ("-moz-placeholder", MozPlaceholder, _, _), + ("-moz-lwtheme", MozLWTheme, _, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS_AND_CHROME), + ("-moz-window-inactive", MozWindowInactive, _, _), + ] + } + } +} diff --git a/servo/components/style/gecko/pseudo_element.rs b/servo/components/style/gecko/pseudo_element.rs new file mode 100644 index 0000000000..d0c47d51a4 --- /dev/null +++ b/servo/components/style/gecko/pseudo_element.rs @@ -0,0 +1,237 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Gecko's definition of a pseudo-element. +//! +//! Note that a few autogenerated bits of this live in +//! `pseudo_element_definition.mako.rs`. If you touch that file, you probably +//! need to update the checked-in files for Servo. + +use crate::gecko_bindings::structs::{self, PseudoStyleType}; +use crate::properties::longhands::display::computed_value::T as Display; +use crate::properties::{ComputedValues, PropertyFlags}; +use crate::selector_parser::{PseudoElementCascadeType, SelectorImpl}; +use crate::str::{starts_with_ignore_ascii_case, string_as_ascii_lowercase}; +use crate::string_cache::Atom; +use crate::values::serialize_atom_identifier; +use crate::values::AtomIdent; +use cssparser::ToCss; +use static_prefs::pref; +use std::fmt; + +include!(concat!( + env!("OUT_DIR"), + "/gecko/pseudo_element_definition.rs" +)); + +impl ::selectors::parser::PseudoElement for PseudoElement { + type Impl = SelectorImpl; + + // ::slotted() should support all tree-abiding pseudo-elements, see + // https://drafts.csswg.org/css-scoping/#slotted-pseudo + // https://drafts.csswg.org/css-pseudo-4/#treelike + #[inline] + fn valid_after_slotted(&self) -> bool { + matches!( + *self, + PseudoElement::Before | + PseudoElement::After | + PseudoElement::Marker | + PseudoElement::Placeholder | + PseudoElement::FileSelectorButton + ) + } + + #[inline] + fn accepts_state_pseudo_classes(&self) -> bool { + self.supports_user_action_state() + } +} + +impl PseudoElement { + /// Returns the kind of cascade type that a given pseudo is going to use. + /// + /// In Gecko we only compute ::before and ::after eagerly. We save the rules + /// for anonymous boxes separately, so we resolve them as precomputed + /// pseudos. + /// + /// We resolve the others lazily, see `Servo_ResolvePseudoStyle`. + pub fn cascade_type(&self) -> PseudoElementCascadeType { + if self.is_eager() { + debug_assert!(!self.is_anon_box()); + return PseudoElementCascadeType::Eager; + } + + if self.is_precomputed() { + return PseudoElementCascadeType::Precomputed; + } + + PseudoElementCascadeType::Lazy + } + + /// Whether the pseudo-element should inherit from the default computed + /// values instead of from the parent element. + /// + /// This is not the common thing, but there are some pseudos (namely: + /// ::backdrop), that shouldn't inherit from the parent element. + pub fn inherits_from_default_values(&self) -> bool { + matches!(*self, PseudoElement::Backdrop) + } + + /// Gets the canonical index of this eagerly-cascaded pseudo-element. + #[inline] + pub fn eager_index(&self) -> usize { + EAGER_PSEUDOS + .iter() + .position(|p| p == self) + .expect("Not an eager pseudo") + } + + /// Creates a pseudo-element from an eager index. + #[inline] + pub fn from_eager_index(i: usize) -> Self { + EAGER_PSEUDOS[i].clone() + } + + /// Whether animations for the current pseudo element are stored in the + /// parent element. + #[inline] + pub fn animations_stored_in_parent(&self) -> bool { + matches!(*self, Self::Before | Self::After | Self::Marker) + } + + /// Whether the current pseudo element is ::before or ::after. + #[inline] + pub fn is_before_or_after(&self) -> bool { + self.is_before() || self.is_after() + } + + /// Whether this pseudo-element is the ::before pseudo. + #[inline] + pub fn is_before(&self) -> bool { + *self == PseudoElement::Before + } + + /// Whether this pseudo-element is the ::after pseudo. + #[inline] + pub fn is_after(&self) -> bool { + *self == PseudoElement::After + } + + /// Whether this pseudo-element is the ::marker pseudo. + #[inline] + pub fn is_marker(&self) -> bool { + *self == PseudoElement::Marker + } + + /// Whether this pseudo-element is the ::selection pseudo. + #[inline] + pub fn is_selection(&self) -> bool { + *self == PseudoElement::Selection + } + + /// Whether this pseudo-element is ::first-letter. + #[inline] + pub fn is_first_letter(&self) -> bool { + *self == PseudoElement::FirstLetter + } + + /// Whether this pseudo-element is ::first-line. + #[inline] + pub fn is_first_line(&self) -> bool { + *self == PseudoElement::FirstLine + } + + /// Whether this pseudo-element is the ::-moz-color-swatch pseudo. + #[inline] + pub fn is_color_swatch(&self) -> bool { + *self == PseudoElement::MozColorSwatch + } + + /// Whether this pseudo-element is lazily-cascaded. + #[inline] + pub fn is_lazy(&self) -> bool { + !self.is_eager() && !self.is_precomputed() + } + + /// The identifier of the highlight this pseudo-element represents. + pub fn highlight_name(&self) -> Option<&AtomIdent> { + match &*self { + PseudoElement::Highlight(name) => Some(&name), + _ => None, + } + } + + /// Whether this pseudo-element is the ::highlight pseudo. + pub fn is_highlight(&self) -> bool { + matches!(*self, PseudoElement::Highlight(_)) + } + + /// Whether this pseudo-element supports user action selectors. + pub fn supports_user_action_state(&self) -> bool { + (self.flags() & structs::CSS_PSEUDO_ELEMENT_SUPPORTS_USER_ACTION_STATE) != 0 + } + + /// Whether this pseudo-element is enabled for all content. + pub fn enabled_in_content(&self) -> bool { + if self.is_highlight() && !pref!("dom.customHighlightAPI.enabled") { + return false; + } + return self.flags() & structs::CSS_PSEUDO_ELEMENT_ENABLED_IN_UA_SHEETS_AND_CHROME == 0; + } + + /// Whether this pseudo is enabled explicitly in UA sheets. + pub fn enabled_in_ua_sheets(&self) -> bool { + (self.flags() & structs::CSS_PSEUDO_ELEMENT_ENABLED_IN_UA_SHEETS) != 0 + } + + /// Whether this pseudo is enabled explicitly in chrome sheets. + pub fn enabled_in_chrome(&self) -> bool { + (self.flags() & structs::CSS_PSEUDO_ELEMENT_ENABLED_IN_CHROME) != 0 + } + + /// Whether this pseudo-element skips flex/grid container display-based + /// fixup. + #[inline] + pub fn skip_item_display_fixup(&self) -> bool { + (self.flags() & structs::CSS_PSEUDO_ELEMENT_IS_FLEX_OR_GRID_ITEM) == 0 + } + + /// Whether this pseudo-element is precomputed. + #[inline] + pub fn is_precomputed(&self) -> bool { + self.is_anon_box() && !self.is_tree_pseudo_element() + } + + /// Property flag that properties must have to apply to this pseudo-element. + #[inline] + pub fn property_restriction(&self) -> Option { + Some(match *self { + PseudoElement::FirstLetter => PropertyFlags::APPLIES_TO_FIRST_LETTER, + PseudoElement::FirstLine => PropertyFlags::APPLIES_TO_FIRST_LINE, + PseudoElement::Placeholder => PropertyFlags::APPLIES_TO_PLACEHOLDER, + PseudoElement::Cue => PropertyFlags::APPLIES_TO_CUE, + PseudoElement::Marker if static_prefs::pref!("layout.css.marker.restricted") => { + PropertyFlags::APPLIES_TO_MARKER + }, + _ => return None, + }) + } + + /// Whether this pseudo-element should actually exist if it has + /// the given styles. + pub fn should_exist(&self, style: &ComputedValues) -> bool { + debug_assert!(self.is_eager()); + + if style.get_box().clone_display() == Display::None { + return false; + } + + if self.is_before_or_after() && style.ineffective_content_property() { + return false; + } + + true + } +} diff --git a/servo/components/style/gecko/pseudo_element_definition.mako.rs b/servo/components/style/gecko/pseudo_element_definition.mako.rs new file mode 100644 index 0000000000..73e7893c99 --- /dev/null +++ b/servo/components/style/gecko/pseudo_element_definition.mako.rs @@ -0,0 +1,276 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +/// Gecko's pseudo-element definition. +/// +/// We intentionally double-box legacy ::-moz-tree pseudo-elements to keep the +/// size of PseudoElement (and thus selector components) small. +#[derive(Clone, Debug, Eq, Hash, MallocSizeOf, PartialEq, ToShmem)] +pub enum PseudoElement { + % for pseudo in PSEUDOS: + /// ${pseudo.value} + % if pseudo.is_tree_pseudo_element(): + ${pseudo.capitalized_pseudo()}(thin_vec::ThinVec), + % elif pseudo.pseudo_ident == "highlight": + ${pseudo.capitalized_pseudo()}(AtomIdent), + % else: + ${pseudo.capitalized_pseudo()}, + % endif + % endfor + /// ::-webkit-* that we don't recognize + /// https://github.com/whatwg/compat/issues/103 + UnknownWebkit(Atom), +} + +/// Important: If you change this, you should also update Gecko's +/// nsCSSPseudoElements::IsEagerlyCascadedInServo. +<% EAGER_PSEUDOS = ["Before", "After", "FirstLine", "FirstLetter"] %> +<% TREE_PSEUDOS = [pseudo for pseudo in PSEUDOS if pseudo.is_tree_pseudo_element()] %> +<% SIMPLE_PSEUDOS = [pseudo for pseudo in PSEUDOS if pseudo.is_simple_pseudo_element()] %> + +/// The number of eager pseudo-elements. +pub const EAGER_PSEUDO_COUNT: usize = ${len(EAGER_PSEUDOS)}; + +/// The number of non-functional pseudo-elements. +pub const SIMPLE_PSEUDO_COUNT: usize = ${len(SIMPLE_PSEUDOS)}; + +/// The number of tree pseudo-elements. +pub const TREE_PSEUDO_COUNT: usize = ${len(TREE_PSEUDOS)}; + +/// The number of all pseudo-elements. +pub const PSEUDO_COUNT: usize = ${len(PSEUDOS)}; + +/// The list of eager pseudos. +pub const EAGER_PSEUDOS: [PseudoElement; EAGER_PSEUDO_COUNT] = [ + % for eager_pseudo_name in EAGER_PSEUDOS: + PseudoElement::${eager_pseudo_name}, + % endfor +]; + +<%def name="pseudo_element_variant(pseudo, tree_arg='..')">\ +PseudoElement::${pseudo.capitalized_pseudo()}${"({})".format(tree_arg) if not pseudo.is_simple_pseudo_element() else ""}\ + + +impl PseudoElement { + /// Returns an index of the pseudo-element. + #[inline] + pub fn index(&self) -> usize { + match *self { + % for i, pseudo in enumerate(PSEUDOS): + ${pseudo_element_variant(pseudo)} => ${i}, + % endfor + PseudoElement::UnknownWebkit(..) => unreachable!(), + } + } + + /// Returns an array of `None` values. + /// + /// FIXME(emilio): Integer generics can't come soon enough. + pub fn pseudo_none_array() -> [Option; PSEUDO_COUNT] { + [ + ${",\n ".join(["None" for pseudo in PSEUDOS])} + ] + } + + /// Whether this pseudo-element is an anonymous box. + #[inline] + pub fn is_anon_box(&self) -> bool { + match *self { + % for pseudo in PSEUDOS: + % if pseudo.is_anon_box(): + ${pseudo_element_variant(pseudo)} => true, + % endif + % endfor + _ => false, + } + } + + /// Whether this pseudo-element is eagerly-cascaded. + #[inline] + pub fn is_eager(&self) -> bool { + matches!(*self, + ${" | ".join(map(lambda name: "PseudoElement::{}".format(name), EAGER_PSEUDOS))}) + } + + /// Whether this pseudo-element is tree pseudo-element. + #[inline] + pub fn is_tree_pseudo_element(&self) -> bool { + match *self { + % for pseudo in TREE_PSEUDOS: + ${pseudo_element_variant(pseudo)} => true, + % endfor + _ => false, + } + } + + /// Whether this pseudo-element is an unknown Webkit-prefixed pseudo-element. + #[inline] + pub fn is_unknown_webkit_pseudo_element(&self) -> bool { + matches!(*self, PseudoElement::UnknownWebkit(..)) + } + + /// Gets the flags associated to this pseudo-element, or 0 if it's an + /// anonymous box. + pub fn flags(&self) -> u32 { + match *self { + % for pseudo in PSEUDOS: + ${pseudo_element_variant(pseudo)} => + % if pseudo.is_tree_pseudo_element(): + structs::CSS_PSEUDO_ELEMENT_ENABLED_IN_UA_SHEETS_AND_CHROME, + % elif pseudo.is_anon_box(): + structs::CSS_PSEUDO_ELEMENT_ENABLED_IN_UA_SHEETS, + % else: + structs::SERVO_CSS_PSEUDO_ELEMENT_FLAGS_${pseudo.pseudo_ident}, + % endif + % endfor + PseudoElement::UnknownWebkit(..) => 0, + } + } + + /// Construct a pseudo-element from a `PseudoStyleType`. + #[inline] + pub fn from_pseudo_type(type_: PseudoStyleType) -> Option { + match type_ { + % for pseudo in PSEUDOS: + % if pseudo.is_simple_pseudo_element(): + PseudoStyleType::${pseudo.pseudo_ident} => { + Some(${pseudo_element_variant(pseudo)}) + }, + % endif + % endfor + _ => None, + } + } + + /// Construct a `PseudoStyleType` from a pseudo-element + #[inline] + pub fn pseudo_type(&self) -> PseudoStyleType { + match *self { + % for pseudo in PSEUDOS: + % if pseudo.is_tree_pseudo_element(): + PseudoElement::${pseudo.capitalized_pseudo()}(..) => PseudoStyleType::XULTree, + % elif pseudo.pseudo_ident == "highlight": + PseudoElement::${pseudo.capitalized_pseudo()}(..) => PseudoStyleType::${pseudo.pseudo_ident}, + % else: + PseudoElement::${pseudo.capitalized_pseudo()} => PseudoStyleType::${pseudo.pseudo_ident}, + % endif + % endfor + PseudoElement::UnknownWebkit(..) => unreachable!(), + } + } + + /// Get the argument list of a tree pseudo-element. + #[inline] + pub fn tree_pseudo_args(&self) -> Option<<&[Atom]> { + match *self { + % for pseudo in TREE_PSEUDOS: + PseudoElement::${pseudo.capitalized_pseudo()}(ref args) => Some(args), + % endfor + _ => None, + } + } + + /// Construct a tree pseudo-element from atom and args. + #[inline] + pub fn from_tree_pseudo_atom(atom: &Atom, args: Box<[Atom]>) -> Option { + % for pseudo in PSEUDOS: + % if pseudo.is_tree_pseudo_element(): + if atom == &atom!("${pseudo.value}") { + return Some(PseudoElement::${pseudo.capitalized_pseudo()}(args.into())); + } + % endif + % endfor + None + } + + /// Constructs a pseudo-element from a string of text. + /// + /// Returns `None` if the pseudo-element is not recognised. + #[inline] + pub fn from_slice(name: &str, allow_unkown_webkit: bool) -> Option { + // We don't need to support tree pseudos because functional + // pseudo-elements needs arguments, and thus should be created + // via other methods. + match_ignore_ascii_case! { name, + % for pseudo in SIMPLE_PSEUDOS: + "${pseudo.value[1:]}" => { + return Some(${pseudo_element_variant(pseudo)}) + }, + % endfor + // Alias some legacy prefixed pseudos to their standardized name at parse time: + "-moz-selection" => { + return Some(PseudoElement::Selection); + }, + "-moz-placeholder" => { + return Some(PseudoElement::Placeholder); + }, + "-moz-list-bullet" | "-moz-list-number" => { + return Some(PseudoElement::Marker); + }, + _ => { + if starts_with_ignore_ascii_case(name, "-moz-tree-") { + return PseudoElement::tree_pseudo_element(name, Default::default()) + } + const WEBKIT_PREFIX: &str = "-webkit-"; + if allow_unkown_webkit && starts_with_ignore_ascii_case(name, WEBKIT_PREFIX) { + let part = string_as_ascii_lowercase(&name[WEBKIT_PREFIX.len()..]); + return Some(PseudoElement::UnknownWebkit(part.into())); + } + } + } + + None + } + + /// Constructs a tree pseudo-element from the given name and arguments. + /// "name" must start with "-moz-tree-". + /// + /// Returns `None` if the pseudo-element is not recognized. + #[inline] + pub fn tree_pseudo_element(name: &str, args: thin_vec::ThinVec) -> Option { + debug_assert!(starts_with_ignore_ascii_case(name, "-moz-tree-")); + let tree_part = &name[10..]; + % for pseudo in TREE_PSEUDOS: + if tree_part.eq_ignore_ascii_case("${pseudo.value[11:]}") { + return Some(${pseudo_element_variant(pseudo, "args")}); + } + % endfor + None + } +} + +impl ToCss for PseudoElement { + fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write { + dest.write_char(':')?; + match *self { + % for pseudo in (p for p in PSEUDOS if p.pseudo_ident != "highlight"): + ${pseudo_element_variant(pseudo)} => dest.write_str("${pseudo.value}")?, + % endfor + PseudoElement::Highlight(ref name) => { + dest.write_str(":highlight(")?; + serialize_atom_identifier(name, dest)?; + dest.write_char(')')?; + } + PseudoElement::UnknownWebkit(ref atom) => { + dest.write_str(":-webkit-")?; + serialize_atom_identifier(atom, dest)?; + } + } + if let Some(args) = self.tree_pseudo_args() { + if !args.is_empty() { + dest.write_char('(')?; + let mut iter = args.iter(); + if let Some(first) = iter.next() { + serialize_atom_identifier(&first, dest)?; + for item in iter { + dest.write_str(", ")?; + serialize_atom_identifier(item, dest)?; + } + } + dest.write_char(')')?; + } + } + Ok(()) + } +} diff --git a/servo/components/style/gecko/regen_atoms.py b/servo/components/style/gecko/regen_atoms.py new file mode 100755 index 0000000000..61f2fc4c63 --- /dev/null +++ b/servo/components/style/gecko/regen_atoms.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python + +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +import re +import os +import sys + +from io import BytesIO + +GECKO_DIR = os.path.dirname(__file__.replace("\\", "/")) +sys.path.insert(0, os.path.join(os.path.dirname(GECKO_DIR), "properties")) + +import build + + +# Matches lines like `GK_ATOM(foo, "foo", 0x12345678, true, nsStaticAtom, PseudoElementAtom)`. +PATTERN = re.compile( + '^GK_ATOM\(([^,]*),[^"]*"([^"]*)",\s*(0x[0-9a-f]+),\s*[^,]*,\s*([^,]*),\s*([^)]*)\)', + re.MULTILINE, +) +FILE = "include/nsGkAtomList.h" + + +def map_atom(ident): + if ident in { + "box", + "loop", + "match", + "mod", + "ref", + "self", + "type", + "use", + "where", + "in", + }: + return ident + "_" + return ident + + +class Atom: + def __init__(self, ident, value, hash, ty, atom_type): + self.ident = "nsGkAtoms_{}".format(ident) + self.original_ident = ident + self.value = value + self.hash = hash + # The Gecko type: "nsStaticAtom", "nsCSSPseudoElementStaticAtom", or + # "nsAnonBoxPseudoStaticAtom". + self.ty = ty + # The type of atom: "Atom", "PseudoElement", "NonInheritingAnonBox", + # or "InheritingAnonBox". + self.atom_type = atom_type + + if ( + self.is_pseudo_element() + or self.is_anon_box() + or self.is_tree_pseudo_element() + ): + self.pseudo_ident = (ident.split("_", 1))[1] + + if self.is_anon_box(): + assert self.is_inheriting_anon_box() or self.is_non_inheriting_anon_box() + + def type(self): + return self.ty + + def capitalized_pseudo(self): + return self.pseudo_ident[0].upper() + self.pseudo_ident[1:] + + def is_pseudo_element(self): + return self.atom_type == "PseudoElementAtom" + + def is_anon_box(self): + if self.is_tree_pseudo_element(): + return False + return self.is_non_inheriting_anon_box() or self.is_inheriting_anon_box() + + def is_non_inheriting_anon_box(self): + assert not self.is_tree_pseudo_element() + return self.atom_type == "NonInheritingAnonBoxAtom" + + def is_inheriting_anon_box(self): + if self.is_tree_pseudo_element(): + return False + return self.atom_type == "InheritingAnonBoxAtom" + + def is_tree_pseudo_element(self): + return self.value.startswith(":-moz-tree-") + + def is_simple_pseudo_element(self) -> bool: + return not (self.is_tree_pseudo_element() or self.pseudo_ident == "highlight") + + +def collect_atoms(objdir): + atoms = [] + path = os.path.abspath(os.path.join(objdir, FILE)) + print("cargo:rerun-if-changed={}".format(path)) + with open(path) as f: + content = f.read() + for result in PATTERN.finditer(content): + atoms.append( + Atom( + result.group(1), + result.group(2), + result.group(3), + result.group(4), + result.group(5), + ) + ) + return atoms + + +class FileAvoidWrite(BytesIO): + """File-like object that buffers output and only writes if content changed.""" + + def __init__(self, filename): + BytesIO.__init__(self) + self.name = filename + + def write(self, buf): + if isinstance(buf, str): + buf = buf.encode("utf-8") + BytesIO.write(self, buf) + + def close(self): + buf = self.getvalue() + BytesIO.close(self) + try: + with open(self.name, "rb") as f: + old_content = f.read() + if old_content == buf: + print("{} is not changed, skip".format(self.name)) + return + except IOError: + pass + with open(self.name, "wb") as f: + f.write(buf) + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + if not self.closed: + self.close() + + +PRELUDE = """ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +// Autogenerated file created by components/style/gecko/regen_atoms.py. +// DO NOT EDIT DIRECTLY +"""[ + 1: +] + +RULE_TEMPLATE = """ + ("{atom}") => {{{{ + #[allow(unsafe_code)] #[allow(unused_unsafe)] + unsafe {{ $crate::string_cache::Atom::from_index_unchecked({index}) }} + }}}}; +"""[ + 1: +] + +MACRO_TEMPLATE = """ +/// Returns a static atom by passing the literal string it represents. +#[macro_export] +macro_rules! atom {{ +{body}\ +}} +""" + + +def write_atom_macro(atoms, file_name): + with FileAvoidWrite(file_name) as f: + f.write(PRELUDE) + macro_rules = [ + RULE_TEMPLATE.format(atom=atom.value, name=atom.ident, index=i) + for (i, atom) in enumerate(atoms) + ] + f.write(MACRO_TEMPLATE.format(body="".join(macro_rules))) + + +def write_pseudo_elements(atoms, target_filename): + pseudos = [] + for atom in atoms: + if ( + atom.type() == "nsCSSPseudoElementStaticAtom" + or atom.type() == "nsCSSAnonBoxPseudoStaticAtom" + ): + pseudos.append(atom) + + pseudo_definition_template = os.path.join( + GECKO_DIR, "pseudo_element_definition.mako.rs" + ) + print("cargo:rerun-if-changed={}".format(pseudo_definition_template)) + contents = build.render(pseudo_definition_template, PSEUDOS=pseudos) + + with FileAvoidWrite(target_filename) as f: + f.write(contents) + + +def generate_atoms(dist, out): + atoms = collect_atoms(dist) + write_atom_macro(atoms, os.path.join(out, "atom_macro.rs")) + write_pseudo_elements(atoms, os.path.join(out, "pseudo_element_definition.rs")) + + +if __name__ == "__main__": + if len(sys.argv) != 3: + print("Usage: {} dist out".format(sys.argv[0])) + exit(2) + generate_atoms(sys.argv[1], sys.argv[2]) diff --git a/servo/components/style/gecko/restyle_damage.rs b/servo/components/style/gecko/restyle_damage.rs new file mode 100644 index 0000000000..4749daea18 --- /dev/null +++ b/servo/components/style/gecko/restyle_damage.rs @@ -0,0 +1,121 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Gecko's restyle damage computation (aka change hints, aka `nsChangeHint`). + +use crate::gecko_bindings::bindings; +use crate::gecko_bindings::structs; +use crate::gecko_bindings::structs::nsChangeHint; +use crate::matching::{StyleChange, StyleDifference}; +use crate::properties::ComputedValues; +use std::ops::{BitAnd, BitOr, BitOrAssign, Not}; + +/// The representation of Gecko's restyle damage is just a wrapper over +/// `nsChangeHint`. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct GeckoRestyleDamage(nsChangeHint); + +impl GeckoRestyleDamage { + /// Trivially construct a new `GeckoRestyleDamage`. + #[inline] + pub fn new(raw: nsChangeHint) -> Self { + GeckoRestyleDamage(raw) + } + + /// Get the inner change hint for this damage. + #[inline] + pub fn as_change_hint(&self) -> nsChangeHint { + self.0 + } + + /// Get an empty change hint, that is (`nsChangeHint(0)`). + #[inline] + pub fn empty() -> Self { + GeckoRestyleDamage(nsChangeHint(0)) + } + + /// Returns whether this restyle damage represents the empty damage. + #[inline] + pub fn is_empty(&self) -> bool { + self.0 == nsChangeHint(0) + } + + /// Computes the `StyleDifference` (including the appropriate change hint) + /// given an old and a new style. + pub fn compute_style_difference( + old_style: &ComputedValues, + new_style: &ComputedValues, + ) -> StyleDifference { + let mut any_style_changed = false; + let mut reset_only = false; + let hint = unsafe { + bindings::Gecko_CalcStyleDifference( + old_style.as_gecko_computed_style(), + new_style.as_gecko_computed_style(), + &mut any_style_changed, + &mut reset_only, + ) + }; + if reset_only && !old_style.custom_properties_equal(new_style) { + // The Gecko_CalcStyleDifference call only checks the non-custom + // property structs, so we check the custom properties here. Since + // they generate no damage themselves, we can skip this check if we + // already know we had some inherited (regular) property + // differences. + any_style_changed = true; + reset_only = false; + } + let change = if any_style_changed { + StyleChange::Changed { reset_only } + } else { + StyleChange::Unchanged + }; + let damage = GeckoRestyleDamage(nsChangeHint(hint)); + StyleDifference { damage, change } + } + + /// Returns true if this restyle damage contains all the damage of |other|. + pub fn contains(self, other: Self) -> bool { + self & other == other + } + + /// Gets restyle damage to reconstruct the entire frame, subsuming all + /// other damage. + pub fn reconstruct() -> Self { + GeckoRestyleDamage(structs::nsChangeHint::nsChangeHint_ReconstructFrame) + } +} + +impl Default for GeckoRestyleDamage { + fn default() -> Self { + Self::empty() + } +} + +impl BitOr for GeckoRestyleDamage { + type Output = Self; + fn bitor(self, other: Self) -> Self { + GeckoRestyleDamage(self.0 | other.0) + } +} + +impl BitOrAssign for GeckoRestyleDamage { + fn bitor_assign(&mut self, other: Self) { + *self = *self | other; + } +} + +impl BitAnd for GeckoRestyleDamage { + type Output = Self; + fn bitand(self, other: Self) -> Self { + GeckoRestyleDamage(nsChangeHint((self.0).0 & (other.0).0)) + } +} + +impl Not for GeckoRestyleDamage { + type Output = Self; + fn not(self) -> Self { + GeckoRestyleDamage(nsChangeHint(!(self.0).0)) + } +} diff --git a/servo/components/style/gecko/selector_parser.rs b/servo/components/style/gecko/selector_parser.rs new file mode 100644 index 0000000000..6bf527b141 --- /dev/null +++ b/servo/components/style/gecko/selector_parser.rs @@ -0,0 +1,498 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Gecko-specific bits for selector-parsing. + +use crate::computed_value_flags::ComputedValueFlags; +use crate::invalidation::element::document_state::InvalidationMatchingData; +use crate::properties::ComputedValues; +use crate::selector_parser::{Direction, HorizontalDirection, SelectorParser}; +use crate::str::starts_with_ignore_ascii_case; +use crate::string_cache::{Atom, Namespace, WeakAtom, WeakNamespace}; +use crate::values::{AtomIdent, AtomString}; +use cssparser::{BasicParseError, BasicParseErrorKind, Parser}; +use cssparser::{CowRcStr, SourceLocation, ToCss, Token}; +use dom::{DocumentState, ElementState}; +use selectors::parser::SelectorParseErrorKind; +use std::fmt; +use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss as ToCss_}; +use thin_vec::ThinVec; + +pub use crate::gecko::pseudo_element::{ + PseudoElement, EAGER_PSEUDOS, EAGER_PSEUDO_COUNT, PSEUDO_COUNT, +}; +pub use crate::gecko::snapshot::SnapshotMap; + +bitflags! { + // See NonTSPseudoClass::is_enabled_in() + #[derive(Copy, Clone)] + struct NonTSPseudoClassFlag: u8 { + const PSEUDO_CLASS_ENABLED_IN_UA_SHEETS = 1 << 0; + const PSEUDO_CLASS_ENABLED_IN_CHROME = 1 << 1; + const PSEUDO_CLASS_ENABLED_IN_UA_SHEETS_AND_CHROME = + NonTSPseudoClassFlag::PSEUDO_CLASS_ENABLED_IN_UA_SHEETS.bits | + NonTSPseudoClassFlag::PSEUDO_CLASS_ENABLED_IN_CHROME.bits; + } +} + +/// The type used to store the language argument to the `:lang` pseudo-class. +#[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, ToCss, ToShmem)] +#[css(comma)] +pub struct Lang(#[css(iterable)] pub ThinVec); + +macro_rules! pseudo_class_name { + ([$(($css:expr, $name:ident, $state:tt, $flags:tt),)*]) => { + /// Our representation of a non tree-structural pseudo-class. + #[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)] + pub enum NonTSPseudoClass { + $( + #[doc = $css] + $name, + )* + /// The `:lang` pseudo-class. + Lang(Lang), + /// The `:dir` pseudo-class. + Dir(Direction), + /// The non-standard `:-moz-locale-dir` pseudo-class. + MozLocaleDir(Direction), + } + } +} +apply_non_ts_list!(pseudo_class_name); + +impl ToCss for NonTSPseudoClass { + fn to_css(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + macro_rules! pseudo_class_serialize { + ([$(($css:expr, $name:ident, $state:tt, $flags:tt),)*]) => { + match *self { + $(NonTSPseudoClass::$name => concat!(":", $css),)* + NonTSPseudoClass::Lang(ref lang) => { + dest.write_str(":lang(")?; + lang.to_css(&mut CssWriter::new(dest))?; + return dest.write_char(')'); + }, + NonTSPseudoClass::MozLocaleDir(ref dir) => { + dest.write_str(":-moz-locale-dir(")?; + dir.to_css(&mut CssWriter::new(dest))?; + return dest.write_char(')') + }, + NonTSPseudoClass::Dir(ref dir) => { + dest.write_str(":dir(")?; + dir.to_css(&mut CssWriter::new(dest))?; + return dest.write_char(')') + }, + } + } + } + let ser = apply_non_ts_list!(pseudo_class_serialize); + dest.write_str(ser) + } +} + +impl NonTSPseudoClass { + /// Parses the name and returns a non-ts-pseudo-class if succeeds. + /// None otherwise. It doesn't check whether the pseudo-class is enabled + /// in a particular state. + pub fn parse_non_functional(name: &str) -> Option { + macro_rules! pseudo_class_parse { + ([$(($css:expr, $name:ident, $state:tt, $flags:tt),)*]) => { + match_ignore_ascii_case! { &name, + $($css => Some(NonTSPseudoClass::$name),)* + "-moz-full-screen" => Some(NonTSPseudoClass::Fullscreen), + "-moz-read-only" => Some(NonTSPseudoClass::ReadOnly), + "-moz-read-write" => Some(NonTSPseudoClass::ReadWrite), + "-moz-focusring" => Some(NonTSPseudoClass::FocusVisible), + "-moz-ui-valid" => Some(NonTSPseudoClass::UserValid), + "-moz-ui-invalid" => Some(NonTSPseudoClass::UserInvalid), + "-webkit-autofill" => Some(NonTSPseudoClass::Autofill), + _ => None, + } + } + } + apply_non_ts_list!(pseudo_class_parse) + } + + /// Returns true if this pseudo-class has any of the given flags set. + fn has_any_flag(&self, flags: NonTSPseudoClassFlag) -> bool { + macro_rules! check_flag { + (_) => { + false + }; + ($flags:ident) => { + NonTSPseudoClassFlag::$flags.intersects(flags) + }; + } + macro_rules! pseudo_class_check_is_enabled_in { + ([$(($css:expr, $name:ident, $state:tt, $flags:tt),)*]) => { + match *self { + $(NonTSPseudoClass::$name => check_flag!($flags),)* + NonTSPseudoClass::MozLocaleDir(_) => check_flag!(PSEUDO_CLASS_ENABLED_IN_UA_SHEETS_AND_CHROME), + NonTSPseudoClass::Lang(_) | + NonTSPseudoClass::Dir(_) => false, + } + } + } + apply_non_ts_list!(pseudo_class_check_is_enabled_in) + } + + /// Returns whether the pseudo-class is enabled in content sheets. + #[inline] + fn is_enabled_in_content(&self) -> bool { + if matches!(*self, Self::PopoverOpen) { + return static_prefs::pref!("dom.element.popover.enabled"); + } + !self.has_any_flag(NonTSPseudoClassFlag::PSEUDO_CLASS_ENABLED_IN_UA_SHEETS_AND_CHROME) + } + + /// Get the state flag associated with a pseudo-class, if any. + pub fn state_flag(&self) -> ElementState { + macro_rules! flag { + (_) => { + ElementState::empty() + }; + ($state:ident) => { + ElementState::$state + }; + } + macro_rules! pseudo_class_state { + ([$(($css:expr, $name:ident, $state:tt, $flags:tt),)*]) => { + match *self { + $(NonTSPseudoClass::$name => flag!($state),)* + NonTSPseudoClass::Dir(ref dir) => dir.element_state(), + NonTSPseudoClass::MozLocaleDir(..) | + NonTSPseudoClass::Lang(..) => ElementState::empty(), + } + } + } + apply_non_ts_list!(pseudo_class_state) + } + + /// Get the document state flag associated with a pseudo-class, if any. + pub fn document_state_flag(&self) -> DocumentState { + match *self { + NonTSPseudoClass::MozLocaleDir(ref dir) => match dir.as_horizontal_direction() { + Some(HorizontalDirection::Ltr) => DocumentState::LTR_LOCALE, + Some(HorizontalDirection::Rtl) => DocumentState::RTL_LOCALE, + None => DocumentState::empty(), + }, + NonTSPseudoClass::MozWindowInactive => DocumentState::WINDOW_INACTIVE, + NonTSPseudoClass::MozLWTheme => DocumentState::LWTHEME, + _ => DocumentState::empty(), + } + } + + /// Returns true if the given pseudoclass should trigger style sharing cache + /// revalidation. + pub fn needs_cache_revalidation(&self) -> bool { + self.state_flag().is_empty() && + !matches!( + *self, + // :dir() depends on state only, but may have an empty + // state_flag for invalid arguments. + NonTSPseudoClass::Dir(_) | + // :-moz-is-html only depends on the state of the document and + // the namespace of the element; the former is invariant + // across all the elements involved and the latter is already + // checked for by our caching precondtions. + NonTSPseudoClass::MozIsHTML | + // We prevent style sharing for NAC. + NonTSPseudoClass::MozNativeAnonymous | + // :-moz-placeholder is parsed but never matches. + NonTSPseudoClass::MozPlaceholder | + // :-moz-lwtheme, :-moz-locale-dir and + // :-moz-window-inactive depend only on the state of the + // document, which is invariant across all the elements + // involved in a given style cache. + NonTSPseudoClass::MozLWTheme | + NonTSPseudoClass::MozLocaleDir(_) | + NonTSPseudoClass::MozWindowInactive + ) + } +} + +impl ::selectors::parser::NonTSPseudoClass for NonTSPseudoClass { + type Impl = SelectorImpl; + + #[inline] + fn is_active_or_hover(&self) -> bool { + matches!(*self, NonTSPseudoClass::Active | NonTSPseudoClass::Hover) + } + + /// We intentionally skip the link-related ones. + #[inline] + fn is_user_action_state(&self) -> bool { + matches!( + *self, + NonTSPseudoClass::Hover | NonTSPseudoClass::Active | NonTSPseudoClass::Focus + ) + } +} + +/// The dummy struct we use to implement our selector parsing. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct SelectorImpl; + +/// A set of extra data to carry along with the matching context, either for +/// selector-matching or invalidation. +#[derive(Default)] +pub struct ExtraMatchingData<'a> { + /// The invalidation data to invalidate doc-state pseudo-classes correctly. + pub invalidation_data: InvalidationMatchingData, + + /// The invalidation bits from matching container queries. These are here + /// just for convenience mostly. + pub cascade_input_flags: ComputedValueFlags, + + /// The style of the originating element in order to evaluate @container + /// size queries affecting pseudo-elements. + pub originating_element_style: Option<&'a ComputedValues>, +} + +impl ::selectors::SelectorImpl for SelectorImpl { + type ExtraMatchingData<'a> = ExtraMatchingData<'a>; + type AttrValue = AtomString; + type Identifier = AtomIdent; + type LocalName = AtomIdent; + type NamespacePrefix = AtomIdent; + type NamespaceUrl = Namespace; + type BorrowedNamespaceUrl = WeakNamespace; + type BorrowedLocalName = WeakAtom; + + type PseudoElement = PseudoElement; + type NonTSPseudoClass = NonTSPseudoClass; + + fn should_collect_attr_hash(name: &AtomIdent) -> bool { + !crate::bloom::is_attr_name_excluded_from_filter(name) + } +} + +impl<'a> SelectorParser<'a> { + fn is_pseudo_class_enabled(&self, pseudo_class: &NonTSPseudoClass) -> bool { + if pseudo_class.is_enabled_in_content() { + return true; + } + + if self.in_user_agent_stylesheet() && + pseudo_class.has_any_flag(NonTSPseudoClassFlag::PSEUDO_CLASS_ENABLED_IN_UA_SHEETS) + { + return true; + } + + if self.chrome_rules_enabled() && + pseudo_class.has_any_flag(NonTSPseudoClassFlag::PSEUDO_CLASS_ENABLED_IN_CHROME) + { + return true; + } + + return false; + } + + fn is_pseudo_element_enabled(&self, pseudo_element: &PseudoElement) -> bool { + if pseudo_element.enabled_in_content() { + return true; + } + + if self.in_user_agent_stylesheet() && pseudo_element.enabled_in_ua_sheets() { + return true; + } + + if self.chrome_rules_enabled() && pseudo_element.enabled_in_chrome() { + return true; + } + + return false; + } +} + +impl<'a, 'i> ::selectors::Parser<'i> for SelectorParser<'a> { + type Impl = SelectorImpl; + type Error = StyleParseErrorKind<'i>; + + fn parse_parent_selector(&self) -> bool { + static_prefs::pref!("layout.css.nesting.enabled") + } + + #[inline] + fn parse_slotted(&self) -> bool { + true + } + + #[inline] + fn parse_host(&self) -> bool { + true + } + + #[inline] + fn parse_nth_child_of(&self) -> bool { + static_prefs::pref!("layout.css.nth-child-of.enabled") + } + + #[inline] + fn parse_is_and_where(&self) -> bool { + true + } + + #[inline] + fn parse_has(&self) -> bool { + static_prefs::pref!("layout.css.has-selector.enabled") + } + + #[inline] + fn parse_part(&self) -> bool { + true + } + + #[inline] + fn is_is_alias(&self, function: &str) -> bool { + function.eq_ignore_ascii_case("-moz-any") + } + + #[inline] + fn allow_forgiving_selectors(&self) -> bool { + !self.for_supports_rule + } + + fn parse_non_ts_pseudo_class( + &self, + location: SourceLocation, + name: CowRcStr<'i>, + ) -> Result> { + if let Some(pseudo_class) = NonTSPseudoClass::parse_non_functional(&name) { + if self.is_pseudo_class_enabled(&pseudo_class) { + return Ok(pseudo_class); + } + } + Err( + location.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement( + name, + )), + ) + } + + fn parse_non_ts_functional_pseudo_class<'t>( + &self, + name: CowRcStr<'i>, + parser: &mut Parser<'i, 't>, + ) -> Result> { + let pseudo_class = match_ignore_ascii_case! { &name, + "lang" => { + let result = parser.parse_comma_separated(|input| { + Ok(AtomIdent::from(input.expect_ident_or_string()?.as_ref())) + })?; + if result.is_empty() { + return Err(parser.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + NonTSPseudoClass::Lang(Lang(result.into())) + }, + "-moz-locale-dir" => { + NonTSPseudoClass::MozLocaleDir(Direction::parse(parser)?) + }, + "dir" => { + NonTSPseudoClass::Dir(Direction::parse(parser)?) + }, + _ => return Err(parser.new_custom_error( + SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name.clone()) + )) + }; + if self.is_pseudo_class_enabled(&pseudo_class) { + Ok(pseudo_class) + } else { + Err( + parser.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement( + name, + )), + ) + } + } + + fn parse_pseudo_element( + &self, + location: SourceLocation, + name: CowRcStr<'i>, + ) -> Result> { + let allow_unkown_webkit = !self.for_supports_rule; + if let Some(pseudo) = PseudoElement::from_slice(&name, allow_unkown_webkit) { + if self.is_pseudo_element_enabled(&pseudo) { + return Ok(pseudo); + } + } + + Err( + location.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement( + name, + )), + ) + } + + fn parse_functional_pseudo_element<'t>( + &self, + name: CowRcStr<'i>, + parser: &mut Parser<'i, 't>, + ) -> Result> { + if starts_with_ignore_ascii_case(&name, "-moz-tree-") { + // Tree pseudo-elements can have zero or more arguments, separated + // by either comma or space. + let mut args = ThinVec::new(); + loop { + let location = parser.current_source_location(); + match parser.next() { + Ok(&Token::Ident(ref ident)) => args.push(Atom::from(ident.as_ref())), + Ok(&Token::Comma) => {}, + Ok(t) => return Err(location.new_unexpected_token_error(t.clone())), + Err(BasicParseError { + kind: BasicParseErrorKind::EndOfInput, + .. + }) => break, + _ => unreachable!("Parser::next() shouldn't return any other error"), + } + } + if let Some(pseudo) = PseudoElement::tree_pseudo_element(&name, args) { + if self.is_pseudo_element_enabled(&pseudo) { + return Ok(pseudo); + } + } + } else if name.eq_ignore_ascii_case("highlight") { + let pseudo = PseudoElement::Highlight(AtomIdent::from(parser.expect_ident()?.as_ref())); + if self.is_pseudo_element_enabled(&pseudo) { + return Ok(pseudo); + } + } + Err( + parser.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement( + name, + )), + ) + } + + fn default_namespace(&self) -> Option { + self.namespaces.default.clone() + } + + fn namespace_for_prefix(&self, prefix: &AtomIdent) -> Option { + self.namespaces.prefixes.get(prefix).cloned() + } +} + +impl SelectorImpl { + /// A helper to traverse each eagerly cascaded pseudo-element, executing + /// `fun` on it. + #[inline] + pub fn each_eagerly_cascaded_pseudo_element(mut fun: F) + where + F: FnMut(PseudoElement), + { + for pseudo in &EAGER_PSEUDOS { + fun(pseudo.clone()) + } + } +} + +// Selector and component sizes are important for matching performance. +size_of_test!(selectors::parser::Selector, 8); +size_of_test!(selectors::parser::Component, 24); +size_of_test!(PseudoElement, 16); +size_of_test!(NonTSPseudoClass, 16); diff --git a/servo/components/style/gecko/snapshot.rs b/servo/components/style/gecko/snapshot.rs new file mode 100644 index 0000000000..cb5f2dea67 --- /dev/null +++ b/servo/components/style/gecko/snapshot.rs @@ -0,0 +1,238 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! A gecko snapshot, that stores the element attributes and state before they +//! change in order to properly calculate restyle hints. + +use crate::dom::TElement; +use crate::gecko::snapshot_helpers; +use crate::gecko::wrapper::{GeckoElement, NamespaceConstraintHelpers}; +use crate::gecko_bindings::bindings; +use crate::gecko_bindings::structs::ServoElementSnapshot; +use crate::gecko_bindings::structs::ServoElementSnapshotFlags as Flags; +use crate::gecko_bindings::structs::ServoElementSnapshotTable; +use crate::invalidation::element::element_wrapper::ElementSnapshot; +use crate::selector_parser::AttrValue; +use crate::string_cache::{Atom, Namespace}; +use crate::values::{AtomIdent, AtomString}; +use crate::LocalName; +use crate::WeakAtom; +use dom::ElementState; +use selectors::attr::{AttrSelectorOperation, AttrSelectorOperator}; +use selectors::attr::{CaseSensitivity, NamespaceConstraint}; + +/// A snapshot of a Gecko element. +pub type GeckoElementSnapshot = ServoElementSnapshot; + +/// A map from elements to snapshots for Gecko's style back-end. +pub type SnapshotMap = ServoElementSnapshotTable; + +impl SnapshotMap { + /// Gets the snapshot for this element, if any. + /// + /// FIXME(emilio): The transmute() business we do here is kind of nasty, but + /// it's a consequence of the map being a OpaqueNode -> Snapshot table in + /// Servo and an Element -> Snapshot table in Gecko. + /// + /// We should be able to make this a more type-safe with type annotations by + /// making SnapshotMap a trait and moving the implementations outside, but + /// that's a pain because it implies parameterizing SharedStyleContext. + pub fn get(&self, element: &E) -> Option<&GeckoElementSnapshot> { + debug_assert!(element.has_snapshot()); + + unsafe { + let element = ::std::mem::transmute::<&E, &GeckoElement>(element); + bindings::Gecko_GetElementSnapshot(self, element.0).as_ref() + } + } +} + +impl GeckoElementSnapshot { + #[inline] + fn has_any(&self, flags: Flags) -> bool { + (self.mContains as u8 & flags as u8) != 0 + } + + /// Returns true if the snapshot has stored state for pseudo-classes + /// that depend on things other than `ElementState`. + #[inline] + pub fn has_other_pseudo_class_state(&self) -> bool { + self.has_any(Flags::OtherPseudoClassState) + } + + /// Returns true if the snapshot recorded an id change. + #[inline] + pub fn id_changed(&self) -> bool { + self.mIdAttributeChanged() + } + + /// Returns true if the snapshot recorded a class attribute change. + #[inline] + pub fn class_changed(&self) -> bool { + self.mClassAttributeChanged() + } + + /// Executes the callback once for each attribute that changed. + #[inline] + pub fn each_attr_changed(&self, mut callback: F) + where + F: FnMut(&AtomIdent), + { + for attr in self.mChangedAttrNames.iter() { + unsafe { AtomIdent::with(attr.mRawPtr, &mut callback) } + } + } + + /// selectors::Element::attr_matches + pub fn attr_matches( + &self, + ns: &NamespaceConstraint<&Namespace>, + local_name: &LocalName, + operation: &AttrSelectorOperation<&AttrValue>, + ) -> bool { + unsafe { + match *operation { + AttrSelectorOperation::Exists => { + bindings::Gecko_SnapshotHasAttr(self, ns.atom_or_null(), local_name.as_ptr()) + }, + AttrSelectorOperation::WithValue { + operator, + case_sensitivity, + expected_value, + } => { + let ignore_case = match case_sensitivity { + CaseSensitivity::CaseSensitive => false, + CaseSensitivity::AsciiCaseInsensitive => true, + }; + // FIXME: case sensitivity for operators other than Equal + match operator { + AttrSelectorOperator::Equal => bindings::Gecko_SnapshotAttrEquals( + self, + ns.atom_or_null(), + local_name.as_ptr(), + expected_value.as_ptr(), + ignore_case, + ), + AttrSelectorOperator::Includes => bindings::Gecko_SnapshotAttrIncludes( + self, + ns.atom_or_null(), + local_name.as_ptr(), + expected_value.as_ptr(), + ignore_case, + ), + AttrSelectorOperator::DashMatch => bindings::Gecko_SnapshotAttrDashEquals( + self, + ns.atom_or_null(), + local_name.as_ptr(), + expected_value.as_ptr(), + ignore_case, + ), + AttrSelectorOperator::Prefix => bindings::Gecko_SnapshotAttrHasPrefix( + self, + ns.atom_or_null(), + local_name.as_ptr(), + expected_value.as_ptr(), + ignore_case, + ), + AttrSelectorOperator::Suffix => bindings::Gecko_SnapshotAttrHasSuffix( + self, + ns.atom_or_null(), + local_name.as_ptr(), + expected_value.as_ptr(), + ignore_case, + ), + AttrSelectorOperator::Substring => { + bindings::Gecko_SnapshotAttrHasSubstring( + self, + ns.atom_or_null(), + local_name.as_ptr(), + expected_value.as_ptr(), + ignore_case, + ) + }, + } + }, + } + } + } +} + +impl ElementSnapshot for GeckoElementSnapshot { + fn debug_list_attributes(&self) -> String { + use nsstring::nsCString; + let mut string = nsCString::new(); + unsafe { + bindings::Gecko_Snapshot_DebugListAttributes(self, &mut string); + } + String::from_utf8_lossy(&*string).into_owned() + } + + fn state(&self) -> Option { + if self.has_any(Flags::State) { + Some(ElementState::from_bits_retain(self.mState)) + } else { + None + } + } + + #[inline] + fn has_attrs(&self) -> bool { + self.has_any(Flags::Attributes) + } + + #[inline] + fn id_attr(&self) -> Option<&WeakAtom> { + if !self.has_any(Flags::Id) { + return None; + } + + snapshot_helpers::get_id(&*self.mAttrs) + } + + #[inline] + fn is_part(&self, name: &AtomIdent) -> bool { + let attr = match snapshot_helpers::find_attr(&*self.mAttrs, &atom!("part")) { + Some(attr) => attr, + None => return false, + }; + + snapshot_helpers::has_class_or_part(name, CaseSensitivity::CaseSensitive, attr) + } + + #[inline] + fn imported_part(&self, name: &AtomIdent) -> Option { + snapshot_helpers::imported_part(&*self.mAttrs, name) + } + + #[inline] + fn has_class(&self, name: &AtomIdent, case_sensitivity: CaseSensitivity) -> bool { + if !self.has_any(Flags::MaybeClass) { + return false; + } + + snapshot_helpers::has_class_or_part(name, case_sensitivity, &self.mClass) + } + + #[inline] + fn each_class(&self, callback: F) + where + F: FnMut(&AtomIdent), + { + if !self.has_any(Flags::MaybeClass) { + return; + } + + snapshot_helpers::each_class_or_part(&self.mClass, callback) + } + + #[inline] + fn lang_attr(&self) -> Option { + let ptr = unsafe { bindings::Gecko_SnapshotLangValue(self) }; + if ptr.is_null() { + None + } else { + Some(AtomString(unsafe { Atom::from_addrefed(ptr) })) + } + } +} diff --git a/servo/components/style/gecko/snapshot_helpers.rs b/servo/components/style/gecko/snapshot_helpers.rs new file mode 100644 index 0000000000..73d740012b --- /dev/null +++ b/servo/components/style/gecko/snapshot_helpers.rs @@ -0,0 +1,196 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Element an snapshot common logic. + +use crate::dom::TElement; +use crate::gecko_bindings::bindings; +use crate::gecko_bindings::structs::{self, nsAtom}; +use crate::invalidation::element::element_wrapper::ElementSnapshot; +use crate::selector_parser::SnapshotMap; +use crate::string_cache::WeakAtom; +use crate::values::AtomIdent; +use crate::Atom; +use crate::CaseSensitivityExt; +use selectors::attr::CaseSensitivity; +use smallvec::SmallVec; + +/// A function that, given an element of type `T`, allows you to get a single +/// class or a class list. +enum Class<'a> { + None, + One(*const nsAtom), + More(&'a [structs::RefPtr]), +} + +#[inline(always)] +fn base_type(attr: &structs::nsAttrValue) -> structs::nsAttrValue_ValueBaseType { + (attr.mBits & structs::NS_ATTRVALUE_BASETYPE_MASK) as structs::nsAttrValue_ValueBaseType +} + +#[inline(always)] +unsafe fn ptr(attr: &structs::nsAttrValue) -> *const T { + (attr.mBits & !structs::NS_ATTRVALUE_BASETYPE_MASK) as *const T +} + +#[inline(always)] +unsafe fn get_class_or_part_from_attr(attr: &structs::nsAttrValue) -> Class { + debug_assert!(bindings::Gecko_AssertClassAttrValueIsSane(attr)); + let base_type = base_type(attr); + if base_type == structs::nsAttrValue_ValueBaseType_eAtomBase { + return Class::One(ptr::(attr)); + } + if base_type == structs::nsAttrValue_ValueBaseType_eOtherBase { + let container = ptr::(attr); + debug_assert_eq!( + (*container).mType, + structs::nsAttrValue_ValueType_eAtomArray + ); + // NOTE: Bindgen doesn't deal with AutoTArray, so cast it below. + let attr_array: *mut _ = *(*container) + .__bindgen_anon_1 + .mValue + .as_ref() + .__bindgen_anon_1 + .mAtomArray + .as_ref(); + let array = + (*attr_array).mArray.as_ptr() as *const structs::nsTArray>; + return Class::More(&**array); + } + debug_assert_eq!(base_type, structs::nsAttrValue_ValueBaseType_eStringBase); + Class::None +} + +#[inline(always)] +unsafe fn get_id_from_attr(attr: &structs::nsAttrValue) -> &WeakAtom { + debug_assert_eq!( + base_type(attr), + structs::nsAttrValue_ValueBaseType_eAtomBase + ); + WeakAtom::new(ptr::(attr)) +} + +/// Find an attribute value with a given name and no namespace. +#[inline(always)] +pub fn find_attr<'a>( + attrs: &'a [structs::AttrArray_InternalAttr], + name: &Atom, +) -> Option<&'a structs::nsAttrValue> { + attrs + .iter() + .find(|attr| attr.mName.mBits == name.as_ptr() as usize) + .map(|attr| &attr.mValue) +} + +/// Finds the id attribute from a list of attributes. +#[inline(always)] +pub fn get_id(attrs: &[structs::AttrArray_InternalAttr]) -> Option<&WeakAtom> { + Some(unsafe { get_id_from_attr(find_attr(attrs, &atom!("id"))?) }) +} + +#[inline(always)] +pub(super) fn each_exported_part( + attrs: &[structs::AttrArray_InternalAttr], + name: &AtomIdent, + mut callback: impl FnMut(&AtomIdent), +) { + let attr = match find_attr(attrs, &atom!("exportparts")) { + Some(attr) => attr, + None => return, + }; + let mut length = 0; + let atoms = unsafe { bindings::Gecko_Element_ExportedParts(attr, name.as_ptr(), &mut length) }; + if atoms.is_null() { + return; + } + + unsafe { + for atom in std::slice::from_raw_parts(atoms, length) { + AtomIdent::with(*atom, &mut callback) + } + } +} + +#[inline(always)] +pub(super) fn imported_part( + attrs: &[structs::AttrArray_InternalAttr], + name: &AtomIdent, +) -> Option { + let attr = find_attr(attrs, &atom!("exportparts"))?; + let atom = unsafe { bindings::Gecko_Element_ImportedPart(attr, name.as_ptr()) }; + if atom.is_null() { + return None; + } + Some(AtomIdent(unsafe { Atom::from_raw(atom) })) +} + +/// Given a class or part name, a case sensitivity, and an array of attributes, +/// returns whether the attribute has that name. +#[inline(always)] +pub fn has_class_or_part( + name: &AtomIdent, + case_sensitivity: CaseSensitivity, + attr: &structs::nsAttrValue, +) -> bool { + match unsafe { get_class_or_part_from_attr(attr) } { + Class::None => false, + Class::One(atom) => unsafe { case_sensitivity.eq_atom(name, WeakAtom::new(atom)) }, + Class::More(atoms) => match case_sensitivity { + CaseSensitivity::CaseSensitive => { + let name_ptr = name.as_ptr(); + atoms.iter().any(|atom| atom.mRawPtr == name_ptr) + }, + CaseSensitivity::AsciiCaseInsensitive => unsafe { + atoms + .iter() + .any(|atom| WeakAtom::new(atom.mRawPtr).eq_ignore_ascii_case(name)) + }, + }, + } +} + +/// Given an item, a callback, and a getter, execute `callback` for each class +/// or part name this `item` has. +#[inline(always)] +pub fn each_class_or_part(attr: &structs::nsAttrValue, mut callback: F) +where + F: FnMut(&AtomIdent), +{ + unsafe { + match get_class_or_part_from_attr(attr) { + Class::None => {}, + Class::One(atom) => AtomIdent::with(atom, callback), + Class::More(atoms) => { + for atom in atoms { + AtomIdent::with(atom.mRawPtr, &mut callback) + } + }, + } + } +} + +/// Returns a list of classes that were either added to or removed from the +/// element since the snapshot. +pub fn classes_changed(element: &E, snapshots: &SnapshotMap) -> SmallVec<[Atom; 8]> { + debug_assert!(element.has_snapshot(), "Why bothering?"); + let snapshot = snapshots.get(element).expect("has_snapshot lied"); + if !snapshot.class_changed() { + return SmallVec::new(); + } + + let mut classes_changed = SmallVec::<[Atom; 8]>::new(); + snapshot.each_class(|c| { + if !element.has_class(c, CaseSensitivity::CaseSensitive) { + classes_changed.push(c.0.clone()); + } + }); + element.each_class(|c| { + if !snapshot.has_class(c, CaseSensitivity::CaseSensitive) { + classes_changed.push(c.0.clone()); + } + }); + + classes_changed +} diff --git a/servo/components/style/gecko/traversal.rs b/servo/components/style/gecko/traversal.rs new file mode 100644 index 0000000000..71d1a2f949 --- /dev/null +++ b/servo/components/style/gecko/traversal.rs @@ -0,0 +1,53 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Gecko-specific bits for the styling DOM traversal. + +use crate::context::{SharedStyleContext, StyleContext}; +use crate::dom::{TElement, TNode}; +use crate::gecko::wrapper::{GeckoElement, GeckoNode}; +use crate::traversal::{recalc_style_at, DomTraversal, PerLevelTraversalData}; + +/// This is the simple struct that Gecko uses to encapsulate a DOM traversal for +/// styling. +pub struct RecalcStyleOnly<'a> { + shared: SharedStyleContext<'a>, +} + +impl<'a> RecalcStyleOnly<'a> { + /// Create a `RecalcStyleOnly` traversal from a `SharedStyleContext`. + pub fn new(shared: SharedStyleContext<'a>) -> Self { + RecalcStyleOnly { shared: shared } + } +} + +impl<'recalc, 'le> DomTraversal> for RecalcStyleOnly<'recalc> { + fn process_preorder( + &self, + traversal_data: &PerLevelTraversalData, + context: &mut StyleContext>, + node: GeckoNode<'le>, + note_child: F, + ) where + F: FnMut(GeckoNode<'le>), + { + if let Some(el) = node.as_element() { + let mut data = unsafe { el.ensure_data() }; + recalc_style_at(self, traversal_data, context, el, &mut data, note_child); + } + } + + fn process_postorder(&self, _: &mut StyleContext>, _: GeckoNode<'le>) { + unreachable!(); + } + + /// We don't use the post-order traversal for anything. + fn needs_postorder_traversal() -> bool { + false + } + + fn shared_context(&self) -> &SharedStyleContext { + &self.shared + } +} diff --git a/servo/components/style/gecko/url.rs b/servo/components/style/gecko/url.rs new file mode 100644 index 0000000000..8cf4aa51c0 --- /dev/null +++ b/servo/components/style/gecko/url.rs @@ -0,0 +1,383 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Common handling for the specified value CSS url() values. + +use crate::gecko_bindings::bindings; +use crate::gecko_bindings::structs; +use crate::parser::{Parse, ParserContext}; +use crate::stylesheets::{CorsMode, UrlExtraData}; +use crate::values::computed::{Context, ToComputedValue}; +use cssparser::Parser; +use malloc_size_of::{MallocSizeOf, MallocSizeOfOps}; +use nsstring::nsCString; +use servo_arc::Arc; +use std::collections::HashMap; +use std::fmt::{self, Write}; +use std::mem::ManuallyDrop; +use std::sync::RwLock; +use style_traits::{CssWriter, ParseError, ToCss}; +use to_shmem::{self, SharedMemoryBuilder, ToShmem}; + +/// A CSS url() value for gecko. +#[derive(Clone, Debug, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +#[css(function = "url")] +#[repr(C)] +pub struct CssUrl(pub Arc); + +/// Data shared between CssUrls. +/// +/// cbindgen:derive-eq=false +/// cbindgen:derive-neq=false +#[derive(Debug, SpecifiedValueInfo, ToCss, ToShmem)] +#[repr(C)] +pub struct CssUrlData { + /// The URL in unresolved string form. + serialization: crate::OwnedStr, + + /// The URL extra data. + #[css(skip)] + pub extra_data: UrlExtraData, + + /// The CORS mode that will be used for the load. + #[css(skip)] + cors_mode: CorsMode, + + /// Data to trigger a load from Gecko. This is mutable in C++. + /// + /// TODO(emilio): Maybe we can eagerly resolve URLs and make this immutable? + #[css(skip)] + load_data: LoadDataSource, +} + +impl PartialEq for CssUrlData { + fn eq(&self, other: &Self) -> bool { + self.serialization == other.serialization && + self.extra_data == other.extra_data && + self.cors_mode == other.cors_mode + } +} + +impl CssUrl { + fn parse_with_cors_mode<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + cors_mode: CorsMode, + ) -> Result> { + let url = input.expect_url()?; + Ok(Self::parse_from_string( + url.as_ref().to_owned(), + context, + cors_mode, + )) + } + + /// Parse a URL from a string value that is a valid CSS token for a URL. + pub fn parse_from_string(url: String, context: &ParserContext, cors_mode: CorsMode) -> Self { + CssUrl(Arc::new(CssUrlData { + serialization: url.into(), + extra_data: context.url_data.clone(), + cors_mode, + load_data: LoadDataSource::Owned(LoadData::default()), + })) + } + + /// Returns true if the URL is definitely invalid. We don't eagerly resolve + /// URLs in gecko, so we just return false here. + /// use its |resolved| status. + pub fn is_invalid(&self) -> bool { + false + } + + /// Returns true if this URL looks like a fragment. + /// See https://drafts.csswg.org/css-values/#local-urls + #[inline] + pub fn is_fragment(&self) -> bool { + self.0.is_fragment() + } + + /// Return the unresolved url as string, or the empty string if it's + /// invalid. + #[inline] + pub fn as_str(&self) -> &str { + self.0.as_str() + } +} + +impl CssUrlData { + /// Returns true if this URL looks like a fragment. + /// See https://drafts.csswg.org/css-values/#local-urls + pub fn is_fragment(&self) -> bool { + self.as_str() + .as_bytes() + .iter() + .next() + .map_or(false, |b| *b == b'#') + } + + /// Return the unresolved url as string, or the empty string if it's + /// invalid. + pub fn as_str(&self) -> &str { + &*self.serialization + } +} + +impl Parse for CssUrl { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + Self::parse_with_cors_mode(context, input, CorsMode::None) + } +} + +impl Eq for CssUrl {} + +impl MallocSizeOf for CssUrl { + fn size_of(&self, _ops: &mut MallocSizeOfOps) -> usize { + // XXX: measure `serialization` once bug 1397971 lands + + // We ignore `extra_data`, because RefPtr is tricky, and there aren't + // many of them in practise (sharing is common). + + 0 + } +} + +/// A key type for LOAD_DATA_TABLE. +#[derive(Eq, Hash, PartialEq)] +struct LoadDataKey(*const LoadDataSource); + +unsafe impl Sync for LoadDataKey {} +unsafe impl Send for LoadDataKey {} + +bitflags! { + /// Various bits of mutable state that are kept for image loads. + #[repr(C)] + pub struct LoadDataFlags: u8 { + /// Whether we tried to resolve the uri at least once. + const TRIED_TO_RESOLVE_URI = 1 << 0; + /// Whether we tried to resolve the image at least once. + const TRIED_TO_RESOLVE_IMAGE = 1 << 1; + } +} + +/// This is usable and movable from multiple threads just fine, as long as it's +/// not cloned (it is not clonable), and the methods that mutate it run only on +/// the main thread (when all the other threads we care about are paused). +unsafe impl Sync for LoadData {} +unsafe impl Send for LoadData {} + +/// The load data for a given URL. This is mutable from C++, and shouldn't be +/// accessed from rust for anything. +#[repr(C)] +#[derive(Debug)] +pub struct LoadData { + /// A strong reference to the imgRequestProxy, if any, that should be + /// released on drop. + /// + /// These are raw pointers because they are not safe to reference-count off + /// the main thread. + resolved_image: *mut structs::imgRequestProxy, + /// A strong reference to the resolved URI of this image. + resolved_uri: *mut structs::nsIURI, + /// A few flags that are set when resolving the image or such. + flags: LoadDataFlags, +} + +impl Drop for LoadData { + fn drop(&mut self) { + unsafe { bindings::Gecko_LoadData_Drop(self) } + } +} + +impl Default for LoadData { + fn default() -> Self { + Self { + resolved_image: std::ptr::null_mut(), + resolved_uri: std::ptr::null_mut(), + flags: LoadDataFlags::empty(), + } + } +} + +/// The data for a load, or a lazy-loaded, static member that will be stored in +/// LOAD_DATA_TABLE, keyed by the memory location of this object, which is +/// always in the heap because it's inside the CssUrlData object. +/// +/// This type is meant not to be used from C++ so we don't derive helper +/// methods. +/// +/// cbindgen:derive-helper-methods=false +#[derive(Debug)] +#[repr(u8, C)] +pub enum LoadDataSource { + /// An owned copy of the load data. + Owned(LoadData), + /// A lazily-resolved copy of it. + Lazy, +} + +impl LoadDataSource { + /// Gets the load data associated with the source. + /// + /// This relies on the source on being in a stable location if lazy. + #[inline] + pub unsafe fn get(&self) -> *const LoadData { + match *self { + LoadDataSource::Owned(ref d) => return d, + LoadDataSource::Lazy => {}, + } + + let key = LoadDataKey(self); + + { + let guard = LOAD_DATA_TABLE.read().unwrap(); + if let Some(r) = guard.get(&key) { + return &**r; + } + } + let mut guard = LOAD_DATA_TABLE.write().unwrap(); + let r = guard.entry(key).or_insert_with(Default::default); + &**r + } +} + +impl ToShmem for LoadDataSource { + fn to_shmem(&self, _builder: &mut SharedMemoryBuilder) -> to_shmem::Result { + Ok(ManuallyDrop::new(match self { + LoadDataSource::Owned(..) => LoadDataSource::Lazy, + LoadDataSource::Lazy => LoadDataSource::Lazy, + })) + } +} + +/// A specified non-image `url()` value. +pub type SpecifiedUrl = CssUrl; + +/// Clears LOAD_DATA_TABLE. Entries in this table, which are for specified URL +/// values that come from shared memory style sheets, would otherwise persist +/// until the end of the process and be reported as leaks. +pub fn shutdown() { + LOAD_DATA_TABLE.write().unwrap().clear(); +} + +impl ToComputedValue for SpecifiedUrl { + type ComputedValue = ComputedUrl; + + #[inline] + fn to_computed_value(&self, _: &Context) -> Self::ComputedValue { + ComputedUrl(self.clone()) + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + computed.0.clone() + } +} + +/// A specified image `url()` value. +#[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub struct SpecifiedImageUrl(pub SpecifiedUrl); + +impl SpecifiedImageUrl { + /// Parse a URL from a string value that is a valid CSS token for a URL. + pub fn parse_from_string(url: String, context: &ParserContext, cors_mode: CorsMode) -> Self { + SpecifiedImageUrl(SpecifiedUrl::parse_from_string(url, context, cors_mode)) + } + + /// Provides an alternate method for parsing that associates the URL + /// with anonymous CORS headers. + pub fn parse_with_cors_mode<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + cors_mode: CorsMode, + ) -> Result> { + Ok(SpecifiedImageUrl(SpecifiedUrl::parse_with_cors_mode( + context, input, cors_mode, + )?)) + } +} + +impl Parse for SpecifiedImageUrl { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + SpecifiedUrl::parse(context, input).map(SpecifiedImageUrl) + } +} + +impl ToComputedValue for SpecifiedImageUrl { + type ComputedValue = ComputedImageUrl; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + ComputedImageUrl(self.0.to_computed_value(context)) + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + SpecifiedImageUrl(ToComputedValue::from_computed_value(&computed.0)) + } +} + +/// The computed value of a CSS non-image `url()`. +/// +/// The only difference between specified and computed URLs is the +/// serialization. +#[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq)] +#[repr(C)] +pub struct ComputedUrl(pub SpecifiedUrl); + +impl ComputedUrl { + fn serialize_with( + &self, + function: unsafe extern "C" fn(*const Self, *mut nsCString), + dest: &mut CssWriter, + ) -> fmt::Result + where + W: Write, + { + dest.write_str("url(")?; + unsafe { + let mut string = nsCString::new(); + function(self, &mut string); + string.as_str_unchecked().to_css(dest)?; + } + dest.write_char(')') + } +} + +impl ToCss for ComputedUrl { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: Write, + { + self.serialize_with(bindings::Gecko_GetComputedURLSpec, dest) + } +} + +/// The computed value of a CSS image `url()`. +#[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq)] +#[repr(transparent)] +pub struct ComputedImageUrl(pub ComputedUrl); + +impl ToCss for ComputedImageUrl { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: Write, + { + self.0 + .serialize_with(bindings::Gecko_GetComputedImageURLSpec, dest) + } +} + +lazy_static! { + /// A table mapping CssUrlData objects to their lazily created LoadData + /// objects. + static ref LOAD_DATA_TABLE: RwLock>> = { + Default::default() + }; +} diff --git a/servo/components/style/gecko/values.rs b/servo/components/style/gecko/values.rs new file mode 100644 index 0000000000..fbdb02c6ba --- /dev/null +++ b/servo/components/style/gecko/values.rs @@ -0,0 +1,72 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#![allow(unsafe_code)] + +//! Different kind of helpers to interact with Gecko values. + +use crate::color::{AbsoluteColor, ColorSpace}; +use crate::counter_style::{Symbol, Symbols}; +use crate::gecko_bindings::bindings; +use crate::gecko_bindings::structs::CounterStylePtr; +use crate::values::generics::CounterStyle; +use crate::values::Either; +use crate::Atom; + +/// Convert a color value to `nscolor`. +pub fn convert_absolute_color_to_nscolor(color: &AbsoluteColor) -> u32 { + let srgb = color.to_color_space(ColorSpace::Srgb); + u32::from_le_bytes([ + (srgb.components.0 * 255.0).round() as u8, + (srgb.components.1 * 255.0).round() as u8, + (srgb.components.2 * 255.0).round() as u8, + (srgb.alpha * 255.0).round() as u8, + ]) +} + +/// Convert a given `nscolor` to a Servo AbsoluteColor value. +pub fn convert_nscolor_to_absolute_color(color: u32) -> AbsoluteColor { + let [r, g, b, a] = color.to_le_bytes(); + AbsoluteColor::srgb( + r as f32 / 255.0, + g as f32 / 255.0, + b as f32 / 255.0, + a as f32 / 255.0, + ) +} + +impl CounterStyle { + /// Convert this counter style to a Gecko CounterStylePtr. + #[inline] + pub fn to_gecko_value(&self, gecko_value: &mut CounterStylePtr) { + unsafe { bindings::Gecko_CounterStyle_ToPtr(self, gecko_value) } + } + + /// Convert Gecko CounterStylePtr to CounterStyle or String. + pub fn from_gecko_value(gecko_value: &CounterStylePtr) -> Either { + use crate::values::CustomIdent; + + let name = unsafe { bindings::Gecko_CounterStyle_GetName(gecko_value) }; + if !name.is_null() { + let name = unsafe { Atom::from_raw(name) }; + debug_assert_ne!(name, atom!("none")); + Either::First(CounterStyle::Name(CustomIdent(name))) + } else { + let anonymous = + unsafe { bindings::Gecko_CounterStyle_GetAnonymous(gecko_value).as_ref() }.unwrap(); + let symbols = &anonymous.mSymbols; + if anonymous.mSingleString { + debug_assert_eq!(symbols.len(), 1); + Either::Second(symbols[0].to_string()) + } else { + let symbol_type = anonymous.mSymbolsType; + let symbols = symbols + .iter() + .map(|gecko_symbol| Symbol::String(gecko_symbol.to_string().into())) + .collect(); + Either::First(CounterStyle::Symbols(symbol_type, Symbols(symbols))) + } + } + } +} diff --git a/servo/components/style/gecko/wrapper.rs b/servo/components/style/gecko/wrapper.rs new file mode 100644 index 0000000000..8c9ba40f93 --- /dev/null +++ b/servo/components/style/gecko/wrapper.rs @@ -0,0 +1,2199 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#![allow(unsafe_code)] + +//! Wrapper definitions on top of Gecko types in order to be used in the style +//! system. +//! +//! This really follows the Servo pattern in +//! `components/script/layout_wrapper.rs`. +//! +//! This theoretically should live in its own crate, but now it lives in the +//! style system it's kind of pointless in the Stylo case, and only Servo forces +//! the separation between the style system implementation and everything else. + +use crate::applicable_declarations::ApplicableDeclarationBlock; +use crate::context::{PostAnimationTasks, QuirksMode, SharedStyleContext, UpdateAnimationsTasks}; +use crate::data::ElementData; +use crate::dom::{LayoutIterator, NodeInfo, OpaqueNode, TDocument, TElement, TNode, TShadowRoot}; +use crate::gecko::selector_parser::{NonTSPseudoClass, PseudoElement, SelectorImpl}; +use crate::gecko::snapshot_helpers; +use crate::gecko_bindings::bindings; +use crate::gecko_bindings::bindings::Gecko_ElementHasAnimations; +use crate::gecko_bindings::bindings::Gecko_ElementHasCSSAnimations; +use crate::gecko_bindings::bindings::Gecko_ElementHasCSSTransitions; +use crate::gecko_bindings::bindings::Gecko_ElementState; +use crate::gecko_bindings::bindings::Gecko_GetActiveLinkAttrDeclarationBlock; +use crate::gecko_bindings::bindings::Gecko_GetAnimationEffectCount; +use crate::gecko_bindings::bindings::Gecko_GetAnimationRule; +use crate::gecko_bindings::bindings::Gecko_GetExtraContentStyleDeclarations; +use crate::gecko_bindings::bindings::Gecko_GetHTMLPresentationAttrDeclarationBlock; +use crate::gecko_bindings::bindings::Gecko_GetStyleAttrDeclarationBlock; +use crate::gecko_bindings::bindings::Gecko_GetUnvisitedLinkAttrDeclarationBlock; +use crate::gecko_bindings::bindings::Gecko_GetVisitedLinkAttrDeclarationBlock; +use crate::gecko_bindings::bindings::Gecko_IsSignificantChild; +use crate::gecko_bindings::bindings::Gecko_MatchLang; +use crate::gecko_bindings::bindings::Gecko_UnsetDirtyStyleAttr; +use crate::gecko_bindings::bindings::Gecko_UpdateAnimations; +use crate::gecko_bindings::structs; +use crate::gecko_bindings::structs::nsChangeHint; +use crate::gecko_bindings::structs::EffectCompositor_CascadeLevel as CascadeLevel; +use crate::gecko_bindings::structs::ELEMENT_HANDLED_SNAPSHOT; +use crate::gecko_bindings::structs::ELEMENT_HAS_ANIMATION_ONLY_DIRTY_DESCENDANTS_FOR_SERVO; +use crate::gecko_bindings::structs::ELEMENT_HAS_DIRTY_DESCENDANTS_FOR_SERVO; +use crate::gecko_bindings::structs::ELEMENT_HAS_SNAPSHOT; +use crate::gecko_bindings::structs::NODE_DESCENDANTS_NEED_FRAMES; +use crate::gecko_bindings::structs::NODE_NEEDS_FRAME; +use crate::gecko_bindings::structs::{nsAtom, nsIContent, nsINode_BooleanFlag}; +use crate::gecko_bindings::structs::{nsINode as RawGeckoNode, Element as RawGeckoElement}; +use crate::global_style_data::GLOBAL_STYLE_DATA; +use crate::invalidation::element::restyle_hints::RestyleHint; +use crate::media_queries::Device; +use crate::properties::animated_properties::{AnimationValue, AnimationValueMap}; +use crate::properties::{ComputedValues, LonghandId}; +use crate::properties::{Importance, PropertyDeclaration, PropertyDeclarationBlock}; +use crate::rule_tree::CascadeLevel as ServoCascadeLevel; +use crate::selector_parser::{AttrValue, Lang}; +use crate::shared_lock::{Locked, SharedRwLock}; +use crate::string_cache::{Atom, Namespace, WeakAtom, WeakNamespace}; +use crate::stylist::CascadeData; +use crate::values::computed::Display; +use crate::values::{AtomIdent, AtomString}; +use crate::CaseSensitivityExt; +use crate::LocalName; +use app_units::Au; +use atomic_refcell::{AtomicRef, AtomicRefCell, AtomicRefMut}; +use dom::{DocumentState, ElementState}; +use euclid::default::Size2D; +use fxhash::FxHashMap; +use selectors::attr::{AttrSelectorOperation, AttrSelectorOperator}; +use selectors::attr::{CaseSensitivity, NamespaceConstraint}; +use selectors::matching::VisitedHandlingMode; +use selectors::matching::{ElementSelectorFlags, MatchingContext}; +use selectors::sink::Push; +use selectors::{Element, OpaqueElement}; +use servo_arc::{Arc, ArcBorrow}; +use std::fmt; +use std::hash::{Hash, Hasher}; +use std::mem; +use std::ptr; +use std::sync::atomic::{AtomicU32, Ordering}; + +#[inline] +fn elements_with_id<'a, 'le>( + array: *const structs::nsTArray<*mut RawGeckoElement>, +) -> &'a [GeckoElement<'le>] { + unsafe { + if array.is_null() { + return &[]; + } + + let elements: &[*mut RawGeckoElement] = &**array; + + // NOTE(emilio): We rely on the in-memory representation of + // GeckoElement<'ld> and *mut RawGeckoElement being the same. + #[allow(dead_code)] + unsafe fn static_assert() { + mem::transmute::<*mut RawGeckoElement, GeckoElement<'static>>(0xbadc0de as *mut _); + } + + mem::transmute(elements) + } +} + +/// A simple wrapper over `Document`. +#[derive(Clone, Copy)] +pub struct GeckoDocument<'ld>(pub &'ld structs::Document); + +impl<'ld> TDocument for GeckoDocument<'ld> { + type ConcreteNode = GeckoNode<'ld>; + + #[inline] + fn as_node(&self) -> Self::ConcreteNode { + GeckoNode(&self.0._base) + } + + #[inline] + fn is_html_document(&self) -> bool { + self.0.mType == structs::Document_Type::eHTML + } + + #[inline] + fn quirks_mode(&self) -> QuirksMode { + self.0.mCompatMode.into() + } + + #[inline] + fn elements_with_id<'a>(&self, id: &AtomIdent) -> Result<&'a [GeckoElement<'ld>], ()> + where + Self: 'a, + { + Ok(elements_with_id(unsafe { + bindings::Gecko_Document_GetElementsWithId(self.0, id.as_ptr()) + })) + } + + fn shared_lock(&self) -> &SharedRwLock { + &GLOBAL_STYLE_DATA.shared_lock + } +} + +/// A simple wrapper over `ShadowRoot`. +#[derive(Clone, Copy)] +pub struct GeckoShadowRoot<'lr>(pub &'lr structs::ShadowRoot); + +impl<'ln> fmt::Debug for GeckoShadowRoot<'ln> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // TODO(emilio): Maybe print the host or something? + write!(f, " ({:#x})", self.as_node().opaque().0) + } +} + +impl<'lr> PartialEq for GeckoShadowRoot<'lr> { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.0 as *const _ == other.0 as *const _ + } +} + +impl<'lr> TShadowRoot for GeckoShadowRoot<'lr> { + type ConcreteNode = GeckoNode<'lr>; + + #[inline] + fn as_node(&self) -> Self::ConcreteNode { + GeckoNode(&self.0._base._base._base._base) + } + + #[inline] + fn host(&self) -> GeckoElement<'lr> { + GeckoElement(unsafe { &*self.0._base.mHost.mRawPtr }) + } + + #[inline] + fn style_data<'a>(&self) -> Option<&'a CascadeData> + where + Self: 'a, + { + let author_styles = unsafe { self.0.mServoStyles.mPtr.as_ref()? }; + Some(&author_styles.data) + } + + #[inline] + fn elements_with_id<'a>(&self, id: &AtomIdent) -> Result<&'a [GeckoElement<'lr>], ()> + where + Self: 'a, + { + Ok(elements_with_id(unsafe { + bindings::Gecko_ShadowRoot_GetElementsWithId(self.0, id.as_ptr()) + })) + } + + #[inline] + fn parts<'a>(&self) -> &[::ConcreteElement] + where + Self: 'a, + { + let slice: &[*const RawGeckoElement] = &*self.0.mParts; + + #[allow(dead_code)] + unsafe fn static_assert() { + mem::transmute::<*const RawGeckoElement, GeckoElement<'static>>(0xbadc0de as *const _); + } + + unsafe { mem::transmute(slice) } + } +} + +/// A simple wrapper over a non-null Gecko node (`nsINode`) pointer. +/// +/// Important: We don't currently refcount the DOM, because the wrapper lifetime +/// magic guarantees that our LayoutFoo references won't outlive the root, and +/// we don't mutate any of the references on the Gecko side during restyle. +/// +/// We could implement refcounting if need be (at a potentially non-trivial +/// performance cost) by implementing Drop and making LayoutFoo non-Copy. +#[derive(Clone, Copy)] +pub struct GeckoNode<'ln>(pub &'ln RawGeckoNode); + +impl<'ln> PartialEq for GeckoNode<'ln> { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.0 as *const _ == other.0 as *const _ + } +} + +impl<'ln> fmt::Debug for GeckoNode<'ln> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if let Some(el) = self.as_element() { + return el.fmt(f); + } + + if self.is_text_node() { + return write!(f, " ({:#x})", self.opaque().0); + } + + if self.is_document() { + return write!(f, " ({:#x})", self.opaque().0); + } + + if let Some(sr) = self.as_shadow_root() { + return sr.fmt(f); + } + + write!(f, " ({:#x})", self.opaque().0) + } +} + +impl<'ln> GeckoNode<'ln> { + #[inline] + fn is_document(&self) -> bool { + // This is a DOM constant that isn't going to change. + const DOCUMENT_NODE: u16 = 9; + self.node_info().mInner.mNodeType == DOCUMENT_NODE + } + + #[inline] + fn is_shadow_root(&self) -> bool { + self.is_in_shadow_tree() && self.parent_node().is_none() + } + + #[inline] + fn from_content(content: &'ln nsIContent) -> Self { + GeckoNode(&content._base) + } + + #[inline] + fn set_flags(&self, flags: u32) { + self.flags_atomic().fetch_or(flags, Ordering::Relaxed); + } + + #[inline] + fn flags_atomic(&self) -> &AtomicU32 { + use std::cell::Cell; + let flags: &Cell = &(self.0)._base._base_1.mFlags; + + #[allow(dead_code)] + fn static_assert() { + let _: [u8; std::mem::size_of::>()] = [0u8; std::mem::size_of::()]; + let _: [u8; std::mem::align_of::>()] = + [0u8; std::mem::align_of::()]; + } + + // Rust doesn't provide standalone atomic functions like GCC/clang do + // (via the atomic intrinsics) or via std::atomic_ref, but it guarantees + // that the memory representation of u32 and AtomicU32 matches: + // https://doc.rust-lang.org/std/sync/atomic/struct.AtomicU32.html + unsafe { std::mem::transmute::<&Cell, &AtomicU32>(flags) } + } + + #[inline] + fn flags(&self) -> u32 { + self.flags_atomic().load(Ordering::Relaxed) + } + + #[inline] + fn node_info(&self) -> &structs::NodeInfo { + debug_assert!(!self.0.mNodeInfo.mRawPtr.is_null()); + unsafe { &*self.0.mNodeInfo.mRawPtr } + } + + // These live in different locations depending on processor architecture. + #[cfg(target_pointer_width = "64")] + #[inline] + fn bool_flags(&self) -> u32 { + (self.0)._base._base_1.mBoolFlags + } + + #[cfg(target_pointer_width = "32")] + #[inline] + fn bool_flags(&self) -> u32 { + (self.0).mBoolFlags + } + + #[inline] + fn get_bool_flag(&self, flag: nsINode_BooleanFlag) -> bool { + self.bool_flags() & (1u32 << flag as u32) != 0 + } + + /// This logic is duplicate in Gecko's nsINode::IsInShadowTree(). + #[inline] + fn is_in_shadow_tree(&self) -> bool { + use crate::gecko_bindings::structs::NODE_IS_IN_SHADOW_TREE; + self.flags() & NODE_IS_IN_SHADOW_TREE != 0 + } + + /// Returns true if we know for sure that `flattened_tree_parent` and `parent_node` return the + /// same thing. + /// + /// TODO(emilio): Measure and consider not doing this fast-path, it's only a function call and + /// from profiles it seems that keeping this fast path makes the compiler not inline + /// `flattened_tree_parent` as a whole, so we're not gaining much either. + #[inline] + fn flattened_tree_parent_is_parent(&self) -> bool { + use crate::gecko_bindings::structs::*; + let flags = self.flags(); + + let parent = match self.parent_node() { + Some(p) => p, + None => return true, + }; + + if parent.is_shadow_root() { + return false; + } + + if let Some(parent) = parent.as_element() { + if flags & NODE_IS_NATIVE_ANONYMOUS_ROOT != 0 && parent.is_root() { + return false; + } + if parent.shadow_root().is_some() || parent.is_html_slot_element() { + return false; + } + } + + true + } + + #[inline] + fn flattened_tree_parent(&self) -> Option { + if self.flattened_tree_parent_is_parent() { + debug_assert_eq!( + unsafe { + bindings::Gecko_GetFlattenedTreeParentNode(self.0) + .as_ref() + .map(GeckoNode) + }, + self.parent_node(), + "Fast path stopped holding!" + ); + return self.parent_node(); + } + + // NOTE(emilio): If this call is too expensive, we could manually inline more aggressively. + unsafe { + bindings::Gecko_GetFlattenedTreeParentNode(self.0) + .as_ref() + .map(GeckoNode) + } + } + + #[inline] + fn contains_non_whitespace_content(&self) -> bool { + unsafe { Gecko_IsSignificantChild(self.0, false) } + } +} + +impl<'ln> NodeInfo for GeckoNode<'ln> { + #[inline] + fn is_element(&self) -> bool { + self.get_bool_flag(nsINode_BooleanFlag::NodeIsElement) + } + + fn is_text_node(&self) -> bool { + // This is a DOM constant that isn't going to change. + const TEXT_NODE: u16 = 3; + self.node_info().mInner.mNodeType == TEXT_NODE + } +} + +impl<'ln> TNode for GeckoNode<'ln> { + type ConcreteDocument = GeckoDocument<'ln>; + type ConcreteShadowRoot = GeckoShadowRoot<'ln>; + type ConcreteElement = GeckoElement<'ln>; + + #[inline] + fn parent_node(&self) -> Option { + unsafe { self.0.mParent.as_ref().map(GeckoNode) } + } + + #[inline] + fn first_child(&self) -> Option { + unsafe { + self.0 + .mFirstChild + .raw() + .as_ref() + .map(GeckoNode::from_content) + } + } + + #[inline] + fn last_child(&self) -> Option { + unsafe { bindings::Gecko_GetLastChild(self.0).as_ref().map(GeckoNode) } + } + + #[inline] + fn prev_sibling(&self) -> Option { + unsafe { + let prev_or_last = GeckoNode::from_content(self.0.mPreviousOrLastSibling.as_ref()?); + if prev_or_last.0.mNextSibling.raw().is_null() { + return None; + } + Some(prev_or_last) + } + } + + #[inline] + fn next_sibling(&self) -> Option { + unsafe { + self.0 + .mNextSibling + .raw() + .as_ref() + .map(GeckoNode::from_content) + } + } + + #[inline] + fn owner_doc(&self) -> Self::ConcreteDocument { + debug_assert!(!self.node_info().mDocument.is_null()); + GeckoDocument(unsafe { &*self.node_info().mDocument }) + } + + #[inline] + fn is_in_document(&self) -> bool { + self.get_bool_flag(nsINode_BooleanFlag::IsInDocument) + } + + fn traversal_parent(&self) -> Option> { + self.flattened_tree_parent().and_then(|n| n.as_element()) + } + + #[inline] + fn opaque(&self) -> OpaqueNode { + let ptr: usize = self.0 as *const _ as usize; + OpaqueNode(ptr) + } + + fn debug_id(self) -> usize { + unimplemented!() + } + + #[inline] + fn as_element(&self) -> Option> { + if !self.is_element() { + return None; + } + + Some(GeckoElement(unsafe { + &*(self.0 as *const _ as *const RawGeckoElement) + })) + } + + #[inline] + fn as_document(&self) -> Option { + if !self.is_document() { + return None; + } + + debug_assert_eq!(self.owner_doc().as_node(), *self, "How?"); + Some(self.owner_doc()) + } + + #[inline] + fn as_shadow_root(&self) -> Option { + if !self.is_shadow_root() { + return None; + } + + Some(GeckoShadowRoot(unsafe { + &*(self.0 as *const _ as *const structs::ShadowRoot) + })) + } +} + +/// A wrapper on top of two kind of iterators, depending on the parent being +/// iterated. +/// +/// We generally iterate children by traversing the light-tree siblings of the +/// first child like Servo does. +/// +/// However, for nodes with anonymous children, we use a custom (heavier-weight) +/// Gecko-implemented iterator. +/// +/// FIXME(emilio): If we take into account shadow DOM, we're going to need the +/// flat tree pretty much always. We can try to optimize the case where there's +/// no shadow root sibling, probably. +pub enum GeckoChildrenIterator<'a> { + /// A simple iterator that tracks the current node being iterated and + /// replaces it with the next sibling when requested. + Current(Option>), + /// A Gecko-implemented iterator we need to drop appropriately. + GeckoIterator(structs::StyleChildrenIterator), +} + +impl<'a> Drop for GeckoChildrenIterator<'a> { + fn drop(&mut self) { + if let GeckoChildrenIterator::GeckoIterator(ref mut it) = *self { + unsafe { + bindings::Gecko_DestroyStyleChildrenIterator(it); + } + } + } +} + +impl<'a> Iterator for GeckoChildrenIterator<'a> { + type Item = GeckoNode<'a>; + fn next(&mut self) -> Option> { + match *self { + GeckoChildrenIterator::Current(curr) => { + let next = curr.and_then(|node| node.next_sibling()); + *self = GeckoChildrenIterator::Current(next); + curr + }, + GeckoChildrenIterator::GeckoIterator(ref mut it) => unsafe { + // We do this unsafe lengthening of the lifetime here because + // structs::StyleChildrenIterator is actually StyleChildrenIterator<'a>, + // however we can't express this easily with bindgen, and it would + // introduce functions with two input lifetimes into bindgen, + // which would be out of scope for elision. + bindings::Gecko_GetNextStyleChild(&mut *(it as *mut _)) + .as_ref() + .map(GeckoNode) + }, + } + } +} + +/// A simple wrapper over a non-null Gecko `Element` pointer. +#[derive(Clone, Copy)] +pub struct GeckoElement<'le>(pub &'le RawGeckoElement); + +impl<'le> fmt::Debug for GeckoElement<'le> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use nsstring::nsCString; + + write!(f, "<{}", self.local_name())?; + + let mut attrs = nsCString::new(); + unsafe { + bindings::Gecko_Element_DebugListAttributes(self.0, &mut attrs); + } + write!(f, "{}", attrs)?; + write!(f, "> ({:#x})", self.as_node().opaque().0) + } +} + +impl<'le> GeckoElement<'le> { + /// Gets the raw `ElementData` refcell for the element. + #[inline(always)] + pub fn get_data(&self) -> Option<&AtomicRefCell> { + unsafe { self.0.mServoData.get().as_ref() } + } + + /// Returns whether any animation applies to this element. + #[inline] + pub fn has_any_animation(&self) -> bool { + self.may_have_animations() && unsafe { Gecko_ElementHasAnimations(self.0) } + } + + #[inline(always)] + fn non_mapped_attrs(&self) -> &[structs::AttrArray_InternalAttr] { + unsafe { + let attrs = match self.0.mAttrs.mImpl.mPtr.as_ref() { + Some(attrs) => attrs, + None => return &[], + }; + + attrs.mBuffer.as_slice(attrs.mAttrCount as usize) + } + } + + #[inline(always)] + fn mapped_attrs(&self) -> &[structs::AttrArray_InternalAttr] { + unsafe { + let attrs = match self.0.mAttrs.mImpl.mPtr.as_ref() { + Some(attrs) => attrs, + None => return &[], + }; + + let attrs = match attrs.mMappedAttrs.as_ref() { + Some(attrs) => attrs, + None => return &[], + }; + + attrs.mBuffer.as_slice(attrs.mAttrCount as usize) + } + } + + #[inline(always)] + fn get_part_attr(&self) -> Option<&structs::nsAttrValue> { + if !self.has_part_attr() { + return None; + } + snapshot_helpers::find_attr(self.non_mapped_attrs(), &atom!("part")) + } + + #[inline(always)] + fn get_class_attr(&self) -> Option<&structs::nsAttrValue> { + if !self.may_have_class() { + return None; + } + + if self.is_svg_element() { + let svg_class = unsafe { bindings::Gecko_GetSVGAnimatedClass(self.0).as_ref() }; + if let Some(c) = svg_class { + return Some(c); + } + } + + snapshot_helpers::find_attr(self.non_mapped_attrs(), &atom!("class")) + } + + #[inline] + fn may_have_anonymous_children(&self) -> bool { + self.as_node() + .get_bool_flag(nsINode_BooleanFlag::ElementMayHaveAnonymousChildren) + } + + #[inline] + fn flags(&self) -> u32 { + self.as_node().flags() + } + + #[inline] + fn set_flags(&self, flags: u32) { + self.as_node().set_flags(flags); + } + + #[inline] + unsafe fn unset_flags(&self, flags: u32) { + self.as_node() + .flags_atomic() + .fetch_and(!flags, Ordering::Relaxed); + } + + /// Returns true if this element has descendants for lazy frame construction. + #[inline] + pub fn descendants_need_frames(&self) -> bool { + self.flags() & NODE_DESCENDANTS_NEED_FRAMES != 0 + } + + /// Returns true if this element needs lazy frame construction. + #[inline] + pub fn needs_frame(&self) -> bool { + self.flags() & NODE_NEEDS_FRAME != 0 + } + + /// Returns a reference to the DOM slots for this Element, if they exist. + #[inline] + fn dom_slots(&self) -> Option<&structs::FragmentOrElement_nsDOMSlots> { + let slots = self.as_node().0.mSlots as *const structs::FragmentOrElement_nsDOMSlots; + unsafe { slots.as_ref() } + } + + /// Returns a reference to the extended DOM slots for this Element. + #[inline] + fn extended_slots(&self) -> Option<&structs::FragmentOrElement_nsExtendedDOMSlots> { + self.dom_slots().and_then(|s| unsafe { + // For the bit usage, see nsContentSlots::GetExtendedSlots. + let e_slots = s._base.mExtendedSlots & + !structs::nsIContent_nsContentSlots_sNonOwningExtendedSlotsFlag; + (e_slots as *const structs::FragmentOrElement_nsExtendedDOMSlots).as_ref() + }) + } + + #[inline] + fn namespace_id(&self) -> i32 { + self.as_node().node_info().mInner.mNamespaceID + } + + #[inline] + fn has_id(&self) -> bool { + self.as_node() + .get_bool_flag(nsINode_BooleanFlag::ElementHasID) + } + + #[inline] + fn state_internal(&self) -> u64 { + if !self + .as_node() + .get_bool_flag(nsINode_BooleanFlag::ElementHasLockedStyleStates) + { + return self.0.mState.bits; + } + unsafe { Gecko_ElementState(self.0) } + } + + #[inline] + fn document_state(&self) -> DocumentState { + DocumentState::from_bits_retain(self.as_node().owner_doc().0.mDocumentState.bits) + } + + #[inline] + fn may_have_class(&self) -> bool { + self.as_node() + .get_bool_flag(nsINode_BooleanFlag::ElementMayHaveClass) + } + + #[inline] + fn has_properties(&self) -> bool { + use crate::gecko_bindings::structs::NODE_HAS_PROPERTIES; + + self.flags() & NODE_HAS_PROPERTIES != 0 + } + + #[inline] + fn before_or_after_pseudo(&self, is_before: bool) -> Option { + if !self.has_properties() { + return None; + } + + unsafe { + bindings::Gecko_GetBeforeOrAfterPseudo(self.0, is_before) + .as_ref() + .map(GeckoElement) + } + } + + #[inline] + fn may_have_style_attribute(&self) -> bool { + self.as_node() + .get_bool_flag(nsINode_BooleanFlag::ElementMayHaveStyle) + } + + /// Only safe to call on the main thread, with exclusive access to the + /// element and its ancestors. + /// + /// This function is also called after display property changed for SMIL + /// animation. + /// + /// Also this function schedules style flush. + pub unsafe fn note_explicit_hints(&self, restyle_hint: RestyleHint, change_hint: nsChangeHint) { + use crate::gecko::restyle_damage::GeckoRestyleDamage; + + let damage = GeckoRestyleDamage::new(change_hint); + debug!( + "note_explicit_hints: {:?}, restyle_hint={:?}, change_hint={:?}", + self, restyle_hint, change_hint + ); + + debug_assert!( + !(restyle_hint.has_animation_hint() && restyle_hint.has_non_animation_hint()), + "Animation restyle hints should not appear with non-animation restyle hints" + ); + + let mut data = match self.mutate_data() { + Some(d) => d, + None => { + debug!("(Element not styled, discarding hints)"); + return; + }, + }; + + debug_assert!(data.has_styles(), "how?"); + + // Propagate the bit up the chain. + if restyle_hint.has_animation_hint() { + bindings::Gecko_NoteAnimationOnlyDirtyElement(self.0); + } else { + bindings::Gecko_NoteDirtyElement(self.0); + } + + data.hint.insert(restyle_hint); + data.damage |= damage; + } + + /// This logic is duplicated in Gecko's nsIContent::IsRootOfNativeAnonymousSubtree. + #[inline] + fn is_root_of_native_anonymous_subtree(&self) -> bool { + use crate::gecko_bindings::structs::NODE_IS_NATIVE_ANONYMOUS_ROOT; + return self.flags() & NODE_IS_NATIVE_ANONYMOUS_ROOT != 0; + } + + /// Returns true if this node is the shadow root of an use-element shadow tree. + #[inline] + fn is_root_of_use_element_shadow_tree(&self) -> bool { + if !self.as_node().is_in_shadow_tree() { + return false; + } + if !self.parent_node_is_shadow_root() { + return false; + } + let host = self.containing_shadow_host().unwrap(); + host.is_svg_element() && host.local_name() == &**local_name!("use") + } + + fn css_transitions_info(&self) -> FxHashMap> { + use crate::gecko_bindings::bindings::Gecko_ElementTransitions_EndValueAt; + use crate::gecko_bindings::bindings::Gecko_ElementTransitions_Length; + + let collection_length = unsafe { Gecko_ElementTransitions_Length(self.0) } as usize; + let mut map = FxHashMap::with_capacity_and_hasher(collection_length, Default::default()); + + for i in 0..collection_length { + let end_value = + unsafe { Arc::from_raw_addrefed(Gecko_ElementTransitions_EndValueAt(self.0, i)) }; + let property = end_value.id(); + debug_assert!(!property.is_logical()); + map.insert(property, end_value); + } + map + } + + fn needs_transitions_update_per_property( + &self, + longhand_id: LonghandId, + combined_duration_seconds: f32, + before_change_style: &ComputedValues, + after_change_style: &ComputedValues, + existing_transitions: &FxHashMap>, + ) -> bool { + use crate::values::animated::{Animate, Procedure}; + debug_assert!(!longhand_id.is_logical()); + + // If there is an existing transition, update only if the end value + // differs. + // + // If the end value has not changed, we should leave the currently + // running transition as-is since we don't want to interrupt its timing + // function. + if let Some(ref existing) = existing_transitions.get(&longhand_id) { + let after_value = + AnimationValue::from_computed_values(longhand_id, after_change_style).unwrap(); + + return ***existing != after_value; + } + + let from = AnimationValue::from_computed_values(longhand_id, before_change_style); + let to = AnimationValue::from_computed_values(longhand_id, after_change_style); + + debug_assert_eq!(to.is_some(), from.is_some()); + + combined_duration_seconds > 0.0f32 && + from != to && + from.unwrap() + .animate( + to.as_ref().unwrap(), + Procedure::Interpolate { progress: 0.5 }, + ) + .is_ok() + } +} + +/// Converts flags from the layout used by rust-selectors to the layout used +/// by Gecko. We could align these and then do this without conditionals, but +/// it's probably not worth the trouble. +fn selector_flags_to_node_flags(flags: ElementSelectorFlags) -> u32 { + use crate::gecko_bindings::structs::*; + let mut gecko_flags = 0u32; + if flags.contains(ElementSelectorFlags::HAS_SLOW_SELECTOR) { + gecko_flags |= NODE_HAS_SLOW_SELECTOR; + } + if flags.contains(ElementSelectorFlags::HAS_SLOW_SELECTOR_LATER_SIBLINGS) { + gecko_flags |= NODE_HAS_SLOW_SELECTOR_LATER_SIBLINGS; + } + if flags.contains(ElementSelectorFlags::HAS_SLOW_SELECTOR_NTH_OF) { + gecko_flags |= NODE_HAS_SLOW_SELECTOR_NTH_OF; + } + if flags.contains(ElementSelectorFlags::HAS_EDGE_CHILD_SELECTOR) { + gecko_flags |= NODE_HAS_EDGE_CHILD_SELECTOR; + } + if flags.contains(ElementSelectorFlags::HAS_EMPTY_SELECTOR) { + gecko_flags |= NODE_HAS_EMPTY_SELECTOR; + } + if flags.contains(ElementSelectorFlags::ANCHORS_RELATIVE_SELECTOR) { + gecko_flags |= NODE_ANCHORS_RELATIVE_SELECTOR; + } + + gecko_flags +} + +fn get_animation_rule( + element: &GeckoElement, + cascade_level: CascadeLevel, +) -> Option>> { + use crate::properties::longhands::ANIMATABLE_PROPERTY_COUNT; + + // There's a very rough correlation between the number of effects + // (animations) on an element and the number of properties it is likely to + // animate, so we use that as an initial guess for the size of the + // AnimationValueMap in order to reduce the number of re-allocations needed. + let effect_count = unsafe { Gecko_GetAnimationEffectCount(element.0) }; + // Also, we should try to reuse the PDB, to avoid creating extra rule nodes. + let mut animation_values = AnimationValueMap::with_capacity_and_hasher( + effect_count.min(ANIMATABLE_PROPERTY_COUNT), + Default::default(), + ); + if unsafe { Gecko_GetAnimationRule(element.0, cascade_level, &mut animation_values) } { + let shared_lock = &GLOBAL_STYLE_DATA.shared_lock; + Some(Arc::new(shared_lock.wrap( + PropertyDeclarationBlock::from_animation_value_map(&animation_values), + ))) + } else { + None + } +} + +impl<'le> TElement for GeckoElement<'le> { + type ConcreteNode = GeckoNode<'le>; + type TraversalChildrenIterator = GeckoChildrenIterator<'le>; + + fn inheritance_parent(&self) -> Option { + if self.is_pseudo_element() { + return self.pseudo_element_originating_element(); + } + + self.as_node() + .flattened_tree_parent() + .and_then(|n| n.as_element()) + } + + fn traversal_children(&self) -> LayoutIterator> { + // This condition is similar to the check that + // StyleChildrenIterator::IsNeeded does, except that it might return + // true if we used to (but no longer) have anonymous content from + // ::before/::after, or nsIAnonymousContentCreators. + if self.is_html_slot_element() || + self.shadow_root().is_some() || + self.may_have_anonymous_children() + { + unsafe { + let mut iter: structs::StyleChildrenIterator = ::std::mem::zeroed(); + bindings::Gecko_ConstructStyleChildrenIterator(self.0, &mut iter); + return LayoutIterator(GeckoChildrenIterator::GeckoIterator(iter)); + } + } + + LayoutIterator(GeckoChildrenIterator::Current(self.as_node().first_child())) + } + + fn before_pseudo_element(&self) -> Option { + self.before_or_after_pseudo(/* is_before = */ true) + } + + fn after_pseudo_element(&self) -> Option { + self.before_or_after_pseudo(/* is_before = */ false) + } + + fn marker_pseudo_element(&self) -> Option { + if !self.has_properties() { + return None; + } + + unsafe { + bindings::Gecko_GetMarkerPseudo(self.0) + .as_ref() + .map(GeckoElement) + } + } + + #[inline] + fn is_html_element(&self) -> bool { + self.namespace_id() == structs::kNameSpaceID_XHTML as i32 + } + + #[inline] + fn is_mathml_element(&self) -> bool { + self.namespace_id() == structs::kNameSpaceID_MathML as i32 + } + + #[inline] + fn is_svg_element(&self) -> bool { + self.namespace_id() == structs::kNameSpaceID_SVG as i32 + } + + #[inline] + fn is_xul_element(&self) -> bool { + self.namespace_id() == structs::root::kNameSpaceID_XUL as i32 + } + + #[inline] + fn local_name(&self) -> &WeakAtom { + unsafe { WeakAtom::new(self.as_node().node_info().mInner.mName) } + } + + #[inline] + fn namespace(&self) -> &WeakNamespace { + unsafe { + let namespace_manager = structs::nsNameSpaceManager_sInstance.mRawPtr; + WeakNamespace::new((*namespace_manager).mURIArray[self.namespace_id() as usize].mRawPtr) + } + } + + #[inline] + fn query_container_size(&self, display: &Display) -> Size2D> { + // If an element gets 'display: contents' and its nsIFrame has not been removed yet, + // Gecko_GetQueryContainerSize will not notice that it can't have size containment. + // Other cases like 'display: inline' will be handled once the new nsIFrame is created. + if display.is_contents() { + return Size2D::new(None, None); + } + + let mut width = -1; + let mut height = -1; + unsafe { + bindings::Gecko_GetQueryContainerSize(self.0, &mut width, &mut height); + } + Size2D::new( + if width >= 0 { Some(Au(width)) } else { None }, + if height >= 0 { Some(Au(height)) } else { None }, + ) + } + + /// Return the list of slotted nodes of this node. + #[inline] + fn slotted_nodes(&self) -> &[Self::ConcreteNode] { + if !self.is_html_slot_element() || !self.as_node().is_in_shadow_tree() { + return &[]; + } + + let slot: &structs::HTMLSlotElement = unsafe { mem::transmute(self.0) }; + + if cfg!(debug_assertions) { + let base: &RawGeckoElement = &slot._base._base._base._base; + assert_eq!(base as *const _, self.0 as *const _, "Bad cast"); + } + + // FIXME(emilio): Workaround a bindgen bug on Android that causes + // mAssignedNodes to be at the wrong offset. See bug 1466406. + // + // Bug 1466580 tracks running the Android layout tests on automation. + // + // The actual bindgen bug still needs reduction. + let assigned_nodes: &[structs::RefPtr] = if !cfg!(target_os = "android") { + debug_assert_eq!( + unsafe { bindings::Gecko_GetAssignedNodes(self.0) }, + &slot.mAssignedNodes as *const _, + ); + + &*slot.mAssignedNodes + } else { + unsafe { &**bindings::Gecko_GetAssignedNodes(self.0) } + }; + + debug_assert_eq!( + mem::size_of::>(), + mem::size_of::(), + "Bad cast!" + ); + + unsafe { mem::transmute(assigned_nodes) } + } + + #[inline] + fn shadow_root(&self) -> Option> { + let slots = self.extended_slots()?; + unsafe { slots.mShadowRoot.mRawPtr.as_ref().map(GeckoShadowRoot) } + } + + #[inline] + fn containing_shadow(&self) -> Option> { + let slots = self.extended_slots()?; + unsafe { + slots + ._base + .mContainingShadow + .mRawPtr + .as_ref() + .map(GeckoShadowRoot) + } + } + + fn each_anonymous_content_child(&self, mut f: F) + where + F: FnMut(Self), + { + if !self.may_have_anonymous_children() { + return; + } + + let array: *mut structs::nsTArray<*mut nsIContent> = + unsafe { bindings::Gecko_GetAnonymousContentForElement(self.0) }; + + if array.is_null() { + return; + } + + for content in unsafe { &**array } { + let node = GeckoNode::from_content(unsafe { &**content }); + let element = match node.as_element() { + Some(e) => e, + None => continue, + }; + + f(element); + } + + unsafe { bindings::Gecko_DestroyAnonymousContentList(array) }; + } + + #[inline] + fn as_node(&self) -> Self::ConcreteNode { + unsafe { GeckoNode(&*(self.0 as *const _ as *const RawGeckoNode)) } + } + + fn owner_doc_matches_for_testing(&self, device: &Device) -> bool { + self.as_node().owner_doc().0 as *const structs::Document == device.document() as *const _ + } + + fn style_attribute(&self) -> Option>> { + if !self.may_have_style_attribute() { + return None; + } + + unsafe { + let declarations = Gecko_GetStyleAttrDeclarationBlock(self.0).as_ref()?; + Some(ArcBorrow::from_ref(declarations)) + } + } + + fn unset_dirty_style_attribute(&self) { + if !self.may_have_style_attribute() { + return; + } + + unsafe { Gecko_UnsetDirtyStyleAttr(self.0) }; + } + + fn smil_override(&self) -> Option>> { + unsafe { + let slots = self.extended_slots()?; + + let declaration: &structs::DeclarationBlock = + slots.mSMILOverrideStyleDeclaration.mRawPtr.as_ref()?; + + let raw: &structs::StyleLockedDeclarationBlock = declaration.mRaw.mRawPtr.as_ref()?; + Some(ArcBorrow::from_ref(raw)) + } + } + + fn animation_rule( + &self, + _: &SharedStyleContext, + ) -> Option>> { + get_animation_rule(self, CascadeLevel::Animations) + } + + fn transition_rule( + &self, + _: &SharedStyleContext, + ) -> Option>> { + get_animation_rule(self, CascadeLevel::Transitions) + } + + #[inline] + fn state(&self) -> ElementState { + ElementState::from_bits_retain(self.state_internal()) + } + + #[inline] + fn has_attr(&self, namespace: &Namespace, attr: &AtomIdent) -> bool { + unsafe { bindings::Gecko_HasAttr(self.0, namespace.0.as_ptr(), attr.as_ptr()) } + } + + #[inline] + fn has_part_attr(&self) -> bool { + self.as_node() + .get_bool_flag(nsINode_BooleanFlag::ElementHasPart) + } + + #[inline] + fn exports_any_part(&self) -> bool { + snapshot_helpers::find_attr(self.non_mapped_attrs(), &atom!("exportparts")).is_some() + } + + // FIXME(emilio): we should probably just return a reference to the Atom. + #[inline] + fn id(&self) -> Option<&WeakAtom> { + if !self.has_id() { + return None; + } + + snapshot_helpers::get_id(self.non_mapped_attrs()) + } + + fn each_attr_name(&self, mut callback: F) + where + F: FnMut(&AtomIdent), + { + for attr in self + .non_mapped_attrs() + .iter() + .chain(self.mapped_attrs().iter()) + { + let is_nodeinfo = attr.mName.mBits & 1 != 0; + unsafe { + let atom = if is_nodeinfo { + let node_info = &*((attr.mName.mBits & !1) as *const structs::NodeInfo); + node_info.mInner.mName + } else { + attr.mName.mBits as *const nsAtom + }; + AtomIdent::with(atom, |a| callback(a)) + } + } + } + + fn each_class(&self, callback: F) + where + F: FnMut(&AtomIdent), + { + let attr = match self.get_class_attr() { + Some(c) => c, + None => return, + }; + + snapshot_helpers::each_class_or_part(attr, callback) + } + + #[inline] + fn each_exported_part(&self, name: &AtomIdent, callback: F) + where + F: FnMut(&AtomIdent), + { + snapshot_helpers::each_exported_part(self.non_mapped_attrs(), name, callback) + } + + fn each_part(&self, callback: F) + where + F: FnMut(&AtomIdent), + { + let attr = match self.get_part_attr() { + Some(c) => c, + None => return, + }; + + snapshot_helpers::each_class_or_part(attr, callback) + } + + #[inline] + fn has_snapshot(&self) -> bool { + self.flags() & ELEMENT_HAS_SNAPSHOT != 0 + } + + #[inline] + fn handled_snapshot(&self) -> bool { + self.flags() & ELEMENT_HANDLED_SNAPSHOT != 0 + } + + unsafe fn set_handled_snapshot(&self) { + debug_assert!(self.has_data()); + self.set_flags(ELEMENT_HANDLED_SNAPSHOT) + } + + #[inline] + fn has_dirty_descendants(&self) -> bool { + self.flags() & ELEMENT_HAS_DIRTY_DESCENDANTS_FOR_SERVO != 0 + } + + unsafe fn set_dirty_descendants(&self) { + debug_assert!(self.has_data()); + debug!("Setting dirty descendants: {:?}", self); + self.set_flags(ELEMENT_HAS_DIRTY_DESCENDANTS_FOR_SERVO) + } + + unsafe fn unset_dirty_descendants(&self) { + self.unset_flags(ELEMENT_HAS_DIRTY_DESCENDANTS_FOR_SERVO) + } + + #[inline] + fn has_animation_only_dirty_descendants(&self) -> bool { + self.flags() & ELEMENT_HAS_ANIMATION_ONLY_DIRTY_DESCENDANTS_FOR_SERVO != 0 + } + + unsafe fn set_animation_only_dirty_descendants(&self) { + self.set_flags(ELEMENT_HAS_ANIMATION_ONLY_DIRTY_DESCENDANTS_FOR_SERVO) + } + + unsafe fn unset_animation_only_dirty_descendants(&self) { + self.unset_flags(ELEMENT_HAS_ANIMATION_ONLY_DIRTY_DESCENDANTS_FOR_SERVO) + } + + unsafe fn clear_descendant_bits(&self) { + self.unset_flags( + ELEMENT_HAS_DIRTY_DESCENDANTS_FOR_SERVO | + ELEMENT_HAS_ANIMATION_ONLY_DIRTY_DESCENDANTS_FOR_SERVO | + NODE_DESCENDANTS_NEED_FRAMES, + ) + } + + fn is_visited_link(&self) -> bool { + self.state().intersects(ElementState::VISITED) + } + + /// We want to match rules from the same tree in all cases, except for native anonymous content + /// that _isn't_ part directly of a UA widget (e.g., such generated by form controls, or + /// pseudo-elements). + #[inline] + fn matches_user_and_content_rules(&self) -> bool { + use crate::gecko_bindings::structs::{ + NODE_HAS_BEEN_IN_UA_WIDGET, NODE_IS_IN_NATIVE_ANONYMOUS_SUBTREE, + }; + let flags = self.flags(); + (flags & NODE_IS_IN_NATIVE_ANONYMOUS_SUBTREE) == 0 || + (flags & NODE_HAS_BEEN_IN_UA_WIDGET) != 0 + } + + #[inline] + fn implemented_pseudo_element(&self) -> Option { + if self.matches_user_and_content_rules() { + return None; + } + + if !self.has_properties() { + return None; + } + + PseudoElement::from_pseudo_type(unsafe { bindings::Gecko_GetImplementedPseudo(self.0) }) + } + + #[inline] + fn store_children_to_process(&self, _: isize) { + // This is only used for bottom-up traversal, and is thus a no-op for Gecko. + } + + fn did_process_child(&self) -> isize { + panic!("Atomic child count not implemented in Gecko"); + } + + unsafe fn ensure_data(&self) -> AtomicRefMut { + if !self.has_data() { + debug!("Creating ElementData for {:?}", self); + let ptr = Box::into_raw(Box::new(AtomicRefCell::new(ElementData::default()))); + self.0.mServoData.set(ptr); + } + self.mutate_data().unwrap() + } + + unsafe fn clear_data(&self) { + let ptr = self.0.mServoData.get(); + self.unset_flags( + ELEMENT_HAS_SNAPSHOT | + ELEMENT_HANDLED_SNAPSHOT | + structs::Element_kAllServoDescendantBits | + NODE_NEEDS_FRAME, + ); + if !ptr.is_null() { + debug!("Dropping ElementData for {:?}", self); + let data = Box::from_raw(self.0.mServoData.get()); + self.0.mServoData.set(ptr::null_mut()); + + // Perform a mutable borrow of the data in debug builds. This + // serves as an assertion that there are no outstanding borrows + // when we destroy the data. + debug_assert!({ + let _ = data.borrow_mut(); + true + }); + } + } + + #[inline] + fn skip_item_display_fixup(&self) -> bool { + debug_assert!( + !self.is_pseudo_element(), + "Just don't call me if I'm a pseudo, you should know the answer already" + ); + self.is_root_of_native_anonymous_subtree() + } + + #[inline] + fn may_have_animations(&self) -> bool { + if let Some(pseudo) = self.implemented_pseudo_element() { + if pseudo.animations_stored_in_parent() { + // FIXME(emilio): When would the parent of a ::before / ::after + // pseudo-element be null? + return self.parent_element().map_or(false, |p| { + p.as_node() + .get_bool_flag(nsINode_BooleanFlag::ElementHasAnimations) + }); + } + } + self.as_node() + .get_bool_flag(nsINode_BooleanFlag::ElementHasAnimations) + } + + /// Process various tasks that are a result of animation-only restyle. + fn process_post_animation(&self, tasks: PostAnimationTasks) { + debug_assert!(!tasks.is_empty(), "Should be involved a task"); + + // If display style was changed from none to other, we need to resolve + // the descendants in the display:none subtree. Instead of resolving + // those styles in animation-only restyle, we defer it to a subsequent + // normal restyle. + if tasks.intersects(PostAnimationTasks::DISPLAY_CHANGED_FROM_NONE_FOR_SMIL) { + debug_assert!( + self.implemented_pseudo_element() + .map_or(true, |p| !p.is_before_or_after()), + "display property animation shouldn't run on pseudo elements \ + since it's only for SMIL" + ); + unsafe { + self.note_explicit_hints( + RestyleHint::restyle_subtree(), + nsChangeHint::nsChangeHint_Empty, + ); + } + } + } + + /// Update various animation-related state on a given (pseudo-)element as + /// results of normal restyle. + fn update_animations( + &self, + before_change_style: Option>, + tasks: UpdateAnimationsTasks, + ) { + // We have to update animations even if the element has no computed + // style since it means the element is in a display:none subtree, we + // should destroy all CSS animations in display:none subtree. + let computed_data = self.borrow_data(); + let computed_values = computed_data.as_ref().map(|d| d.styles.primary()); + let before_change_values = before_change_style + .as_ref() + .map_or(ptr::null(), |x| x.as_gecko_computed_style()); + let computed_values_opt = computed_values + .as_ref() + .map_or(ptr::null(), |x| x.as_gecko_computed_style()); + unsafe { + Gecko_UpdateAnimations( + self.0, + before_change_values, + computed_values_opt, + tasks.bits(), + ); + } + } + + #[inline] + fn has_animations(&self, _: &SharedStyleContext) -> bool { + self.has_any_animation() + } + + fn has_css_animations(&self, _: &SharedStyleContext, _: Option) -> bool { + self.may_have_animations() && unsafe { Gecko_ElementHasCSSAnimations(self.0) } + } + + fn has_css_transitions(&self, _: &SharedStyleContext, _: Option) -> bool { + self.may_have_animations() && unsafe { Gecko_ElementHasCSSTransitions(self.0) } + } + + // Detect if there are any changes that require us to update transitions. + // + // This is used as a more thoroughgoing check than the cheaper + // might_need_transitions_update check. + // + // The following logic shadows the logic used on the Gecko side + // (nsTransitionManager::DoUpdateTransitions) where we actually perform the + // update. + // + // https://drafts.csswg.org/css-transitions/#starting + fn needs_transitions_update( + &self, + before_change_style: &ComputedValues, + after_change_style: &ComputedValues, + ) -> bool { + use crate::properties::LonghandIdSet; + + let after_change_ui_style = after_change_style.get_ui(); + let existing_transitions = self.css_transitions_info(); + + if after_change_style.get_box().clone_display().is_none() { + // We need to cancel existing transitions. + return !existing_transitions.is_empty(); + } + + let mut transitions_to_keep = LonghandIdSet::new(); + for transition_property in after_change_style.transition_properties() { + let physical_longhand = transition_property + .longhand_id + .to_physical(after_change_style.writing_mode); + transitions_to_keep.insert(physical_longhand); + if self.needs_transitions_update_per_property( + physical_longhand, + after_change_ui_style + .transition_combined_duration_at(transition_property.index) + .seconds(), + before_change_style, + after_change_style, + &existing_transitions, + ) { + return true; + } + } + + // Check if we have to cancel the running transition because this is not + // a matching transition-property value. + existing_transitions + .keys() + .any(|property| !transitions_to_keep.contains(*property)) + } + + /// Whether there is an ElementData container. + #[inline] + fn has_data(&self) -> bool { + self.get_data().is_some() + } + + /// Immutably borrows the ElementData. + fn borrow_data(&self) -> Option> { + self.get_data().map(|x| x.borrow()) + } + + /// Mutably borrows the ElementData. + fn mutate_data(&self) -> Option> { + self.get_data().map(|x| x.borrow_mut()) + } + + #[inline] + fn lang_attr(&self) -> Option { + let ptr = unsafe { bindings::Gecko_LangValue(self.0) }; + if ptr.is_null() { + None + } else { + Some(AtomString(unsafe { Atom::from_addrefed(ptr) })) + } + } + + fn match_element_lang(&self, override_lang: Option>, value: &Lang) -> bool { + // Gecko supports :lang() from CSS Selectors 4, which accepts a list + // of language tags, and does BCP47-style range matching. + let override_lang_ptr = match override_lang { + Some(Some(ref atom)) => atom.as_ptr(), + _ => ptr::null_mut(), + }; + value.0.iter().any(|lang| unsafe { + Gecko_MatchLang( + self.0, + override_lang_ptr, + override_lang.is_some(), + lang.as_slice().as_ptr(), + ) + }) + } + + fn is_html_document_body_element(&self) -> bool { + if self.local_name() != &**local_name!("body") { + return false; + } + + if !self.is_html_element() { + return false; + } + + unsafe { bindings::Gecko_IsDocumentBody(self.0) } + } + + fn synthesize_presentational_hints_for_legacy_attributes( + &self, + visited_handling: VisitedHandlingMode, + hints: &mut V, + ) where + V: Push, + { + use crate::properties::longhands::_x_lang::SpecifiedValue as SpecifiedLang; + use crate::properties::longhands::color::SpecifiedValue as SpecifiedColor; + use crate::stylesheets::layer_rule::LayerOrder; + use crate::values::specified::{color::Color, font::XTextScale}; + lazy_static! { + static ref TABLE_COLOR_RULE: ApplicableDeclarationBlock = { + let global_style_data = &*GLOBAL_STYLE_DATA; + let pdb = PropertyDeclarationBlock::with_one( + PropertyDeclaration::Color(SpecifiedColor(Color::InheritFromBodyQuirk.into())), + Importance::Normal, + ); + let arc = Arc::new_leaked(global_style_data.shared_lock.wrap(pdb)); + ApplicableDeclarationBlock::from_declarations( + arc, + ServoCascadeLevel::PresHints, + LayerOrder::root(), + ) + }; + static ref MATHML_LANG_RULE: ApplicableDeclarationBlock = { + let global_style_data = &*GLOBAL_STYLE_DATA; + let pdb = PropertyDeclarationBlock::with_one( + PropertyDeclaration::XLang(SpecifiedLang(atom!("x-math"))), + Importance::Normal, + ); + let arc = Arc::new_leaked(global_style_data.shared_lock.wrap(pdb)); + ApplicableDeclarationBlock::from_declarations( + arc, + ServoCascadeLevel::PresHints, + LayerOrder::root(), + ) + }; + static ref SVG_TEXT_DISABLE_SCALE_RULE: ApplicableDeclarationBlock = { + let global_style_data = &*GLOBAL_STYLE_DATA; + let pdb = PropertyDeclarationBlock::with_one( + PropertyDeclaration::XTextScale(XTextScale::None), + Importance::Normal, + ); + let arc = Arc::new_leaked(global_style_data.shared_lock.wrap(pdb)); + ApplicableDeclarationBlock::from_declarations( + arc, + ServoCascadeLevel::PresHints, + LayerOrder::root(), + ) + }; + }; + + let ns = self.namespace_id(); + //
elements get a default MozCenterOrInherit which may get overridden + if ns == structs::kNameSpaceID_XHTML as i32 { + if self.local_name().as_ptr() == atom!("table").as_ptr() && + self.as_node().owner_doc().quirks_mode() == QuirksMode::Quirks + { + hints.push(TABLE_COLOR_RULE.clone()); + } + } + if ns == structs::kNameSpaceID_SVG as i32 { + if self.local_name().as_ptr() == atom!("text").as_ptr() { + hints.push(SVG_TEXT_DISABLE_SCALE_RULE.clone()); + } + } + let declarations = + unsafe { Gecko_GetHTMLPresentationAttrDeclarationBlock(self.0).as_ref() }; + if let Some(decl) = declarations { + hints.push(ApplicableDeclarationBlock::from_declarations( + unsafe { Arc::from_raw_addrefed(decl) }, + ServoCascadeLevel::PresHints, + LayerOrder::root(), + )); + } + let declarations = unsafe { Gecko_GetExtraContentStyleDeclarations(self.0).as_ref() }; + if let Some(decl) = declarations { + hints.push(ApplicableDeclarationBlock::from_declarations( + unsafe { Arc::from_raw_addrefed(decl) }, + ServoCascadeLevel::PresHints, + LayerOrder::root(), + )); + } + + // Support for link, vlink, and alink presentation hints on + if self.is_link() { + // Unvisited vs. visited styles are computed up-front based on the + // visited mode (not the element's actual state). + let declarations = match visited_handling { + VisitedHandlingMode::AllLinksVisitedAndUnvisited => { + unreachable!( + "We should never try to selector match with \ + AllLinksVisitedAndUnvisited" + ); + }, + VisitedHandlingMode::AllLinksUnvisited => unsafe { + Gecko_GetUnvisitedLinkAttrDeclarationBlock(self.0).as_ref() + }, + VisitedHandlingMode::RelevantLinkVisited => unsafe { + Gecko_GetVisitedLinkAttrDeclarationBlock(self.0).as_ref() + }, + }; + if let Some(decl) = declarations { + hints.push(ApplicableDeclarationBlock::from_declarations( + unsafe { Arc::from_raw_addrefed(decl) }, + ServoCascadeLevel::PresHints, + LayerOrder::root(), + )); + } + + let active = self + .state() + .intersects(NonTSPseudoClass::Active.state_flag()); + if active { + let declarations = + unsafe { Gecko_GetActiveLinkAttrDeclarationBlock(self.0).as_ref() }; + if let Some(decl) = declarations { + hints.push(ApplicableDeclarationBlock::from_declarations( + unsafe { Arc::from_raw_addrefed(decl) }, + ServoCascadeLevel::PresHints, + LayerOrder::root(), + )); + } + } + } + + // xml:lang has precedence over lang, which can be + // set by Gecko_GetHTMLPresentationAttrDeclarationBlock + // + // http://www.whatwg.org/specs/web-apps/current-work/multipage/elements.html#language + let ptr = unsafe { bindings::Gecko_GetXMLLangValue(self.0) }; + if !ptr.is_null() { + let global_style_data = &*GLOBAL_STYLE_DATA; + + let pdb = PropertyDeclarationBlock::with_one( + PropertyDeclaration::XLang(SpecifiedLang(unsafe { Atom::from_addrefed(ptr) })), + Importance::Normal, + ); + let arc = Arc::new(global_style_data.shared_lock.wrap(pdb)); + hints.push(ApplicableDeclarationBlock::from_declarations( + arc, + ServoCascadeLevel::PresHints, + LayerOrder::root(), + )) + } + // MathML's default lang has precedence over both `lang` and `xml:lang` + if ns == structs::kNameSpaceID_MathML as i32 { + if self.local_name().as_ptr() == atom!("math").as_ptr() { + hints.push(MATHML_LANG_RULE.clone()); + } + } + } + + fn anchors_relative_selector(&self) -> bool { + use crate::gecko_bindings::structs::NODE_ANCHORS_RELATIVE_SELECTOR; + self.flags() & NODE_ANCHORS_RELATIVE_SELECTOR != 0 + } +} + +impl<'le> PartialEq for GeckoElement<'le> { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.0 as *const _ == other.0 as *const _ + } +} + +impl<'le> Eq for GeckoElement<'le> {} + +impl<'le> Hash for GeckoElement<'le> { + #[inline] + fn hash(&self, state: &mut H) { + (self.0 as *const RawGeckoElement).hash(state); + } +} + +impl<'le> ::selectors::Element for GeckoElement<'le> { + type Impl = SelectorImpl; + + #[inline] + fn opaque(&self) -> OpaqueElement { + OpaqueElement::new(self.0) + } + + #[inline] + fn parent_element(&self) -> Option { + let parent_node = self.as_node().parent_node(); + parent_node.and_then(|n| n.as_element()) + } + + #[inline] + fn parent_node_is_shadow_root(&self) -> bool { + self.as_node() + .parent_node() + .map_or(false, |p| p.is_shadow_root()) + } + + #[inline] + fn containing_shadow_host(&self) -> Option { + let shadow = self.containing_shadow()?; + Some(shadow.host()) + } + + #[inline] + fn is_pseudo_element(&self) -> bool { + self.implemented_pseudo_element().is_some() + } + + #[inline] + fn pseudo_element_originating_element(&self) -> Option { + debug_assert!(self.is_pseudo_element()); + debug_assert!(!self.matches_user_and_content_rules()); + let mut current = *self; + loop { + if current.is_root_of_native_anonymous_subtree() { + return current.traversal_parent(); + } + + current = current.traversal_parent()?; + } + } + + #[inline] + fn assigned_slot(&self) -> Option { + let slot = self.extended_slots()?._base.mAssignedSlot.mRawPtr; + + unsafe { Some(GeckoElement(&slot.as_ref()?._base._base._base._base)) } + } + + #[inline] + fn prev_sibling_element(&self) -> Option { + let mut sibling = self.as_node().prev_sibling(); + while let Some(sibling_node) = sibling { + if let Some(el) = sibling_node.as_element() { + return Some(el); + } + sibling = sibling_node.prev_sibling(); + } + None + } + + #[inline] + fn next_sibling_element(&self) -> Option { + let mut sibling = self.as_node().next_sibling(); + while let Some(sibling_node) = sibling { + if let Some(el) = sibling_node.as_element() { + return Some(el); + } + sibling = sibling_node.next_sibling(); + } + None + } + + #[inline] + fn first_element_child(&self) -> Option { + let mut child = self.as_node().first_child(); + while let Some(child_node) = child { + if let Some(el) = child_node.as_element() { + return Some(el); + } + child = child_node.next_sibling(); + } + None + } + + fn apply_selector_flags(&self, flags: ElementSelectorFlags) { + // Handle flags that apply to the element. + let self_flags = flags.for_self(); + if !self_flags.is_empty() { + self.set_flags(selector_flags_to_node_flags(flags)) + } + + // Handle flags that apply to the parent. + let parent_flags = flags.for_parent(); + if !parent_flags.is_empty() { + if let Some(p) = self.as_node().parent_node() { + if p.is_element() || p.is_shadow_root() { + p.set_flags(selector_flags_to_node_flags(parent_flags)); + } + } + } + } + + fn attr_matches( + &self, + ns: &NamespaceConstraint<&Namespace>, + local_name: &LocalName, + operation: &AttrSelectorOperation<&AttrValue>, + ) -> bool { + unsafe { + match *operation { + AttrSelectorOperation::Exists => { + bindings::Gecko_HasAttr(self.0, ns.atom_or_null(), local_name.as_ptr()) + }, + AttrSelectorOperation::WithValue { + operator, + case_sensitivity, + expected_value, + } => { + let ignore_case = match case_sensitivity { + CaseSensitivity::CaseSensitive => false, + CaseSensitivity::AsciiCaseInsensitive => true, + }; + // FIXME: case sensitivity for operators other than Equal + match operator { + AttrSelectorOperator::Equal => bindings::Gecko_AttrEquals( + self.0, + ns.atom_or_null(), + local_name.as_ptr(), + expected_value.as_ptr(), + ignore_case, + ), + AttrSelectorOperator::Includes => bindings::Gecko_AttrIncludes( + self.0, + ns.atom_or_null(), + local_name.as_ptr(), + expected_value.as_ptr(), + ignore_case, + ), + AttrSelectorOperator::DashMatch => bindings::Gecko_AttrDashEquals( + self.0, + ns.atom_or_null(), + local_name.as_ptr(), + expected_value.as_ptr(), + ignore_case, + ), + AttrSelectorOperator::Prefix => bindings::Gecko_AttrHasPrefix( + self.0, + ns.atom_or_null(), + local_name.as_ptr(), + expected_value.as_ptr(), + ignore_case, + ), + AttrSelectorOperator::Suffix => bindings::Gecko_AttrHasSuffix( + self.0, + ns.atom_or_null(), + local_name.as_ptr(), + expected_value.as_ptr(), + ignore_case, + ), + AttrSelectorOperator::Substring => bindings::Gecko_AttrHasSubstring( + self.0, + ns.atom_or_null(), + local_name.as_ptr(), + expected_value.as_ptr(), + ignore_case, + ), + } + }, + } + } + } + + #[inline] + fn is_root(&self) -> bool { + if self + .as_node() + .get_bool_flag(nsINode_BooleanFlag::ParentIsContent) + { + return false; + } + + if !self.as_node().is_in_document() { + return false; + } + + debug_assert!(self + .as_node() + .parent_node() + .map_or(false, |p| p.is_document())); + // XXX this should always return true at this point, shouldn't it? + unsafe { bindings::Gecko_IsRootElement(self.0) } + } + + fn is_empty(&self) -> bool { + !self + .as_node() + .dom_children() + .any(|child| unsafe { Gecko_IsSignificantChild(child.0, true) }) + } + + #[inline] + fn has_local_name(&self, name: &WeakAtom) -> bool { + self.local_name() == name + } + + #[inline] + fn has_namespace(&self, ns: &WeakNamespace) -> bool { + self.namespace() == ns + } + + #[inline] + fn is_same_type(&self, other: &Self) -> bool { + self.local_name() == other.local_name() && self.namespace() == other.namespace() + } + + fn match_non_ts_pseudo_class( + &self, + pseudo_class: &NonTSPseudoClass, + context: &mut MatchingContext, + ) -> bool { + use selectors::matching::*; + match *pseudo_class { + NonTSPseudoClass::Autofill | + NonTSPseudoClass::Defined | + NonTSPseudoClass::Focus | + NonTSPseudoClass::Enabled | + NonTSPseudoClass::Disabled | + NonTSPseudoClass::Checked | + NonTSPseudoClass::Fullscreen | + NonTSPseudoClass::Indeterminate | + NonTSPseudoClass::MozInert | + NonTSPseudoClass::PopoverOpen | + NonTSPseudoClass::PlaceholderShown | + NonTSPseudoClass::Target | + NonTSPseudoClass::Valid | + NonTSPseudoClass::Invalid | + NonTSPseudoClass::MozBroken | + NonTSPseudoClass::MozLoading | + NonTSPseudoClass::Required | + NonTSPseudoClass::Optional | + NonTSPseudoClass::ReadOnly | + NonTSPseudoClass::ReadWrite | + NonTSPseudoClass::FocusWithin | + NonTSPseudoClass::FocusVisible | + NonTSPseudoClass::MozDragOver | + NonTSPseudoClass::MozDevtoolsHighlighted | + NonTSPseudoClass::MozStyleeditorTransitioning | + NonTSPseudoClass::MozMathIncrementScriptLevel | + NonTSPseudoClass::InRange | + NonTSPseudoClass::OutOfRange | + NonTSPseudoClass::Default | + NonTSPseudoClass::UserValid | + NonTSPseudoClass::UserInvalid | + NonTSPseudoClass::MozMeterOptimum | + NonTSPseudoClass::MozMeterSubOptimum | + NonTSPseudoClass::MozMeterSubSubOptimum | + NonTSPseudoClass::MozHasDirAttr | + NonTSPseudoClass::MozDirAttrLTR | + NonTSPseudoClass::MozDirAttrRTL | + NonTSPseudoClass::MozDirAttrLikeAuto | + NonTSPseudoClass::Modal | + NonTSPseudoClass::MozTopmostModal | + NonTSPseudoClass::Active | + NonTSPseudoClass::Hover | + NonTSPseudoClass::MozAutofillPreview | + NonTSPseudoClass::MozRevealed | + NonTSPseudoClass::MozValueEmpty | + NonTSPseudoClass::Dir(..) => self.state().intersects(pseudo_class.state_flag()), + NonTSPseudoClass::AnyLink => self.is_link(), + NonTSPseudoClass::Link => { + self.is_link() && context.visited_handling().matches_unvisited() + }, + NonTSPseudoClass::Visited => { + self.is_link() && context.visited_handling().matches_visited() + }, + NonTSPseudoClass::MozFirstNode => { + if context.needs_selector_flags() { + self.apply_selector_flags(ElementSelectorFlags::HAS_EDGE_CHILD_SELECTOR); + } + let mut elem = self.as_node(); + while let Some(prev) = elem.prev_sibling() { + if prev.contains_non_whitespace_content() { + return false; + } + elem = prev; + } + true + }, + NonTSPseudoClass::MozLastNode => { + if context.needs_selector_flags() { + self.apply_selector_flags(ElementSelectorFlags::HAS_EDGE_CHILD_SELECTOR); + } + let mut elem = self.as_node(); + while let Some(next) = elem.next_sibling() { + if next.contains_non_whitespace_content() { + return false; + } + elem = next; + } + true + }, + NonTSPseudoClass::MozOnlyWhitespace => { + if context.needs_selector_flags() { + self.apply_selector_flags(ElementSelectorFlags::HAS_EMPTY_SELECTOR); + } + if self + .as_node() + .dom_children() + .any(|c| c.contains_non_whitespace_content()) + { + return false; + } + true + }, + NonTSPseudoClass::MozNativeAnonymous => !self.matches_user_and_content_rules(), + NonTSPseudoClass::MozUseShadowTreeRoot => self.is_root_of_use_element_shadow_tree(), + NonTSPseudoClass::MozTableBorderNonzero => unsafe { + bindings::Gecko_IsTableBorderNonzero(self.0) + }, + NonTSPseudoClass::MozBrowserFrame => unsafe { bindings::Gecko_IsBrowserFrame(self.0) }, + NonTSPseudoClass::MozSelectListBox => unsafe { + bindings::Gecko_IsSelectListBox(self.0) + }, + NonTSPseudoClass::MozIsHTML => self.is_html_element_in_html_document(), + + NonTSPseudoClass::MozLWTheme | + NonTSPseudoClass::MozLocaleDir(..) | + NonTSPseudoClass::MozWindowInactive => { + let state_bit = pseudo_class.document_state_flag(); + if state_bit.is_empty() { + debug_assert!( + matches!(pseudo_class, NonTSPseudoClass::MozLocaleDir(..)), + "Only moz-locale-dir should ever return an empty state" + ); + return false; + } + if context + .extra_data + .invalidation_data + .document_state + .intersects(state_bit) + { + return !context.in_negation(); + } + self.document_state().contains(state_bit) + }, + NonTSPseudoClass::MozPlaceholder => false, + NonTSPseudoClass::Lang(ref lang_arg) => self.match_element_lang(None, lang_arg), + } + } + + fn match_pseudo_element( + &self, + pseudo_element: &PseudoElement, + _context: &mut MatchingContext, + ) -> bool { + // TODO(emilio): I believe we could assert we are a pseudo-element and + // match the proper pseudo-element, given how we rulehash the stuff + // based on the pseudo. + match self.implemented_pseudo_element() { + Some(ref pseudo) => *pseudo == *pseudo_element, + None => false, + } + } + + #[inline] + fn is_link(&self) -> bool { + self.state().intersects(ElementState::VISITED_OR_UNVISITED) + } + + #[inline] + fn has_id(&self, id: &AtomIdent, case_sensitivity: CaseSensitivity) -> bool { + if !self.has_id() { + return false; + } + + let element_id = match snapshot_helpers::get_id(self.non_mapped_attrs()) { + Some(id) => id, + None => return false, + }; + + case_sensitivity.eq_atom(element_id, id) + } + + #[inline] + fn is_part(&self, name: &AtomIdent) -> bool { + let attr = match self.get_part_attr() { + Some(c) => c, + None => return false, + }; + + snapshot_helpers::has_class_or_part(name, CaseSensitivity::CaseSensitive, attr) + } + + #[inline] + fn imported_part(&self, name: &AtomIdent) -> Option { + snapshot_helpers::imported_part(self.non_mapped_attrs(), name) + } + + #[inline(always)] + fn has_class(&self, name: &AtomIdent, case_sensitivity: CaseSensitivity) -> bool { + let attr = match self.get_class_attr() { + Some(c) => c, + None => return false, + }; + + snapshot_helpers::has_class_or_part(name, case_sensitivity, attr) + } + + #[inline] + fn is_html_element_in_html_document(&self) -> bool { + self.is_html_element() && self.as_node().owner_doc().is_html_document() + } + + #[inline] + fn is_html_slot_element(&self) -> bool { + self.is_html_element() && self.local_name().as_ptr() == local_name!("slot").as_ptr() + } + + #[inline] + fn ignores_nth_child_selectors(&self) -> bool { + self.is_root_of_native_anonymous_subtree() + } +} + +/// A few helpers to help with attribute selectors and snapshotting. +pub trait NamespaceConstraintHelpers { + /// Returns the namespace of the selector, or null otherwise. + fn atom_or_null(&self) -> *mut nsAtom; +} + +impl<'a> NamespaceConstraintHelpers for NamespaceConstraint<&'a Namespace> { + fn atom_or_null(&self) -> *mut nsAtom { + match *self { + NamespaceConstraint::Any => ptr::null_mut(), + NamespaceConstraint::Specific(ref ns) => ns.0.as_ptr(), + } + } +} diff --git a/servo/components/style/gecko_bindings/mod.rs b/servo/components/style/gecko_bindings/mod.rs new file mode 100644 index 0000000000..f0b0adc7ec --- /dev/null +++ b/servo/components/style/gecko_bindings/mod.rs @@ -0,0 +1,28 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Gecko's C++ bindings, along with some rust helpers to ease its use. + +// FIXME: We allow `improper_ctypes` (for now), because the lint doesn't allow +// foreign structs to have `PhantomData`. We should remove this once the lint +// ignores this case. + +#[allow( + dead_code, + improper_ctypes, + non_camel_case_types, + non_snake_case, + non_upper_case_globals, + missing_docs +)] +// TODO: Remove this when updating bindgen, see +// https://github.com/rust-lang/rust-bindgen/issues/1651 +#[cfg_attr(test, allow(deref_nullptr))] +pub mod structs { + include!(concat!(env!("OUT_DIR"), "/gecko/structs.rs")); +} + +pub use self::structs as bindings; + +pub mod sugar; diff --git a/servo/components/style/gecko_bindings/sugar/mod.rs b/servo/components/style/gecko_bindings/sugar/mod.rs new file mode 100644 index 0000000000..00faf63ba6 --- /dev/null +++ b/servo/components/style/gecko_bindings/sugar/mod.rs @@ -0,0 +1,13 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Rust sugar and convenience methods for Gecko types. + +mod ns_com_ptr; +mod ns_compatibility; +mod ns_style_auto_array; +mod ns_t_array; +pub mod origin_flags; +pub mod ownership; +pub mod refptr; diff --git a/servo/components/style/gecko_bindings/sugar/ns_com_ptr.rs b/servo/components/style/gecko_bindings/sugar/ns_com_ptr.rs new file mode 100644 index 0000000000..1c54541bd8 --- /dev/null +++ b/servo/components/style/gecko_bindings/sugar/ns_com_ptr.rs @@ -0,0 +1,15 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Little helpers for `nsCOMPtr`. + +use crate::gecko_bindings::structs::nsCOMPtr; + +impl nsCOMPtr { + /// Get this pointer as a raw pointer. + #[inline] + pub fn raw(&self) -> *mut T { + self.mRawPtr + } +} diff --git a/servo/components/style/gecko_bindings/sugar/ns_compatibility.rs b/servo/components/style/gecko_bindings/sugar/ns_compatibility.rs new file mode 100644 index 0000000000..f4b81e9f79 --- /dev/null +++ b/servo/components/style/gecko_bindings/sugar/ns_compatibility.rs @@ -0,0 +1,19 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Little helper for `nsCompatibility`. + +use crate::context::QuirksMode; +use crate::gecko_bindings::structs::nsCompatibility; + +impl From for QuirksMode { + #[inline] + fn from(mode: nsCompatibility) -> QuirksMode { + match mode { + nsCompatibility::eCompatibility_FullStandards => QuirksMode::NoQuirks, + nsCompatibility::eCompatibility_AlmostStandards => QuirksMode::LimitedQuirks, + nsCompatibility::eCompatibility_NavQuirks => QuirksMode::Quirks, + } + } +} diff --git a/servo/components/style/gecko_bindings/sugar/ns_style_auto_array.rs b/servo/components/style/gecko_bindings/sugar/ns_style_auto_array.rs new file mode 100644 index 0000000000..b5772a6c77 --- /dev/null +++ b/servo/components/style/gecko_bindings/sugar/ns_style_auto_array.rs @@ -0,0 +1,111 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Rust helpers for Gecko's `nsStyleAutoArray`. + +use crate::gecko_bindings::bindings::Gecko_EnsureStyleAnimationArrayLength; +use crate::gecko_bindings::bindings::Gecko_EnsureStyleScrollTimelineArrayLength; +use crate::gecko_bindings::bindings::Gecko_EnsureStyleTransitionArrayLength; +use crate::gecko_bindings::bindings::Gecko_EnsureStyleViewTimelineArrayLength; +use crate::gecko_bindings::structs::nsStyleAutoArray; +use crate::gecko_bindings::structs::{StyleAnimation, StyleTransition}; +use crate::gecko_bindings::structs::{StyleScrollTimeline, StyleViewTimeline}; +use std::iter::{once, Chain, IntoIterator, Once}; +use std::ops::{Index, IndexMut}; +use std::slice::{Iter, IterMut}; + +impl Index for nsStyleAutoArray { + type Output = T; + fn index(&self, index: usize) -> &T { + match index { + 0 => &self.mFirstElement, + _ => &self.mOtherElements[index - 1], + } + } +} + +impl IndexMut for nsStyleAutoArray { + fn index_mut(&mut self, index: usize) -> &mut T { + match index { + 0 => &mut self.mFirstElement, + _ => &mut self.mOtherElements[index - 1], + } + } +} + +impl nsStyleAutoArray { + /// Mutably iterate over the array elements. + pub fn iter_mut(&mut self) -> Chain, IterMut> { + once(&mut self.mFirstElement).chain(self.mOtherElements.iter_mut()) + } + + /// Iterate over the array elements. + pub fn iter(&self) -> Chain, Iter> { + once(&self.mFirstElement).chain(self.mOtherElements.iter()) + } + + /// Returns the length of the array. + /// + /// Note that often structs containing autoarrays will have additional + /// member fields that contain the length, which must be kept in sync. + pub fn len(&self) -> usize { + 1 + self.mOtherElements.len() + } +} + +impl nsStyleAutoArray { + /// Ensures that the array has length at least the given length. + pub fn ensure_len(&mut self, len: usize) { + unsafe { + Gecko_EnsureStyleAnimationArrayLength( + self as *mut nsStyleAutoArray as *mut _, + len, + ); + } + } +} + +impl nsStyleAutoArray { + /// Ensures that the array has length at least the given length. + pub fn ensure_len(&mut self, len: usize) { + unsafe { + Gecko_EnsureStyleTransitionArrayLength( + self as *mut nsStyleAutoArray as *mut _, + len, + ); + } + } +} + +impl nsStyleAutoArray { + /// Ensures that the array has length at least the given length. + pub fn ensure_len(&mut self, len: usize) { + unsafe { + Gecko_EnsureStyleViewTimelineArrayLength( + self as *mut nsStyleAutoArray as *mut _, + len, + ); + } + } +} + +impl nsStyleAutoArray { + /// Ensures that the array has length at least the given length. + pub fn ensure_len(&mut self, len: usize) { + unsafe { + Gecko_EnsureStyleScrollTimelineArrayLength( + self as *mut nsStyleAutoArray as *mut _, + len, + ); + } + } +} + +impl<'a, T> IntoIterator for &'a mut nsStyleAutoArray { + type Item = &'a mut T; + type IntoIter = Chain, IterMut<'a, T>>; + fn into_iter(self) -> Self::IntoIter { + self.iter_mut() + } +} diff --git a/servo/components/style/gecko_bindings/sugar/ns_t_array.rs b/servo/components/style/gecko_bindings/sugar/ns_t_array.rs new file mode 100644 index 0000000000..d10ed420dd --- /dev/null +++ b/servo/components/style/gecko_bindings/sugar/ns_t_array.rs @@ -0,0 +1,144 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Rust helpers for Gecko's nsTArray. + +use crate::gecko_bindings::bindings; +use crate::gecko_bindings::structs::{nsTArray, nsTArrayHeader, CopyableTArray}; +use std::mem; +use std::ops::{Deref, DerefMut}; +use std::slice; + +impl Deref for nsTArray { + type Target = [T]; + + #[inline] + fn deref<'a>(&'a self) -> &'a [T] { + unsafe { slice::from_raw_parts(self.slice_begin(), self.header().mLength as usize) } + } +} + +impl DerefMut for nsTArray { + fn deref_mut<'a>(&'a mut self) -> &'a mut [T] { + unsafe { slice::from_raw_parts_mut(self.slice_begin(), self.header().mLength as usize) } + } +} + +impl nsTArray { + #[inline] + fn header<'a>(&'a self) -> &'a nsTArrayHeader { + debug_assert!(!self.mBuffer.is_null()); + unsafe { mem::transmute(self.mBuffer) } + } + + // unsafe, since header may be in shared static or something + unsafe fn header_mut<'a>(&'a mut self) -> &'a mut nsTArrayHeader { + debug_assert!(!self.mBuffer.is_null()); + + mem::transmute(self.mBuffer) + } + + #[inline] + unsafe fn slice_begin(&self) -> *mut T { + debug_assert!(!self.mBuffer.is_null()); + (self.mBuffer as *const nsTArrayHeader).offset(1) as *mut _ + } + + /// Ensures the array has enough capacity at least to hold `cap` elements. + /// + /// NOTE: This doesn't call the constructor on the values! + pub fn ensure_capacity(&mut self, cap: usize) { + if cap >= self.len() { + unsafe { + bindings::Gecko_EnsureTArrayCapacity( + self as *mut nsTArray as *mut _, + cap, + mem::size_of::(), + ) + } + } + } + + /// Clears the array storage without calling the destructor on the values. + #[inline] + pub unsafe fn clear(&mut self) { + if self.len() != 0 { + bindings::Gecko_ClearPODTArray( + self as *mut nsTArray as *mut _, + mem::size_of::(), + mem::align_of::(), + ); + } + } + + /// Clears a POD array. This is safe since copy types are memcopyable. + #[inline] + pub fn clear_pod(&mut self) + where + T: Copy, + { + unsafe { self.clear() } + } + + /// Resize and set the length of the array to `len`. + /// + /// unsafe because this may leave the array with uninitialized elements. + /// + /// This will not call constructors. If you need that, either manually add + /// bindings or run the typed `EnsureCapacity` call on the gecko side. + pub unsafe fn set_len(&mut self, len: u32) { + // this can leak + debug_assert!(len >= self.len() as u32); + if self.len() == len as usize { + return; + } + self.ensure_capacity(len as usize); + self.header_mut().mLength = len; + } + + /// Resizes an array containing only POD elements + /// + /// unsafe because this may leave the array with uninitialized elements. + /// + /// This will not leak since it only works on POD types (and thus doesn't assert) + pub unsafe fn set_len_pod(&mut self, len: u32) + where + T: Copy, + { + if self.len() == len as usize { + return; + } + self.ensure_capacity(len as usize); + let header = self.header_mut(); + header.mLength = len; + } + + /// Collects the given iterator into this array. + /// + /// Not unsafe because we won't leave uninitialized elements in the array. + pub fn assign_from_iter_pod(&mut self, iter: I) + where + T: Copy, + I: ExactSizeIterator + Iterator, + { + debug_assert!(iter.len() <= 0xFFFFFFFF); + unsafe { + self.set_len_pod(iter.len() as u32); + } + self.iter_mut().zip(iter).for_each(|(r, v)| *r = v); + } +} + +impl Deref for CopyableTArray { + type Target = nsTArray; + fn deref(&self) -> &Self::Target { + &self._base + } +} + +impl DerefMut for CopyableTArray { + fn deref_mut(&mut self) -> &mut nsTArray { + &mut self._base + } +} diff --git a/servo/components/style/gecko_bindings/sugar/origin_flags.rs b/servo/components/style/gecko_bindings/sugar/origin_flags.rs new file mode 100644 index 0000000000..b27060405a --- /dev/null +++ b/servo/components/style/gecko_bindings/sugar/origin_flags.rs @@ -0,0 +1,31 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Helper to iterate over `OriginFlags` bits. + +use crate::gecko_bindings::structs::OriginFlags; +use crate::stylesheets::OriginSet; + +/// Checks that the values for OriginFlags are the ones we expect. +pub fn assert_flags_match() { + use crate::stylesheets::origin::*; + debug_assert_eq!( + OriginFlags::UserAgent.0, + OriginSet::ORIGIN_USER_AGENT.bits() + ); + debug_assert_eq!(OriginFlags::Author.0, OriginSet::ORIGIN_AUTHOR.bits()); + debug_assert_eq!(OriginFlags::User.0, OriginSet::ORIGIN_USER.bits()); +} + +impl From for OriginSet { + fn from(flags: OriginFlags) -> Self { + Self::from_bits_retain(flags.0) + } +} + +impl From for OriginFlags { + fn from(set: OriginSet) -> Self { + OriginFlags(set.bits()) + } +} diff --git a/servo/components/style/gecko_bindings/sugar/ownership.rs b/servo/components/style/gecko_bindings/sugar/ownership.rs new file mode 100644 index 0000000000..31b512cf1e --- /dev/null +++ b/servo/components/style/gecko_bindings/sugar/ownership.rs @@ -0,0 +1,61 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Helpers for different FFI pointer kinds that Gecko's FFI layer uses. + +use crate::gecko_bindings::structs::root::mozilla::detail::CopyablePtr; +use servo_arc::Arc; +use std::marker::PhantomData; +use std::ops::{Deref, DerefMut}; +use std::ptr; + +/// Gecko-FFI-safe Arc (T is an ArcInner). +/// +/// This can be null. +/// +/// Leaks on drop. Please don't drop this. +#[repr(C)] +pub struct Strong { + ptr: *const GeckoType, + _marker: PhantomData, +} + +impl From> for Strong { + fn from(arc: Arc) -> Self { + Self { + ptr: Arc::into_raw(arc), + _marker: PhantomData, + } + } +} + +impl Strong { + #[inline] + /// Returns whether this reference is null. + pub fn is_null(&self) -> bool { + self.ptr.is_null() + } + + #[inline] + /// Returns a null pointer + pub fn null() -> Self { + Self { + ptr: ptr::null(), + _marker: PhantomData, + } + } +} + +impl Deref for CopyablePtr { + type Target = T; + fn deref(&self) -> &Self::Target { + &self.mPtr + } +} + +impl DerefMut for CopyablePtr { + fn deref_mut<'a>(&'a mut self) -> &'a mut T { + &mut self.mPtr + } +} diff --git a/servo/components/style/gecko_bindings/sugar/refptr.rs b/servo/components/style/gecko_bindings/sugar/refptr.rs new file mode 100644 index 0000000000..c4a0479a07 --- /dev/null +++ b/servo/components/style/gecko_bindings/sugar/refptr.rs @@ -0,0 +1,289 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! A rust helper to ease the use of Gecko's refcounted types. + +use crate::gecko_bindings::{bindings, structs}; +use crate::Atom; +use servo_arc::Arc; +use std::fmt::Write; +use std::marker::PhantomData; +use std::ops::Deref; +use std::{fmt, mem, ptr}; + +/// Trait for all objects that have Addref() and Release +/// methods and can be placed inside RefPtr +pub unsafe trait RefCounted { + /// Bump the reference count. + fn addref(&self); + /// Decrease the reference count. + unsafe fn release(&self); +} + +/// Trait for types which can be shared across threads in RefPtr. +pub unsafe trait ThreadSafeRefCounted: RefCounted {} + +/// A custom RefPtr implementation to take into account Drop semantics and +/// a bit less-painful memory management. +pub struct RefPtr { + ptr: *mut T, + _marker: PhantomData, +} + +impl fmt::Debug for RefPtr { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("RefPtr { ")?; + self.ptr.fmt(f)?; + f.write_char('}') + } +} + +impl RefPtr { + /// Create a new RefPtr from an already addrefed pointer obtained from FFI. + /// + /// The pointer must be valid, non-null and have been addrefed. + pub unsafe fn from_addrefed(ptr: *mut T) -> Self { + debug_assert!(!ptr.is_null()); + RefPtr { + ptr, + _marker: PhantomData, + } + } + + /// Returns whether the current pointer is null. + pub fn is_null(&self) -> bool { + self.ptr.is_null() + } + + /// Returns a null pointer. + pub fn null() -> Self { + Self { + ptr: ptr::null_mut(), + _marker: PhantomData, + } + } + + /// Create a new RefPtr from a pointer obtained from FFI. + /// + /// This method calls addref() internally + pub unsafe fn new(ptr: *mut T) -> Self { + let ret = RefPtr { + ptr, + _marker: PhantomData, + }; + ret.addref(); + ret + } + + /// Produces an FFI-compatible RefPtr that can be stored in style structs. + /// + /// structs::RefPtr does not have a destructor, so this may leak + pub fn forget(self) -> structs::RefPtr { + let ret = structs::RefPtr { + mRawPtr: self.ptr, + _phantom_0: PhantomData, + }; + mem::forget(self); + ret + } + + /// Returns the raw inner pointer to be fed back into FFI. + pub fn get(&self) -> *mut T { + self.ptr + } + + /// Addref the inner data, obviously leaky on its own. + pub fn addref(&self) { + if !self.ptr.is_null() { + unsafe { + (*self.ptr).addref(); + } + } + } + + /// Release the inner data. + /// + /// Call only when the data actually needs releasing. + pub unsafe fn release(&self) { + if !self.ptr.is_null() { + (*self.ptr).release(); + } + } +} + +impl Deref for RefPtr { + type Target = T; + fn deref(&self) -> &T { + debug_assert!(!self.ptr.is_null()); + unsafe { &*self.ptr } + } +} + +impl structs::RefPtr { + /// Produces a Rust-side RefPtr from an FFI RefPtr, bumping the refcount. + /// + /// Must be called on a valid, non-null structs::RefPtr. + pub unsafe fn to_safe(&self) -> RefPtr { + let r = RefPtr { + ptr: self.mRawPtr, + _marker: PhantomData, + }; + r.addref(); + r + } + /// Produces a Rust-side RefPtr, consuming the existing one (and not bumping + /// the refcount). + pub unsafe fn into_safe(self) -> RefPtr { + debug_assert!(!self.mRawPtr.is_null()); + RefPtr { + ptr: self.mRawPtr, + _marker: PhantomData, + } + } + + /// Replace a structs::RefPtr with a different one, appropriately + /// addref/releasing. + /// + /// Both `self` and `other` must be valid, but can be null. + /// + /// Safe when called on an aliased pointer because the refcount in that case + /// needs to be at least two. + pub unsafe fn set(&mut self, other: &Self) { + self.clear(); + if !other.mRawPtr.is_null() { + *self = other.to_safe().forget(); + } + } + + /// Clear an instance of the structs::RefPtr, by releasing + /// it and setting its contents to null. + /// + /// `self` must be valid, but can be null. + pub unsafe fn clear(&mut self) { + if !self.mRawPtr.is_null() { + (*self.mRawPtr).release(); + self.mRawPtr = ptr::null_mut(); + } + } + + /// Replace a `structs::RefPtr` with a `RefPtr`, + /// consuming the `RefPtr`, and releasing the old + /// value in `self` if necessary. + /// + /// `self` must be valid, possibly null. + pub fn set_move(&mut self, other: RefPtr) { + if !self.mRawPtr.is_null() { + unsafe { + (*self.mRawPtr).release(); + } + } + *self = other.forget(); + } +} + +impl structs::RefPtr { + /// Returns a new, null refptr. + pub fn null() -> Self { + Self { + mRawPtr: ptr::null_mut(), + _phantom_0: PhantomData, + } + } + + /// Create a new RefPtr from an arc. + pub fn from_arc(s: Arc) -> Self { + Self { + mRawPtr: Arc::into_raw(s) as *mut _, + _phantom_0: PhantomData, + } + } + + /// Sets the contents to an Arc. + pub fn set_arc(&mut self, other: Arc) { + unsafe { + if !self.mRawPtr.is_null() { + let _ = Arc::from_raw(self.mRawPtr); + } + self.mRawPtr = Arc::into_raw(other) as *mut _; + } + } +} + +impl Drop for RefPtr { + fn drop(&mut self) { + unsafe { self.release() } + } +} + +impl Clone for RefPtr { + fn clone(&self) -> Self { + self.addref(); + RefPtr { + ptr: self.ptr, + _marker: PhantomData, + } + } +} + +impl PartialEq for RefPtr { + fn eq(&self, other: &Self) -> bool { + self.ptr == other.ptr + } +} + +unsafe impl Send for RefPtr {} +unsafe impl Sync for RefPtr {} + +macro_rules! impl_refcount { + ($t:ty, $addref:path, $release:path) => { + unsafe impl RefCounted for $t { + #[inline] + fn addref(&self) { + unsafe { $addref(self as *const _ as *mut _) } + } + + #[inline] + unsafe fn release(&self) { + $release(self as *const _ as *mut _) + } + } + }; +} + +// Companion of NS_DECL_THREADSAFE_FFI_REFCOUNTING. +// +// Gets you a free RefCounted impl implemented via FFI. +macro_rules! impl_threadsafe_refcount { + ($t:ty, $addref:path, $release:path) => { + impl_refcount!($t, $addref, $release); + unsafe impl ThreadSafeRefCounted for $t {} + }; +} + +impl_threadsafe_refcount!( + structs::mozilla::URLExtraData, + bindings::Gecko_AddRefURLExtraDataArbitraryThread, + bindings::Gecko_ReleaseURLExtraDataArbitraryThread +); +impl_threadsafe_refcount!( + structs::nsIURI, + bindings::Gecko_AddRefnsIURIArbitraryThread, + bindings::Gecko_ReleasensIURIArbitraryThread +); +impl_threadsafe_refcount!( + structs::SheetLoadDataHolder, + bindings::Gecko_AddRefSheetLoadDataHolderArbitraryThread, + bindings::Gecko_ReleaseSheetLoadDataHolderArbitraryThread +); + +#[inline] +unsafe fn addref_atom(atom: *mut structs::nsAtom) { + mem::forget(Atom::from_raw(atom)); +} + +#[inline] +unsafe fn release_atom(atom: *mut structs::nsAtom) { + let _ = Atom::from_addrefed(atom); +} +impl_threadsafe_refcount!(structs::nsAtom, addref_atom, release_atom); diff --git a/servo/components/style/gecko_string_cache/mod.rs b/servo/components/style/gecko_string_cache/mod.rs new file mode 100644 index 0000000000..cb040390cf --- /dev/null +++ b/servo/components/style/gecko_string_cache/mod.rs @@ -0,0 +1,532 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#![allow(unsafe_code)] +// This is needed for the constants in atom_macro.rs, because we have some +// atoms whose names differ only by case, e.g. datetime and dateTime. +#![allow(non_upper_case_globals)] + +//! A drop-in replacement for string_cache, but backed by Gecko `nsAtom`s. + +use crate::gecko_bindings::bindings::Gecko_AddRefAtom; +use crate::gecko_bindings::bindings::Gecko_Atomize; +use crate::gecko_bindings::bindings::Gecko_Atomize16; +use crate::gecko_bindings::bindings::Gecko_ReleaseAtom; +use crate::gecko_bindings::structs::root::mozilla::detail::gGkAtoms; +use crate::gecko_bindings::structs::root::mozilla::detail::kGkAtomsArrayOffset; +use crate::gecko_bindings::structs::root::mozilla::detail::GkAtoms_Atoms_AtomsCount; +use crate::gecko_bindings::structs::{nsAtom, nsDynamicAtom, nsStaticAtom}; +use nsstring::{nsAString, nsStr}; +use precomputed_hash::PrecomputedHash; +use std::borrow::{Borrow, Cow}; +use std::char::{self, DecodeUtf16}; +use std::fmt::{self, Write}; +use std::hash::{Hash, Hasher}; +use std::iter::Cloned; +use std::mem::{self, ManuallyDrop}; +use std::num::NonZeroUsize; +use std::ops::Deref; +use std::{slice, str}; +use style_traits::SpecifiedValueInfo; +use to_shmem::{self, SharedMemoryBuilder, ToShmem}; + +#[macro_use] +#[allow(improper_ctypes, non_camel_case_types, missing_docs)] +pub mod atom_macro { + include!(concat!(env!("OUT_DIR"), "/gecko/atom_macro.rs")); +} + +#[macro_use] +pub mod namespace; + +pub use self::namespace::{Namespace, WeakNamespace}; + +/// A handle to a Gecko atom. This is a type that can represent either: +/// +/// * A strong reference to a dynamic atom (an `nsAtom` pointer), in which case +/// the `usize` just holds the pointer value. +/// +/// * A byte offset from `gGkAtoms` to the `nsStaticAtom` object (shifted to +/// the left one bit, and with the lower bit set to `1` to differentiate it +/// from the above), so `(offset << 1 | 1)`. +/// +#[derive(Eq, PartialEq)] +#[repr(C)] +pub struct Atom(NonZeroUsize); + +/// An atom *without* a strong reference. +/// +/// Only usable as `&'a WeakAtom`, +/// where `'a` is the lifetime of something that holds a strong reference to that atom. +pub struct WeakAtom(nsAtom); + +/// The number of static atoms we have. +const STATIC_ATOM_COUNT: usize = GkAtoms_Atoms_AtomsCount as usize; + +/// Returns the Gecko static atom array. +/// +/// We have this rather than use rust-bindgen to generate +/// mozilla::detail::gGkAtoms and then just reference gGkAtoms.mAtoms, so we +/// avoid a problem with lld-link.exe on Windows. +/// +/// https://bugzilla.mozilla.org/show_bug.cgi?id=1517685 +#[inline] +fn static_atoms() -> &'static [nsStaticAtom; STATIC_ATOM_COUNT] { + unsafe { + let addr = &gGkAtoms as *const _ as usize + kGkAtomsArrayOffset as usize; + &*(addr as *const _) + } +} + +/// Returns whether the specified address points to one of the nsStaticAtom +/// objects in the Gecko static atom array. +#[inline] +fn valid_static_atom_addr(addr: usize) -> bool { + unsafe { + let atoms = static_atoms(); + let start = atoms.as_ptr(); + let end = atoms.get_unchecked(STATIC_ATOM_COUNT) as *const _; + let in_range = addr >= start as usize && addr < end as usize; + let aligned = addr % mem::align_of::() == 0; + in_range && aligned + } +} + +impl Deref for Atom { + type Target = WeakAtom; + + #[inline] + fn deref(&self) -> &WeakAtom { + unsafe { + let addr = if self.is_static() { + (&gGkAtoms as *const _ as usize) + (self.0.get() >> 1) + } else { + self.0.get() + }; + debug_assert!(!self.is_static() || valid_static_atom_addr(addr)); + WeakAtom::new(addr as *const nsAtom) + } + } +} + +impl PrecomputedHash for Atom { + #[inline] + fn precomputed_hash(&self) -> u32 { + self.get_hash() + } +} + +impl Borrow for Atom { + #[inline] + fn borrow(&self) -> &WeakAtom { + self + } +} + +impl ToShmem for Atom { + fn to_shmem(&self, _builder: &mut SharedMemoryBuilder) -> to_shmem::Result { + if !self.is_static() { + return Err(format!( + "ToShmem failed for Atom: must be a static atom: {}", + self + )); + } + + Ok(ManuallyDrop::new(Atom(self.0))) + } +} + +impl Eq for WeakAtom {} +impl PartialEq for WeakAtom { + #[inline] + fn eq(&self, other: &Self) -> bool { + let weak: *const WeakAtom = self; + let other: *const WeakAtom = other; + weak == other + } +} + +impl PartialEq for WeakAtom { + #[inline] + fn eq(&self, other: &Atom) -> bool { + self == &**other + } +} + +unsafe impl Send for Atom {} +unsafe impl Sync for Atom {} +unsafe impl Sync for WeakAtom {} + +impl WeakAtom { + /// Construct a `WeakAtom` from a raw `nsAtom`. + #[inline] + pub unsafe fn new<'a>(atom: *const nsAtom) -> &'a mut Self { + &mut *(atom as *mut WeakAtom) + } + + /// Clone this atom, bumping the refcount if the atom is not static. + #[inline] + pub fn clone(&self) -> Atom { + unsafe { Atom::from_raw(self.as_ptr()) } + } + + /// Get the atom hash. + #[inline] + pub fn get_hash(&self) -> u32 { + self.0.mHash + } + + /// Get the atom as a slice of utf-16 chars. + #[inline] + pub fn as_slice(&self) -> &[u16] { + let string = if self.is_static() { + let atom_ptr = self.as_ptr() as *const nsStaticAtom; + let string_offset = unsafe { (*atom_ptr).mStringOffset }; + let string_offset = -(string_offset as isize); + let u8_ptr = atom_ptr as *const u8; + // It is safe to use offset() here because both addresses are within + // the same struct, e.g. mozilla::detail::gGkAtoms. + unsafe { u8_ptr.offset(string_offset) as *const u16 } + } else { + let atom_ptr = self.as_ptr() as *const nsDynamicAtom; + // Dynamic atom chars are stored at the end of the object. + unsafe { atom_ptr.offset(1) as *const u16 } + }; + unsafe { slice::from_raw_parts(string, self.len() as usize) } + } + + // NOTE: don't expose this, since it's slow, and easy to be misused. + fn chars(&self) -> DecodeUtf16>> { + char::decode_utf16(self.as_slice().iter().cloned()) + } + + /// Execute `cb` with the string that this atom represents. + /// + /// Find alternatives to this function when possible, please, since it's + /// pretty slow. + pub fn with_str(&self, cb: F) -> Output + where + F: FnOnce(&str) -> Output, + { + let mut buffer = mem::MaybeUninit::<[u8; 64]>::uninit(); + let buffer = unsafe { &mut *buffer.as_mut_ptr() }; + + // The total string length in utf16 is going to be less than or equal + // the slice length (each utf16 character is going to take at least one + // and at most 2 items in the utf16 slice). + // + // Each of those characters will take at most four bytes in the utf8 + // one. Thus if the slice is less than 64 / 4 (16) we can guarantee that + // we'll decode it in place. + let owned_string; + let len = self.len(); + let utf8_slice = if len <= 16 { + let mut total_len = 0; + + for c in self.chars() { + let c = c.unwrap_or(char::REPLACEMENT_CHARACTER); + let utf8_len = c.encode_utf8(&mut buffer[total_len..]).len(); + total_len += utf8_len; + } + + let slice = unsafe { str::from_utf8_unchecked(&buffer[..total_len]) }; + debug_assert_eq!(slice, String::from_utf16_lossy(self.as_slice())); + slice + } else { + owned_string = String::from_utf16_lossy(self.as_slice()); + &*owned_string + }; + + cb(utf8_slice) + } + + /// Returns whether this atom is static. + #[inline] + pub fn is_static(&self) -> bool { + self.0.mIsStatic() != 0 + } + + /// Returns whether this atom is ascii lowercase. + #[inline] + fn is_ascii_lowercase(&self) -> bool { + self.0.mIsAsciiLowercase() != 0 + } + + /// Returns the length of the atom string. + #[inline] + pub fn len(&self) -> u32 { + self.0.mLength() + } + + /// Returns whether this atom is the empty string. + #[inline] + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Returns the atom as a mutable pointer. + #[inline] + pub fn as_ptr(&self) -> *mut nsAtom { + let const_ptr: *const nsAtom = &self.0; + const_ptr as *mut nsAtom + } + + /// Convert this atom to ASCII lower-case + pub fn to_ascii_lowercase(&self) -> Atom { + if self.is_ascii_lowercase() { + return self.clone(); + } + + let slice = self.as_slice(); + let mut buffer = mem::MaybeUninit::<[u16; 64]>::uninit(); + let buffer = unsafe { &mut *buffer.as_mut_ptr() }; + let mut vec; + let mutable_slice = if let Some(buffer_prefix) = buffer.get_mut(..slice.len()) { + buffer_prefix.copy_from_slice(slice); + buffer_prefix + } else { + vec = slice.to_vec(); + &mut vec + }; + for char16 in &mut *mutable_slice { + if *char16 <= 0x7F { + *char16 = (*char16 as u8).to_ascii_lowercase() as u16 + } + } + Atom::from(&*mutable_slice) + } + + /// Return whether two atoms are ASCII-case-insensitive matches + #[inline] + pub fn eq_ignore_ascii_case(&self, other: &Self) -> bool { + if self == other { + return true; + } + + // If we know both atoms are ascii-lowercase, then we can stick with + // pointer equality. + if self.is_ascii_lowercase() && other.is_ascii_lowercase() { + debug_assert!(!self.eq_ignore_ascii_case_slow(other)); + return false; + } + + self.eq_ignore_ascii_case_slow(other) + } + + fn eq_ignore_ascii_case_slow(&self, other: &Self) -> bool { + let a = self.as_slice(); + let b = other.as_slice(); + + if a.len() != b.len() { + return false; + } + + a.iter().zip(b).all(|(&a16, &b16)| { + if a16 <= 0x7F && b16 <= 0x7F { + (a16 as u8).eq_ignore_ascii_case(&(b16 as u8)) + } else { + a16 == b16 + } + }) + } +} + +impl fmt::Debug for WeakAtom { + fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result { + write!(w, "Gecko WeakAtom({:p}, {})", self, self) + } +} + +impl fmt::Display for WeakAtom { + fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result { + for c in self.chars() { + w.write_char(c.unwrap_or(char::REPLACEMENT_CHARACTER))? + } + Ok(()) + } +} + +#[inline] +unsafe fn make_handle(ptr: *const nsAtom) -> NonZeroUsize { + debug_assert!(!ptr.is_null()); + if !WeakAtom::new(ptr).is_static() { + NonZeroUsize::new_unchecked(ptr as usize) + } else { + make_static_handle(ptr as *mut nsStaticAtom) + } +} + +#[inline] +unsafe fn make_static_handle(ptr: *const nsStaticAtom) -> NonZeroUsize { + // FIXME(heycam): Use offset_from once it's stabilized. + // https://github.com/rust-lang/rust/issues/41079 + debug_assert!(valid_static_atom_addr(ptr as usize)); + let base = &gGkAtoms as *const _; + let offset = ptr as usize - base as usize; + NonZeroUsize::new_unchecked((offset << 1) | 1) +} + +impl Atom { + #[inline] + fn is_static(&self) -> bool { + self.0.get() & 1 == 1 + } + + /// Execute a callback with the atom represented by `ptr`. + pub unsafe fn with(ptr: *const nsAtom, callback: F) -> R + where + F: FnOnce(&Atom) -> R, + { + let atom = Atom(make_handle(ptr as *mut nsAtom)); + let ret = callback(&atom); + mem::forget(atom); + ret + } + + /// Creates a static atom from its index in the static atom table, without + /// checking. + #[inline] + pub const unsafe fn from_index_unchecked(index: u16) -> Self { + // FIXME(emilio): No support for debug_assert! in const fn for now. Note + // that violating this invariant will debug-assert in the `Deref` impl + // though. + // + // debug_assert!((index as usize) < STATIC_ATOM_COUNT); + let offset = + (index as usize) * std::mem::size_of::() + kGkAtomsArrayOffset as usize; + Atom(NonZeroUsize::new_unchecked((offset << 1) | 1)) + } + + /// Creates an atom from an atom pointer. + #[inline(always)] + pub unsafe fn from_raw(ptr: *mut nsAtom) -> Self { + let atom = Atom(make_handle(ptr)); + if !atom.is_static() { + Gecko_AddRefAtom(ptr); + } + atom + } + + /// Creates an atom from an atom pointer that has already had AddRef + /// called on it. This may be a static or dynamic atom. + #[inline] + pub unsafe fn from_addrefed(ptr: *mut nsAtom) -> Self { + assert!(!ptr.is_null()); + Atom(make_handle(ptr)) + } + + /// Convert this atom into an addrefed nsAtom pointer. + #[inline] + pub fn into_addrefed(self) -> *mut nsAtom { + let ptr = self.as_ptr(); + mem::forget(self); + ptr + } +} + +impl Hash for Atom { + fn hash(&self, state: &mut H) + where + H: Hasher, + { + state.write_u32(self.get_hash()); + } +} + +impl Hash for WeakAtom { + fn hash(&self, state: &mut H) + where + H: Hasher, + { + state.write_u32(self.get_hash()); + } +} + +impl Clone for Atom { + #[inline(always)] + fn clone(&self) -> Atom { + unsafe { + let atom = Atom(self.0); + if !atom.is_static() { + Gecko_AddRefAtom(atom.as_ptr()); + } + atom + } + } +} + +impl Drop for Atom { + #[inline] + fn drop(&mut self) { + if !self.is_static() { + unsafe { + Gecko_ReleaseAtom(self.as_ptr()); + } + } + } +} + +impl Default for Atom { + #[inline] + fn default() -> Self { + atom!("") + } +} + +impl fmt::Debug for Atom { + fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result { + write!(w, "Atom(0x{:08x}, {})", self.0, self) + } +} + +impl fmt::Display for Atom { + fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result { + self.deref().fmt(w) + } +} + +impl<'a> From<&'a str> for Atom { + #[inline] + fn from(string: &str) -> Atom { + debug_assert!(string.len() <= u32::max_value() as usize); + unsafe { + Atom::from_addrefed(Gecko_Atomize( + string.as_ptr() as *const _, + string.len() as u32, + )) + } + } +} + +impl<'a> From<&'a [u16]> for Atom { + #[inline] + fn from(slice: &[u16]) -> Atom { + Atom::from(&*nsStr::from(slice)) + } +} + +impl<'a> From<&'a nsAString> for Atom { + #[inline] + fn from(string: &nsAString) -> Atom { + unsafe { Atom::from_addrefed(Gecko_Atomize16(string)) } + } +} + +impl<'a> From> for Atom { + #[inline] + fn from(string: Cow<'a, str>) -> Atom { + Atom::from(&*string) + } +} + +impl From for Atom { + #[inline] + fn from(string: String) -> Atom { + Atom::from(&*string) + } +} + +malloc_size_of_is_0!(Atom); + +impl SpecifiedValueInfo for Atom {} diff --git a/servo/components/style/gecko_string_cache/namespace.rs b/servo/components/style/gecko_string_cache/namespace.rs new file mode 100644 index 0000000000..d9745b9e21 --- /dev/null +++ b/servo/components/style/gecko_string_cache/namespace.rs @@ -0,0 +1,105 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! A type to represent a namespace. + +use crate::gecko_bindings::structs::nsAtom; +use crate::string_cache::{Atom, WeakAtom}; +use precomputed_hash::PrecomputedHash; +use std::borrow::Borrow; +use std::fmt; +use std::ops::Deref; + +/// In Gecko namespaces are just regular atoms, so this is a simple macro to +/// forward one macro to the other. +#[macro_export] +macro_rules! ns { + () => { + $crate::string_cache::Namespace(atom!("")) + }; + ($s:tt) => { + $crate::string_cache::Namespace(atom!($s)) + }; +} + +/// A Gecko namespace is just a wrapped atom. +#[derive( + Clone, + Debug, + Default, + Eq, + Hash, + MallocSizeOf, + PartialEq, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(transparent)] +pub struct Namespace(pub Atom); + +impl PrecomputedHash for Namespace { + #[inline] + fn precomputed_hash(&self) -> u32 { + self.0.precomputed_hash() + } +} + +/// A Gecko WeakNamespace is a wrapped WeakAtom. +#[derive(Deref, Hash)] +pub struct WeakNamespace(WeakAtom); + +impl Deref for Namespace { + type Target = WeakNamespace; + + #[inline] + fn deref(&self) -> &WeakNamespace { + let weak: *const WeakAtom = &*self.0; + unsafe { &*(weak as *const WeakNamespace) } + } +} + +impl<'a> From<&'a str> for Namespace { + fn from(s: &'a str) -> Self { + Namespace(Atom::from(s)) + } +} + +impl fmt::Display for Namespace { + fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result { + self.0.fmt(w) + } +} + +impl Borrow for Namespace { + #[inline] + fn borrow(&self) -> &WeakNamespace { + self + } +} + +impl WeakNamespace { + /// Trivially construct a WeakNamespace. + #[inline] + pub unsafe fn new<'a>(atom: *mut nsAtom) -> &'a Self { + &*(atom as *const WeakNamespace) + } + + /// Clone this WeakNamespace to obtain a strong reference to the same + /// underlying namespace. + #[inline] + pub fn clone(&self) -> Namespace { + Namespace(self.0.clone()) + } +} + +impl Eq for WeakNamespace {} +impl PartialEq for WeakNamespace { + #[inline] + fn eq(&self, other: &Self) -> bool { + let weak: *const WeakNamespace = self; + let other: *const WeakNamespace = other; + weak == other + } +} diff --git a/servo/components/style/global_style_data.rs b/servo/components/style/global_style_data.rs new file mode 100644 index 0000000000..2a00bccd77 --- /dev/null +++ b/servo/components/style/global_style_data.rs @@ -0,0 +1,171 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Global style data + +use crate::context::StyleSystemOptions; +#[cfg(feature = "gecko")] +use crate::gecko_bindings::bindings; +use crate::parallel::STYLE_THREAD_STACK_SIZE_KB; +use crate::shared_lock::SharedRwLock; +use crate::thread_state; +use gecko_profiler; +use parking_lot::{Mutex, RwLock, RwLockReadGuard}; +use rayon; +use std::{io, thread}; + +/// Global style data +pub struct GlobalStyleData { + /// Shared RWLock for CSSOM objects + pub shared_lock: SharedRwLock, + + /// Global style system options determined by env vars. + pub options: StyleSystemOptions, +} + +/// Global thread pool. +pub struct StyleThreadPool { + /// How many threads parallel styling can use. If not using a thread pool, this is set to `None`. + pub num_threads: Option, + + /// The parallel styling thread pool. + /// + /// For leak-checking purposes, we want to terminate the thread-pool, which + /// waits for all the async jobs to complete. Thus the RwLock. + style_thread_pool: RwLock>, +} + +fn thread_name(index: usize) -> String { + format!("StyleThread#{}", index) +} + +lazy_static! { + /// JoinHandles for spawned style threads. These will be joined during + /// StyleThreadPool::shutdown() after exiting the thread pool. + /// + /// This would be quite inefficient if rayon destroyed and re-created + /// threads regularly during threadpool operation in response to demand, + /// however rayon actually never destroys its threads until the entire + /// thread pool is shut-down, so the size of this list is bounded. + static ref STYLE_THREAD_JOIN_HANDLES: Mutex>> = + Mutex::new(Vec::new()); +} + +fn thread_spawn(options: rayon::ThreadBuilder) -> io::Result<()> { + let mut b = thread::Builder::new(); + if let Some(name) = options.name() { + b = b.name(name.to_owned()); + } + if let Some(stack_size) = options.stack_size() { + b = b.stack_size(stack_size); + } + let join_handle = b.spawn(|| options.run())?; + STYLE_THREAD_JOIN_HANDLES.lock().push(join_handle); + Ok(()) +} + +fn thread_startup(_index: usize) { + thread_state::initialize_layout_worker_thread(); + #[cfg(feature = "gecko")] + unsafe { + bindings::Gecko_SetJemallocThreadLocalArena(true); + let name = thread_name(_index); + gecko_profiler::register_thread(&name); + } +} + +fn thread_shutdown(_: usize) { + #[cfg(feature = "gecko")] + unsafe { + gecko_profiler::unregister_thread(); + bindings::Gecko_SetJemallocThreadLocalArena(false); + } +} + +impl StyleThreadPool { + /// Shuts down the thread pool, waiting for all work to complete. + pub fn shutdown() { + if STYLE_THREAD_JOIN_HANDLES.lock().is_empty() { + return; + } + { + // Drop the pool. + let _ = STYLE_THREAD_POOL.style_thread_pool.write().take(); + } + + // Join spawned threads until all of the threads have been joined. This + // will usually be pretty fast, as on shutdown there should be basically + // no threads left running. + while let Some(join_handle) = STYLE_THREAD_JOIN_HANDLES.lock().pop() { + let _ = join_handle.join(); + } + } + + /// Returns a reference to the thread pool. + /// + /// We only really want to give read-only access to the pool, except + /// for shutdown(). + pub fn pool(&self) -> RwLockReadGuard> { + self.style_thread_pool.read() + } +} + +#[cfg(feature = "servo")] +fn stylo_threads_pref() -> i32 { + pref!(layout.threads) +} + +#[cfg(feature = "gecko")] +fn stylo_threads_pref() -> i32 { + static_prefs::pref!("layout.css.stylo-threads") +} + +lazy_static! { + /// Global thread pool + pub static ref STYLE_THREAD_POOL: StyleThreadPool = { + // We always set this pref on startup, before layout or script have had a chance of + // accessing (and thus creating) the thread-pool. + let threads_pref: i32 = stylo_threads_pref(); + + let num_threads = if threads_pref >= 0 { + threads_pref as usize + } else { + use num_cpus; + use std::cmp; + // The default heuristic is num_virtual_cores * .75. This gives us three threads on a + // hyper-threaded dual core, and six threads on a hyper-threaded quad core. + // + // The performance benefit of additional threads seems to level off at around six, so + // we cap it there on many-core machines (see bug 1431285 comment 14). + let threads = cmp::min(cmp::max(num_cpus::get() * 3 / 4, 1), 6); + // There's no point in creating a thread pool if there's one thread. + if threads == 1 { 0 } else { threads } + }; + + let (pool, num_threads) = if num_threads < 1 { + (None, None) + } else { + let workers = rayon::ThreadPoolBuilder::new() + .spawn_handler(thread_spawn) + .num_threads(num_threads) + .thread_name(thread_name) + .start_handler(thread_startup) + .exit_handler(thread_shutdown) + .stack_size(STYLE_THREAD_STACK_SIZE_KB * 1024) + .build(); + (workers.ok(), Some(num_threads)) + }; + + StyleThreadPool { + num_threads, + style_thread_pool: RwLock::new(pool), + } + }; + + /// Global style data + pub static ref GLOBAL_STYLE_DATA: GlobalStyleData = GlobalStyleData { + shared_lock: SharedRwLock::new_leaked(), + options: StyleSystemOptions::default(), + }; +} diff --git a/servo/components/style/invalidation/element/document_state.rs b/servo/components/style/invalidation/element/document_state.rs new file mode 100644 index 0000000000..5900549c79 --- /dev/null +++ b/servo/components/style/invalidation/element/document_state.rs @@ -0,0 +1,142 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! An invalidation processor for style changes due to document state changes. + +use crate::dom::TElement; +use crate::invalidation::element::invalidation_map::Dependency; +use crate::invalidation::element::invalidator::{DescendantInvalidationLists, InvalidationVector}; +use crate::invalidation::element::invalidator::{Invalidation, InvalidationProcessor}; +use crate::invalidation::element::state_and_attributes; +use crate::stylist::CascadeData; +use dom::DocumentState; +use selectors::matching::{ + MatchingContext, MatchingMode, NeedsSelectorFlags, QuirksMode, VisitedHandlingMode, +}; +use selectors::NthIndexCache; + +/// A struct holding the members necessary to invalidate document state +/// selectors. +#[derive(Debug)] +pub struct InvalidationMatchingData { + /// The document state that has changed, which makes it always match. + pub document_state: DocumentState, +} + +impl Default for InvalidationMatchingData { + #[inline(always)] + fn default() -> Self { + Self { + document_state: DocumentState::empty(), + } + } +} + +/// An invalidation processor for style changes due to state and attribute +/// changes. +pub struct DocumentStateInvalidationProcessor<'a, E: TElement, I> { + rules: I, + matching_context: MatchingContext<'a, E::Impl>, + document_states_changed: DocumentState, +} + +impl<'a, E: TElement, I> DocumentStateInvalidationProcessor<'a, E, I> { + /// Creates a new DocumentStateInvalidationProcessor. + #[inline] + pub fn new( + rules: I, + document_states_changed: DocumentState, + nth_index_cache: &'a mut NthIndexCache, + quirks_mode: QuirksMode, + ) -> Self { + let mut matching_context = MatchingContext::<'a, E::Impl>::new_for_visited( + MatchingMode::Normal, + None, + nth_index_cache, + VisitedHandlingMode::AllLinksVisitedAndUnvisited, + quirks_mode, + NeedsSelectorFlags::No, + ); + + matching_context.extra_data.invalidation_data.document_state = document_states_changed; + + Self { + rules, + document_states_changed, + matching_context, + } + } +} + +impl<'a, E, I> InvalidationProcessor<'a, E> for DocumentStateInvalidationProcessor<'a, E, I> +where + E: TElement, + I: Iterator, +{ + fn check_outer_dependency(&mut self, _: &Dependency, _: E) -> bool { + debug_assert!( + false, + "how, we should only have parent-less dependencies here!" + ); + true + } + + fn collect_invalidations( + &mut self, + _element: E, + self_invalidations: &mut InvalidationVector<'a>, + _descendant_invalidations: &mut DescendantInvalidationLists<'a>, + _sibling_invalidations: &mut InvalidationVector<'a>, + ) -> bool { + for cascade_data in &mut self.rules { + let map = cascade_data.invalidation_map(); + for dependency in &map.document_state_selectors { + if !dependency.state.intersects(self.document_states_changed) { + continue; + } + + // We pass `None` as a scope, as document state selectors aren't + // affected by the current scope. + // + // FIXME(emilio): We should really pass the relevant host for + // self.rules, so that we invalidate correctly if the selector + // happens to have something like :host(:-moz-window-inactive) + // for example. + self_invalidations.push(Invalidation::new( + &dependency.dependency, + /* scope = */ None, + )); + } + } + + false + } + + fn matching_context(&mut self) -> &mut MatchingContext<'a, E::Impl> { + &mut self.matching_context + } + + fn recursion_limit_exceeded(&mut self, _: E) { + unreachable!("We don't run document state invalidation with stack limits") + } + + fn should_process_descendants(&mut self, element: E) -> bool { + match element.borrow_data() { + Some(d) => state_and_attributes::should_process_descendants(&d), + None => false, + } + } + + fn invalidated_descendants(&mut self, element: E, child: E) { + state_and_attributes::invalidated_descendants(element, child) + } + + fn invalidated_self(&mut self, element: E) { + state_and_attributes::invalidated_self(element); + } + + fn invalidated_sibling(&mut self, sibling: E, of: E) { + state_and_attributes::invalidated_sibling(sibling, of); + } +} diff --git a/servo/components/style/invalidation/element/element_wrapper.rs b/servo/components/style/invalidation/element/element_wrapper.rs new file mode 100644 index 0000000000..71c3c94daf --- /dev/null +++ b/servo/components/style/invalidation/element/element_wrapper.rs @@ -0,0 +1,391 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! A wrapper over an element and a snapshot, that allows us to selector-match +//! against a past state of the element. + +use crate::dom::TElement; +use crate::selector_parser::{AttrValue, NonTSPseudoClass, PseudoElement, SelectorImpl}; +use crate::selector_parser::{Snapshot, SnapshotMap}; +use crate::values::AtomIdent; +use crate::{CaseSensitivityExt, LocalName, Namespace, WeakAtom}; +use dom::ElementState; +use selectors::attr::{AttrSelectorOperation, CaseSensitivity, NamespaceConstraint}; +use selectors::matching::{ElementSelectorFlags, MatchingContext}; +use selectors::{Element, OpaqueElement}; +use std::cell::Cell; +use std::fmt; + +/// In order to compute restyle hints, we perform a selector match against a +/// list of partial selectors whose rightmost simple selector may be sensitive +/// to the thing being changed. We do this matching twice, once for the element +/// as it exists now and once for the element as it existed at the time of the +/// last restyle. If the results of the selector match differ, that means that +/// the given partial selector is sensitive to the change, and we compute a +/// restyle hint based on its combinator. +/// +/// In order to run selector matching against the old element state, we generate +/// a wrapper for the element which claims to have the old state. This is the +/// ElementWrapper logic below. +/// +/// Gecko does this differently for element states, and passes a mask called +/// mStateMask, which indicates the states that need to be ignored during +/// selector matching. This saves an ElementWrapper allocation and an additional +/// selector match call at the expense of additional complexity inside the +/// selector matching logic. This only works for boolean states though, so we +/// still need to take the ElementWrapper approach for attribute-dependent +/// style. So we do it the same both ways for now to reduce complexity, but it's +/// worth measuring the performance impact (if any) of the mStateMask approach. +pub trait ElementSnapshot: Sized { + /// The state of the snapshot, if any. + fn state(&self) -> Option; + + /// If this snapshot contains attribute information. + fn has_attrs(&self) -> bool; + + /// Gets the attribute information of the snapshot as a string. + /// + /// Only for debugging purposes. + fn debug_list_attributes(&self) -> String { + String::new() + } + + /// The ID attribute per this snapshot. Should only be called if + /// `has_attrs()` returns true. + fn id_attr(&self) -> Option<&WeakAtom>; + + /// Whether this snapshot contains the class `name`. Should only be called + /// if `has_attrs()` returns true. + fn has_class(&self, name: &AtomIdent, case_sensitivity: CaseSensitivity) -> bool; + + /// Whether this snapshot represents the part named `name`. Should only be + /// called if `has_attrs()` returns true. + fn is_part(&self, name: &AtomIdent) -> bool; + + /// See Element::imported_part. + fn imported_part(&self, name: &AtomIdent) -> Option; + + /// A callback that should be called for each class of the snapshot. Should + /// only be called if `has_attrs()` returns true. + fn each_class(&self, _: F) + where + F: FnMut(&AtomIdent); + + /// The `xml:lang=""` or `lang=""` attribute value per this snapshot. + fn lang_attr(&self) -> Option; +} + +/// A simple wrapper over an element and a snapshot, that allows us to +/// selector-match against a past state of the element. +#[derive(Clone)] +pub struct ElementWrapper<'a, E> +where + E: TElement, +{ + element: E, + cached_snapshot: Cell>, + snapshot_map: &'a SnapshotMap, +} + +impl<'a, E> ElementWrapper<'a, E> +where + E: TElement, +{ + /// Trivially constructs an `ElementWrapper`. + pub fn new(el: E, snapshot_map: &'a SnapshotMap) -> Self { + ElementWrapper { + element: el, + cached_snapshot: Cell::new(None), + snapshot_map: snapshot_map, + } + } + + /// Gets the snapshot associated with this element, if any. + pub fn snapshot(&self) -> Option<&'a Snapshot> { + if !self.element.has_snapshot() { + return None; + } + + if let Some(s) = self.cached_snapshot.get() { + return Some(s); + } + + let snapshot = self.snapshot_map.get(&self.element); + debug_assert!(snapshot.is_some(), "has_snapshot lied!"); + + self.cached_snapshot.set(snapshot); + + snapshot + } + + /// Returns the states that have changed since the element was snapshotted. + pub fn state_changes(&self) -> ElementState { + let snapshot = match self.snapshot() { + Some(s) => s, + None => return ElementState::empty(), + }; + + match snapshot.state() { + Some(state) => state ^ self.element.state(), + None => ElementState::empty(), + } + } + + /// Returns the value of the `xml:lang=""` (or, if appropriate, `lang=""`) + /// attribute from this element's snapshot or the closest ancestor + /// element snapshot with the attribute specified. + fn get_lang(&self) -> Option { + let mut current = self.clone(); + loop { + let lang = match self.snapshot() { + Some(snapshot) if snapshot.has_attrs() => snapshot.lang_attr(), + _ => current.element.lang_attr(), + }; + if lang.is_some() { + return lang; + } + current = current.parent_element()?; + } + } +} + +impl<'a, E> fmt::Debug for ElementWrapper<'a, E> +where + E: TElement, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // Ignore other fields for now, can change later if needed. + self.element.fmt(f) + } +} + +impl<'a, E> Element for ElementWrapper<'a, E> +where + E: TElement, +{ + type Impl = SelectorImpl; + + fn match_non_ts_pseudo_class( + &self, + pseudo_class: &NonTSPseudoClass, + context: &mut MatchingContext, + ) -> bool { + // Some pseudo-classes need special handling to evaluate them against + // the snapshot. + match *pseudo_class { + // For :link and :visited, we don't actually want to test the + // element state directly. + // + // Instead, we use the `visited_handling` to determine if they + // match. + NonTSPseudoClass::Link => { + return self.is_link() && context.visited_handling().matches_unvisited(); + }, + NonTSPseudoClass::Visited => { + return self.is_link() && context.visited_handling().matches_visited(); + }, + + #[cfg(feature = "gecko")] + NonTSPseudoClass::MozTableBorderNonzero => { + if let Some(snapshot) = self.snapshot() { + if snapshot.has_other_pseudo_class_state() { + return snapshot.mIsTableBorderNonzero(); + } + } + }, + + #[cfg(feature = "gecko")] + NonTSPseudoClass::MozBrowserFrame => { + if let Some(snapshot) = self.snapshot() { + if snapshot.has_other_pseudo_class_state() { + return snapshot.mIsMozBrowserFrame(); + } + } + }, + + #[cfg(feature = "gecko")] + NonTSPseudoClass::MozSelectListBox => { + if let Some(snapshot) = self.snapshot() { + if snapshot.has_other_pseudo_class_state() { + return snapshot.mIsSelectListBox(); + } + } + }, + + // :lang() needs to match using the closest ancestor xml:lang="" or + // lang="" attribtue from snapshots. + NonTSPseudoClass::Lang(ref lang_arg) => { + return self + .element + .match_element_lang(Some(self.get_lang()), lang_arg); + }, + + _ => {}, + } + + let flag = pseudo_class.state_flag(); + if flag.is_empty() { + return self + .element + .match_non_ts_pseudo_class(pseudo_class, context); + } + match self.snapshot().and_then(|s| s.state()) { + Some(snapshot_state) => snapshot_state.intersects(flag), + None => self + .element + .match_non_ts_pseudo_class(pseudo_class, context), + } + } + + fn apply_selector_flags(&self, _flags: ElementSelectorFlags) { + debug_assert!(false, "Shouldn't need selector flags for invalidation"); + } + + fn match_pseudo_element( + &self, + pseudo_element: &PseudoElement, + context: &mut MatchingContext, + ) -> bool { + self.element.match_pseudo_element(pseudo_element, context) + } + + fn is_link(&self) -> bool { + match self.snapshot().and_then(|s| s.state()) { + Some(state) => state.intersects(ElementState::VISITED_OR_UNVISITED), + None => self.element.is_link(), + } + } + + fn opaque(&self) -> OpaqueElement { + self.element.opaque() + } + + fn parent_element(&self) -> Option { + let parent = self.element.parent_element()?; + Some(Self::new(parent, self.snapshot_map)) + } + + fn parent_node_is_shadow_root(&self) -> bool { + self.element.parent_node_is_shadow_root() + } + + fn containing_shadow_host(&self) -> Option { + let host = self.element.containing_shadow_host()?; + Some(Self::new(host, self.snapshot_map)) + } + + fn prev_sibling_element(&self) -> Option { + let sibling = self.element.prev_sibling_element()?; + Some(Self::new(sibling, self.snapshot_map)) + } + + fn next_sibling_element(&self) -> Option { + let sibling = self.element.next_sibling_element()?; + Some(Self::new(sibling, self.snapshot_map)) + } + + fn first_element_child(&self) -> Option { + let child = self.element.first_element_child()?; + Some(Self::new(child, self.snapshot_map)) + } + + #[inline] + fn is_html_element_in_html_document(&self) -> bool { + self.element.is_html_element_in_html_document() + } + + #[inline] + fn is_html_slot_element(&self) -> bool { + self.element.is_html_slot_element() + } + + #[inline] + fn has_local_name( + &self, + local_name: &::BorrowedLocalName, + ) -> bool { + self.element.has_local_name(local_name) + } + + #[inline] + fn has_namespace( + &self, + ns: &::BorrowedNamespaceUrl, + ) -> bool { + self.element.has_namespace(ns) + } + + #[inline] + fn is_same_type(&self, other: &Self) -> bool { + self.element.is_same_type(&other.element) + } + + fn attr_matches( + &self, + ns: &NamespaceConstraint<&Namespace>, + local_name: &LocalName, + operation: &AttrSelectorOperation<&AttrValue>, + ) -> bool { + match self.snapshot() { + Some(snapshot) if snapshot.has_attrs() => { + snapshot.attr_matches(ns, local_name, operation) + }, + _ => self.element.attr_matches(ns, local_name, operation), + } + } + + fn has_id(&self, id: &AtomIdent, case_sensitivity: CaseSensitivity) -> bool { + match self.snapshot() { + Some(snapshot) if snapshot.has_attrs() => snapshot + .id_attr() + .map_or(false, |atom| case_sensitivity.eq_atom(&atom, id)), + _ => self.element.has_id(id, case_sensitivity), + } + } + + fn is_part(&self, name: &AtomIdent) -> bool { + match self.snapshot() { + Some(snapshot) if snapshot.has_attrs() => snapshot.is_part(name), + _ => self.element.is_part(name), + } + } + + fn imported_part(&self, name: &AtomIdent) -> Option { + match self.snapshot() { + Some(snapshot) if snapshot.has_attrs() => snapshot.imported_part(name), + _ => self.element.imported_part(name), + } + } + + fn has_class(&self, name: &AtomIdent, case_sensitivity: CaseSensitivity) -> bool { + match self.snapshot() { + Some(snapshot) if snapshot.has_attrs() => snapshot.has_class(name, case_sensitivity), + _ => self.element.has_class(name, case_sensitivity), + } + } + + fn is_empty(&self) -> bool { + self.element.is_empty() + } + + fn is_root(&self) -> bool { + self.element.is_root() + } + + fn is_pseudo_element(&self) -> bool { + self.element.is_pseudo_element() + } + + fn pseudo_element_originating_element(&self) -> Option { + self.element + .pseudo_element_originating_element() + .map(|e| ElementWrapper::new(e, self.snapshot_map)) + } + + fn assigned_slot(&self) -> Option { + self.element + .assigned_slot() + .map(|e| ElementWrapper::new(e, self.snapshot_map)) + } +} diff --git a/servo/components/style/invalidation/element/invalidation_map.rs b/servo/components/style/invalidation/element/invalidation_map.rs new file mode 100644 index 0000000000..7fa552e458 --- /dev/null +++ b/servo/components/style/invalidation/element/invalidation_map.rs @@ -0,0 +1,545 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Code for invalidations due to state or attribute changes. + +use crate::context::QuirksMode; +use crate::selector_map::{ + MaybeCaseInsensitiveHashMap, PrecomputedHashMap, SelectorMap, SelectorMapEntry, +}; +use crate::selector_parser::SelectorImpl; +use crate::AllocErr; +use crate::{Atom, LocalName, Namespace, ShrinkIfNeeded}; +use dom::{DocumentState, ElementState}; +use selectors::attr::NamespaceConstraint; +use selectors::parser::{Combinator, Component}; +use selectors::parser::{Selector, SelectorIter}; +use selectors::visitor::{SelectorListKind, SelectorVisitor}; +use smallvec::SmallVec; + +/// Mapping between (partial) CompoundSelectors (and the combinator to their +/// right) and the states and attributes they depend on. +/// +/// In general, for all selectors in all applicable stylesheets of the form: +/// +/// |a _ b _ c _ d _ e| +/// +/// Where: +/// * |b| and |d| are simple selectors that depend on state (like :hover) or +/// attributes (like [attr...], .foo, or #foo). +/// * |a|, |c|, and |e| are arbitrary simple selectors that do not depend on +/// state or attributes. +/// +/// We generate a Dependency for both |a _ b:X _| and |a _ b:X _ c _ d:Y _|, +/// even though those selectors may not appear on their own in any stylesheet. +/// This allows us to quickly scan through the dependency sites of all style +/// rules and determine the maximum effect that a given state or attribute +/// change may have on the style of elements in the document. +#[derive(Clone, Debug, MallocSizeOf)] +pub struct Dependency { + /// The dependency selector. + #[cfg_attr( + feature = "gecko", + ignore_malloc_size_of = "CssRules have primary refs, we measure there" + )] + #[cfg_attr(feature = "servo", ignore_malloc_size_of = "Arc")] + pub selector: Selector, + + /// The offset into the selector that we should match on. + pub selector_offset: usize, + + /// The parent dependency for an ancestor selector. For example, consider + /// the following: + /// + /// .foo .bar:where(.baz span) .qux + /// ^ ^ ^ + /// A B C + /// + /// We'd generate: + /// + /// * One dependency for .qux (offset: 0, parent: None) + /// * One dependency for .baz pointing to B with parent being a + /// dependency pointing to C. + /// * One dependency from .bar pointing to C (parent: None) + /// * One dependency from .foo pointing to A (parent: None) + /// + pub parent: Option>, +} + +/// The kind of elements down the tree this dependency may affect. +#[derive(Debug, Eq, PartialEq)] +pub enum DependencyInvalidationKind { + /// This dependency may affect the element that changed itself. + Element, + /// This dependency affects the style of the element itself, and also the + /// style of its descendants. + /// + /// TODO(emilio): Each time this feels more of a hack for eager pseudos... + ElementAndDescendants, + /// This dependency may affect descendants down the tree. + Descendants, + /// This dependency may affect siblings to the right of the element that + /// changed. + Siblings, + /// This dependency may affect slotted elements of the element that changed. + SlottedElements, + /// This dependency may affect parts of the element that changed. + Parts, +} + +impl Dependency { + /// Creates a dummy dependency to invalidate the whole selector. + /// + /// This is necessary because document state invalidation wants to + /// invalidate all elements in the document. + /// + /// The offset is such as that Invalidation::new(self) returns a zero + /// offset. That is, it points to a virtual "combinator" outside of the + /// selector, so calling combinator() on such a dependency will panic. + pub fn for_full_selector_invalidation(selector: Selector) -> Self { + Self { + selector_offset: selector.len() + 1, + selector, + parent: None, + } + } + + /// Returns the combinator to the right of the partial selector this + /// dependency represents. + /// + /// TODO(emilio): Consider storing inline if it helps cache locality? + pub fn combinator(&self) -> Option { + if self.selector_offset == 0 { + return None; + } + + Some( + self.selector + .combinator_at_match_order(self.selector_offset - 1), + ) + } + + /// The kind of invalidation that this would generate. + pub fn invalidation_kind(&self) -> DependencyInvalidationKind { + match self.combinator() { + None => DependencyInvalidationKind::Element, + Some(Combinator::Child) | Some(Combinator::Descendant) => { + DependencyInvalidationKind::Descendants + }, + Some(Combinator::LaterSibling) | Some(Combinator::NextSibling) => { + DependencyInvalidationKind::Siblings + }, + // TODO(emilio): We could look at the selector itself to see if it's + // an eager pseudo, and return only Descendants here if not. + Some(Combinator::PseudoElement) => DependencyInvalidationKind::ElementAndDescendants, + Some(Combinator::SlotAssignment) => DependencyInvalidationKind::SlottedElements, + Some(Combinator::Part) => DependencyInvalidationKind::Parts, + } + } +} + +impl SelectorMapEntry for Dependency { + fn selector(&self) -> SelectorIter { + self.selector.iter_from(self.selector_offset) + } +} + +/// The same, but for state selectors, which can track more exactly what state +/// do they track. +#[derive(Clone, Debug, MallocSizeOf)] +pub struct StateDependency { + /// The other dependency fields. + pub dep: Dependency, + /// The state this dependency is affected by. + pub state: ElementState, +} + +impl SelectorMapEntry for StateDependency { + fn selector(&self) -> SelectorIter { + self.dep.selector() + } +} + +/// The same, but for document state selectors. +#[derive(Clone, Debug, MallocSizeOf)] +pub struct DocumentStateDependency { + /// We track `Dependency` even though we don't need to track an offset, + /// since when it changes it changes for the whole document anyway. + #[cfg_attr( + feature = "gecko", + ignore_malloc_size_of = "CssRules have primary refs, we measure there" + )] + #[cfg_attr(feature = "servo", ignore_malloc_size_of = "Arc")] + pub dependency: Dependency, + /// The state this dependency is affected by. + pub state: DocumentState, +} + +/// A map where we store invalidations. +/// +/// This is slightly different to a SelectorMap, in the sense of that the same +/// selector may appear multiple times. +/// +/// In particular, we want to lookup as few things as possible to get the fewer +/// selectors the better, so this looks up by id, class, or looks at the list of +/// state/other attribute affecting selectors. +#[derive(Clone, Debug, MallocSizeOf)] +pub struct InvalidationMap { + /// A map from a given class name to all the selectors with that class + /// selector. + pub class_to_selector: MaybeCaseInsensitiveHashMap>, + /// A map from a given id to all the selectors with that ID in the + /// stylesheets currently applying to the document. + pub id_to_selector: MaybeCaseInsensitiveHashMap>, + /// A map of all the state dependencies. + pub state_affecting_selectors: SelectorMap, + /// A list of document state dependencies in the rules we represent. + pub document_state_selectors: Vec, + /// A map of other attribute affecting selectors. + pub other_attribute_affecting_selectors: + PrecomputedHashMap>, +} + +impl InvalidationMap { + /// Creates an empty `InvalidationMap`. + pub fn new() -> Self { + Self { + class_to_selector: MaybeCaseInsensitiveHashMap::new(), + id_to_selector: MaybeCaseInsensitiveHashMap::new(), + state_affecting_selectors: SelectorMap::new(), + document_state_selectors: Vec::new(), + other_attribute_affecting_selectors: PrecomputedHashMap::default(), + } + } + + /// Returns the number of dependencies stored in the invalidation map. + pub fn len(&self) -> usize { + self.state_affecting_selectors.len() + + self.document_state_selectors.len() + + self.other_attribute_affecting_selectors + .iter() + .fold(0, |accum, (_, ref v)| accum + v.len()) + + self.id_to_selector + .iter() + .fold(0, |accum, (_, ref v)| accum + v.len()) + + self.class_to_selector + .iter() + .fold(0, |accum, (_, ref v)| accum + v.len()) + } + + /// Clears this map, leaving it empty. + pub fn clear(&mut self) { + self.class_to_selector.clear(); + self.id_to_selector.clear(); + self.state_affecting_selectors.clear(); + self.document_state_selectors.clear(); + self.other_attribute_affecting_selectors.clear(); + } + + /// Shrink the capacity of hash maps if needed. + pub fn shrink_if_needed(&mut self) { + self.class_to_selector.shrink_if_needed(); + self.id_to_selector.shrink_if_needed(); + self.state_affecting_selectors.shrink_if_needed(); + self.other_attribute_affecting_selectors.shrink_if_needed(); + } + + /// Adds a selector to this `InvalidationMap`. Returns Err(..) to + /// signify OOM. + pub fn note_selector( + &mut self, + selector: &Selector, + quirks_mode: QuirksMode, + ) -> Result<(), AllocErr> { + debug!("InvalidationMap::note_selector({:?})", selector); + + let mut document_state = DocumentState::empty(); + + { + let mut parent_stack = SmallVec::new(); + let mut alloc_error = None; + let mut collector = SelectorDependencyCollector { + map: self, + document_state: &mut document_state, + selector, + parent_selectors: &mut parent_stack, + quirks_mode, + compound_state: PerCompoundState::new(0), + alloc_error: &mut alloc_error, + }; + + let visit_result = collector.visit_whole_selector(); + debug_assert_eq!(!visit_result, alloc_error.is_some()); + if let Some(alloc_error) = alloc_error { + return Err(alloc_error); + } + } + + if !document_state.is_empty() { + let dep = DocumentStateDependency { + state: document_state, + dependency: Dependency::for_full_selector_invalidation(selector.clone()), + }; + self.document_state_selectors.try_reserve(1)?; + self.document_state_selectors.push(dep); + } + + Ok(()) + } +} + +struct PerCompoundState { + /// The offset at which our compound starts. + offset: usize, + + /// The state this compound selector is affected by. + element_state: ElementState, +} + +impl PerCompoundState { + fn new(offset: usize) -> Self { + Self { + offset, + element_state: ElementState::empty(), + } + } +} + +/// A struct that collects invalidations for a given compound selector. +struct SelectorDependencyCollector<'a> { + map: &'a mut InvalidationMap, + + /// The document this _complex_ selector is affected by. + /// + /// We don't need to track state per compound selector, since it's global + /// state and it changes for everything. + document_state: &'a mut DocumentState, + + /// The current selector and offset we're iterating. + selector: &'a Selector, + + /// The stack of parent selectors that we have, and at which offset of the + /// sequence. + /// + /// This starts empty. It grows when we find nested :is and :where selector + /// lists. + parent_selectors: &'a mut SmallVec<[(Selector, usize); 5]>, + + /// The quirks mode of the document where we're inserting dependencies. + quirks_mode: QuirksMode, + + /// State relevant to a given compound selector. + compound_state: PerCompoundState, + + /// The allocation error, if we OOM. + alloc_error: &'a mut Option, +} + +impl<'a> SelectorDependencyCollector<'a> { + fn visit_whole_selector(&mut self) -> bool { + let iter = self.selector.iter(); + self.visit_whole_selector_from(iter, 0) + } + + fn visit_whole_selector_from( + &mut self, + mut iter: SelectorIter, + mut index: usize, + ) -> bool { + loop { + // Reset the compound state. + self.compound_state = PerCompoundState::new(index); + + // Visit all the simple selectors in this sequence. + for ss in &mut iter { + if !ss.visit(self) { + return false; + } + index += 1; // Account for the simple selector. + } + + if !self.compound_state.element_state.is_empty() { + let dependency = self.dependency(); + let result = self.map.state_affecting_selectors.insert( + StateDependency { + dep: dependency, + state: self.compound_state.element_state, + }, + self.quirks_mode, + ); + if let Err(alloc_error) = result { + *self.alloc_error = Some(alloc_error.into()); + return false; + } + } + + let combinator = iter.next_sequence(); + if combinator.is_none() { + return true; + } + index += 1; // account for the combinator + } + } + + fn add_attr_dependency(&mut self, name: LocalName) -> bool { + let dependency = self.dependency(); + + let map = &mut self.map.other_attribute_affecting_selectors; + if let Err(err) = map.try_reserve(1) { + *self.alloc_error = Some(err.into()); + return false; + } + let vec = map.entry(name).or_default(); + if let Err(err) = vec.try_reserve(1) { + *self.alloc_error = Some(err.into()); + return false; + } + vec.push(dependency); + true + } + + fn dependency(&self) -> Dependency { + let mut parent = None; + + // TODO(emilio): Maybe we should refcount the parent dependencies, or + // cache them or something. + for &(ref selector, ref selector_offset) in self.parent_selectors.iter() { + debug_assert_ne!( + self.compound_state.offset, 0, + "Shouldn't bother creating nested dependencies for the rightmost compound", + ); + let new_parent = Dependency { + selector: selector.clone(), + selector_offset: *selector_offset, + parent, + }; + parent = Some(Box::new(new_parent)); + } + + Dependency { + selector: self.selector.clone(), + selector_offset: self.compound_state.offset, + parent, + } + } +} + +impl<'a> SelectorVisitor for SelectorDependencyCollector<'a> { + type Impl = SelectorImpl; + + fn visit_selector_list( + &mut self, + _list_kind: SelectorListKind, + list: &[Selector], + ) -> bool { + for selector in list { + // Here we cheat a bit: We can visit the rightmost compound with + // the "outer" visitor, and it'd be fine. This reduces the amount of + // state and attribute invalidations, and we need to check the outer + // selector to the left anyway to avoid over-invalidation, so it + // avoids matching it twice uselessly. + let mut iter = selector.iter(); + let mut index = 0; + + for ss in &mut iter { + if !ss.visit(self) { + return false; + } + index += 1; + } + + let combinator = iter.next_sequence(); + if combinator.is_none() { + continue; + } + + index += 1; // account for the combinator. + + self.parent_selectors + .push((self.selector.clone(), self.compound_state.offset)); + let mut nested = SelectorDependencyCollector { + map: &mut *self.map, + document_state: &mut *self.document_state, + selector, + parent_selectors: &mut *self.parent_selectors, + quirks_mode: self.quirks_mode, + compound_state: PerCompoundState::new(index), + alloc_error: &mut *self.alloc_error, + }; + if !nested.visit_whole_selector_from(iter, index) { + return false; + } + self.parent_selectors.pop(); + } + true + } + + fn visit_simple_selector(&mut self, s: &Component) -> bool { + use crate::selector_parser::NonTSPseudoClass; + + match *s { + Component::ID(ref atom) | Component::Class(ref atom) => { + let dependency = self.dependency(); + let map = match *s { + Component::ID(..) => &mut self.map.id_to_selector, + Component::Class(..) => &mut self.map.class_to_selector, + _ => unreachable!(), + }; + let entry = match map.try_entry(atom.0.clone(), self.quirks_mode) { + Ok(entry) => entry, + Err(err) => { + *self.alloc_error = Some(err.into()); + return false; + }, + }; + let vec = entry.or_insert_with(SmallVec::new); + if let Err(err) = vec.try_reserve(1) { + *self.alloc_error = Some(err.into()); + return false; + } + vec.push(dependency); + true + }, + Component::NonTSPseudoClass(ref pc) => { + self.compound_state.element_state |= pc.state_flag(); + *self.document_state |= pc.document_state_flag(); + + let attr_name = match *pc { + #[cfg(feature = "gecko")] + NonTSPseudoClass::MozTableBorderNonzero => local_name!("border"), + #[cfg(feature = "gecko")] + NonTSPseudoClass::MozBrowserFrame => local_name!("mozbrowser"), + #[cfg(feature = "gecko")] + NonTSPseudoClass::MozSelectListBox => { + // This depends on two attributes. + return self.add_attr_dependency(local_name!("multiple")) && + self.add_attr_dependency(local_name!("size")); + }, + NonTSPseudoClass::Lang(..) => local_name!("lang"), + _ => return true, + }; + + self.add_attr_dependency(attr_name) + }, + _ => true, + } + } + + fn visit_attribute_selector( + &mut self, + _: &NamespaceConstraint<&Namespace>, + local_name: &LocalName, + local_name_lower: &LocalName, + ) -> bool { + if !self.add_attr_dependency(local_name.clone()) { + return false; + } + + if local_name != local_name_lower && !self.add_attr_dependency(local_name_lower.clone()) { + return false; + } + + true + } +} diff --git a/servo/components/style/invalidation/element/invalidator.rs b/servo/components/style/invalidation/element/invalidator.rs new file mode 100644 index 0000000000..00f714c5b1 --- /dev/null +++ b/servo/components/style/invalidation/element/invalidator.rs @@ -0,0 +1,1017 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! The struct that takes care of encapsulating all the logic on where and how +//! element styles need to be invalidated. + +use crate::context::StackLimitChecker; +use crate::dom::{TElement, TNode, TShadowRoot}; +use crate::invalidation::element::invalidation_map::{Dependency, DependencyInvalidationKind}; +use selectors::matching::matches_compound_selector_from; +use selectors::matching::{CompoundSelectorMatchingResult, MatchingContext}; +use selectors::parser::{Combinator, Component}; +use selectors::OpaqueElement; +use smallvec::SmallVec; +use std::fmt; +use std::fmt::Write; + +/// A trait to abstract the collection of invalidations for a given pass. +pub trait InvalidationProcessor<'a, E> +where + E: TElement, +{ + /// Whether an invalidation that contains only a pseudo-element selector + /// like ::before or ::after triggers invalidation of the element that would + /// originate it. + fn invalidates_on_pseudo_element(&self) -> bool { + false + } + + /// Whether the invalidation processor only cares about light-tree + /// descendants of a given element, that is, doesn't invalidate + /// pseudo-elements, NAC, shadow dom... + fn light_tree_only(&self) -> bool { + false + } + + /// When a dependency from a :where or :is selector matches, it may still be + /// the case that we don't need to invalidate the full style. Consider the + /// case of: + /// + /// div .foo:where(.bar *, .baz) .qux + /// + /// We can get to the `*` part after a .bar class change, but you only need + /// to restyle the element if it also matches .foo. + /// + /// Similarly, you only need to restyle .baz if the whole result of matching + /// the selector changes. + /// + /// This function is called to check the result of matching the "outer" + /// dependency that we generate for the parent of the `:where` selector, + /// that is, in the case above it should match + /// `div .foo:where(.bar *, .baz)`. + /// + /// Returning true unconditionally here is over-optimistic and may + /// over-invalidate. + fn check_outer_dependency(&mut self, dependency: &Dependency, element: E) -> bool; + + /// The matching context that should be used to process invalidations. + fn matching_context(&mut self) -> &mut MatchingContext<'a, E::Impl>; + + /// Collect invalidations for a given element's descendants and siblings. + /// + /// Returns whether the element itself was invalidated. + fn collect_invalidations( + &mut self, + element: E, + self_invalidations: &mut InvalidationVector<'a>, + descendant_invalidations: &mut DescendantInvalidationLists<'a>, + sibling_invalidations: &mut InvalidationVector<'a>, + ) -> bool; + + /// Returns whether the invalidation process should process the descendants + /// of the given element. + fn should_process_descendants(&mut self, element: E) -> bool; + + /// Executes an arbitrary action when the recursion limit is exceded (if + /// any). + fn recursion_limit_exceeded(&mut self, element: E); + + /// Executes an action when `Self` is invalidated. + fn invalidated_self(&mut self, element: E); + + /// Executes an action when `sibling` is invalidated as a sibling of + /// `of`. + fn invalidated_sibling(&mut self, sibling: E, of: E); + + /// Executes an action when any descendant of `Self` is invalidated. + fn invalidated_descendants(&mut self, element: E, child: E); +} + +/// Different invalidation lists for descendants. +#[derive(Debug, Default)] +pub struct DescendantInvalidationLists<'a> { + /// Invalidations for normal DOM children and pseudo-elements. + /// + /// TODO(emilio): Having a list of invalidations just for pseudo-elements + /// may save some work here and there. + pub dom_descendants: InvalidationVector<'a>, + /// Invalidations for slotted children of an element. + pub slotted_descendants: InvalidationVector<'a>, + /// Invalidations for ::part()s of an element. + pub parts: InvalidationVector<'a>, +} + +impl<'a> DescendantInvalidationLists<'a> { + fn is_empty(&self) -> bool { + self.dom_descendants.is_empty() && + self.slotted_descendants.is_empty() && + self.parts.is_empty() + } +} + +/// The struct that takes care of encapsulating all the logic on where and how +/// element styles need to be invalidated. +pub struct TreeStyleInvalidator<'a, 'b, E, P: 'a> +where + 'b: 'a, + E: TElement, + P: InvalidationProcessor<'b, E>, +{ + element: E, + stack_limit_checker: Option<&'a StackLimitChecker>, + processor: &'a mut P, + _marker: ::std::marker::PhantomData<&'b ()>, +} + +/// A vector of invalidations, optimized for small invalidation sets. +pub type InvalidationVector<'a> = SmallVec<[Invalidation<'a>; 10]>; + +/// The kind of descendant invalidation we're processing. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum DescendantInvalidationKind { + /// A DOM descendant invalidation. + Dom, + /// A ::slotted() descendant invalidation. + Slotted, + /// A ::part() descendant invalidation. + Part, +} + +/// The kind of invalidation we're processing. +/// +/// We can use this to avoid pushing invalidations of the same kind to our +/// descendants or siblings. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum InvalidationKind { + Descendant(DescendantInvalidationKind), + Sibling, +} + +/// An `Invalidation` is a complex selector that describes which elements, +/// relative to a current element we are processing, must be restyled. +#[derive(Clone)] +pub struct Invalidation<'a> { + /// The dependency that generated this invalidation. + /// + /// Note that the offset inside the dependency is not really useful after + /// construction. + dependency: &'a Dependency, + /// The right shadow host from where the rule came from, if any. + /// + /// This is needed to ensure that we match the selector with the right + /// state, as whether some selectors like :host and ::part() match depends + /// on it. + scope: Option, + /// The offset of the selector pointing to a compound selector. + /// + /// This order is a "parse order" offset, that is, zero is the leftmost part + /// of the selector written as parsed / serialized. + /// + /// It is initialized from the offset from `dependency`. + offset: usize, + /// Whether the invalidation was already matched by any previous sibling or + /// ancestor. + /// + /// If this is the case, we can avoid pushing invalidations generated by + /// this one if the generated invalidation is effective for all the siblings + /// or descendants after us. + matched_by_any_previous: bool, +} + +impl<'a> Invalidation<'a> { + /// Create a new invalidation for matching a dependency. + pub fn new(dependency: &'a Dependency, scope: Option) -> Self { + debug_assert!( + dependency.selector_offset == dependency.selector.len() + 1 || + dependency.invalidation_kind() != DependencyInvalidationKind::Element, + "No point to this, if the dependency matched the element we should just invalidate it" + ); + Self { + dependency, + scope, + // + 1 to go past the combinator. + offset: dependency.selector.len() + 1 - dependency.selector_offset, + matched_by_any_previous: false, + } + } + + /// Whether this invalidation is effective for the next sibling or + /// descendant after us. + fn effective_for_next(&self) -> bool { + if self.offset == 0 { + return true; + } + + // TODO(emilio): For pseudo-elements this should be mostly false, except + // for the weird pseudos in . + // + // We should be able to do better here! + match self + .dependency + .selector + .combinator_at_parse_order(self.offset - 1) + { + Combinator::Descendant | Combinator::LaterSibling | Combinator::PseudoElement => true, + Combinator::Part | + Combinator::SlotAssignment | + Combinator::NextSibling | + Combinator::Child => false, + } + } + + fn kind(&self) -> InvalidationKind { + if self.offset == 0 { + return InvalidationKind::Descendant(DescendantInvalidationKind::Dom); + } + + match self + .dependency + .selector + .combinator_at_parse_order(self.offset - 1) + { + Combinator::Child | Combinator::Descendant | Combinator::PseudoElement => { + InvalidationKind::Descendant(DescendantInvalidationKind::Dom) + }, + Combinator::Part => InvalidationKind::Descendant(DescendantInvalidationKind::Part), + Combinator::SlotAssignment => { + InvalidationKind::Descendant(DescendantInvalidationKind::Slotted) + }, + Combinator::NextSibling | Combinator::LaterSibling => InvalidationKind::Sibling, + } + } +} + +impl<'a> fmt::Debug for Invalidation<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use cssparser::ToCss; + + f.write_str("Invalidation(")?; + for component in self + .dependency + .selector + .iter_raw_parse_order_from(self.offset) + { + if matches!(*component, Component::Combinator(..)) { + break; + } + component.to_css(f)?; + } + f.write_char(')') + } +} + +/// The result of processing a single invalidation for a given element. +struct SingleInvalidationResult { + /// Whether the element itself was invalidated. + invalidated_self: bool, + /// Whether the invalidation matched, either invalidating the element or + /// generating another invalidation. + matched: bool, +} + +/// The result of a whole invalidation process for a given element. +pub struct InvalidationResult { + /// Whether the element itself was invalidated. + invalidated_self: bool, + /// Whether the element's descendants were invalidated. + invalidated_descendants: bool, + /// Whether the element's siblings were invalidated. + invalidated_siblings: bool, +} + +impl InvalidationResult { + /// Create an emtpy result. + pub fn empty() -> Self { + Self { + invalidated_self: false, + invalidated_descendants: false, + invalidated_siblings: false, + } + } + + /// Whether the invalidation has invalidate the element itself. + pub fn has_invalidated_self(&self) -> bool { + self.invalidated_self + } + + /// Whether the invalidation has invalidate desendants. + pub fn has_invalidated_descendants(&self) -> bool { + self.invalidated_descendants + } + + /// Whether the invalidation has invalidate siblings. + pub fn has_invalidated_siblings(&self) -> bool { + self.invalidated_siblings + } +} + +impl<'a, 'b, E, P: 'a> TreeStyleInvalidator<'a, 'b, E, P> +where + 'b: 'a, + E: TElement, + P: InvalidationProcessor<'b, E>, +{ + /// Trivially constructs a new `TreeStyleInvalidator`. + pub fn new( + element: E, + stack_limit_checker: Option<&'a StackLimitChecker>, + processor: &'a mut P, + ) -> Self { + Self { + element, + stack_limit_checker, + processor, + _marker: ::std::marker::PhantomData, + } + } + + /// Perform the invalidation pass. + pub fn invalidate(mut self) -> InvalidationResult { + debug!("StyleTreeInvalidator::invalidate({:?})", self.element); + + let mut self_invalidations = InvalidationVector::new(); + let mut descendant_invalidations = DescendantInvalidationLists::default(); + let mut sibling_invalidations = InvalidationVector::new(); + + let mut invalidated_self = self.processor.collect_invalidations( + self.element, + &mut self_invalidations, + &mut descendant_invalidations, + &mut sibling_invalidations, + ); + + debug!("Collected invalidations (self: {}): ", invalidated_self); + debug!( + " > self: {}, {:?}", + self_invalidations.len(), + self_invalidations + ); + debug!(" > descendants: {:?}", descendant_invalidations); + debug!( + " > siblings: {}, {:?}", + sibling_invalidations.len(), + sibling_invalidations + ); + + let invalidated_self_from_collection = invalidated_self; + + invalidated_self |= self.process_descendant_invalidations( + &self_invalidations, + &mut descendant_invalidations, + &mut sibling_invalidations, + DescendantInvalidationKind::Dom, + ); + + if invalidated_self && !invalidated_self_from_collection { + self.processor.invalidated_self(self.element); + } + + let invalidated_descendants = self.invalidate_descendants(&descendant_invalidations); + let invalidated_siblings = self.invalidate_siblings(&mut sibling_invalidations); + + InvalidationResult { + invalidated_self, + invalidated_descendants, + invalidated_siblings, + } + } + + /// Go through later DOM siblings, invalidating style as needed using the + /// `sibling_invalidations` list. + /// + /// Returns whether any sibling's style or any sibling descendant's style + /// was invalidated. + fn invalidate_siblings(&mut self, sibling_invalidations: &mut InvalidationVector<'b>) -> bool { + if sibling_invalidations.is_empty() { + return false; + } + + let mut current = self.element.next_sibling_element(); + let mut any_invalidated = false; + + while let Some(sibling) = current { + let mut sibling_invalidator = + TreeStyleInvalidator::new(sibling, self.stack_limit_checker, self.processor); + + let mut invalidations_for_descendants = DescendantInvalidationLists::default(); + let invalidated_sibling = sibling_invalidator.process_sibling_invalidations( + &mut invalidations_for_descendants, + sibling_invalidations, + ); + + if invalidated_sibling { + sibling_invalidator + .processor + .invalidated_sibling(sibling, self.element); + } + + any_invalidated |= invalidated_sibling; + + any_invalidated |= + sibling_invalidator.invalidate_descendants(&invalidations_for_descendants); + + if sibling_invalidations.is_empty() { + break; + } + + current = sibling.next_sibling_element(); + } + + any_invalidated + } + + fn invalidate_pseudo_element_or_nac( + &mut self, + child: E, + invalidations: &[Invalidation<'b>], + ) -> bool { + let mut sibling_invalidations = InvalidationVector::new(); + + let result = self.invalidate_child( + child, + invalidations, + &mut sibling_invalidations, + DescendantInvalidationKind::Dom, + ); + + // Roots of NAC subtrees can indeed generate sibling invalidations, but + // they can be just ignored, since they have no siblings. + // + // Note that we can end up testing selectors that wouldn't end up + // matching due to this being NAC, like those coming from document + // rules, but we overinvalidate instead of checking this. + + result + } + + /// Invalidate a child and recurse down invalidating its descendants if + /// needed. + fn invalidate_child( + &mut self, + child: E, + invalidations: &[Invalidation<'b>], + sibling_invalidations: &mut InvalidationVector<'b>, + descendant_invalidation_kind: DescendantInvalidationKind, + ) -> bool { + let mut invalidations_for_descendants = DescendantInvalidationLists::default(); + + let mut invalidated_child = false; + let invalidated_descendants = { + let mut child_invalidator = + TreeStyleInvalidator::new(child, self.stack_limit_checker, self.processor); + + invalidated_child |= child_invalidator.process_sibling_invalidations( + &mut invalidations_for_descendants, + sibling_invalidations, + ); + + invalidated_child |= child_invalidator.process_descendant_invalidations( + invalidations, + &mut invalidations_for_descendants, + sibling_invalidations, + descendant_invalidation_kind, + ); + + if invalidated_child { + child_invalidator.processor.invalidated_self(child); + } + + child_invalidator.invalidate_descendants(&invalidations_for_descendants) + }; + + // The child may not be a flattened tree child of the current element, + // but may be arbitrarily deep. + // + // Since we keep the traversal flags in terms of the flattened tree, + // we need to propagate it as appropriate. + if invalidated_child || invalidated_descendants { + self.processor.invalidated_descendants(self.element, child); + } + + invalidated_child || invalidated_descendants + } + + fn invalidate_nac(&mut self, invalidations: &[Invalidation<'b>]) -> bool { + let mut any_nac_root = false; + + let element = self.element; + element.each_anonymous_content_child(|nac| { + any_nac_root |= self.invalidate_pseudo_element_or_nac(nac, invalidations); + }); + + any_nac_root + } + + // NB: It's important that this operates on DOM children, which is what + // selector-matching operates on. + fn invalidate_dom_descendants_of( + &mut self, + parent: E::ConcreteNode, + invalidations: &[Invalidation<'b>], + ) -> bool { + let mut any_descendant = false; + + let mut sibling_invalidations = InvalidationVector::new(); + for child in parent.dom_children() { + let child = match child.as_element() { + Some(e) => e, + None => continue, + }; + + any_descendant |= self.invalidate_child( + child, + invalidations, + &mut sibling_invalidations, + DescendantInvalidationKind::Dom, + ); + } + + any_descendant + } + + fn invalidate_parts_in_shadow_tree( + &mut self, + shadow: ::ConcreteShadowRoot, + invalidations: &[Invalidation<'b>], + ) -> bool { + debug_assert!(!invalidations.is_empty()); + + let mut any = false; + let mut sibling_invalidations = InvalidationVector::new(); + + for node in shadow.as_node().dom_descendants() { + let element = match node.as_element() { + Some(e) => e, + None => continue, + }; + + if element.has_part_attr() { + any |= self.invalidate_child( + element, + invalidations, + &mut sibling_invalidations, + DescendantInvalidationKind::Part, + ); + debug_assert!( + sibling_invalidations.is_empty(), + "::part() shouldn't have sibling combinators to the right, \ + this makes no sense! {:?}", + sibling_invalidations + ); + } + + if let Some(shadow) = element.shadow_root() { + if element.exports_any_part() { + any |= self.invalidate_parts_in_shadow_tree(shadow, invalidations) + } + } + } + + any + } + + fn invalidate_parts(&mut self, invalidations: &[Invalidation<'b>]) -> bool { + if invalidations.is_empty() { + return false; + } + + let shadow = match self.element.shadow_root() { + Some(s) => s, + None => return false, + }; + + self.invalidate_parts_in_shadow_tree(shadow, invalidations) + } + + fn invalidate_slotted_elements(&mut self, invalidations: &[Invalidation<'b>]) -> bool { + if invalidations.is_empty() { + return false; + } + + let slot = self.element; + self.invalidate_slotted_elements_in_slot(slot, invalidations) + } + + fn invalidate_slotted_elements_in_slot( + &mut self, + slot: E, + invalidations: &[Invalidation<'b>], + ) -> bool { + let mut any = false; + + let mut sibling_invalidations = InvalidationVector::new(); + for node in slot.slotted_nodes() { + let element = match node.as_element() { + Some(e) => e, + None => continue, + }; + + if element.is_html_slot_element() { + any |= self.invalidate_slotted_elements_in_slot(element, invalidations); + } else { + any |= self.invalidate_child( + element, + invalidations, + &mut sibling_invalidations, + DescendantInvalidationKind::Slotted, + ); + } + + debug_assert!( + sibling_invalidations.is_empty(), + "::slotted() shouldn't have sibling combinators to the right, \ + this makes no sense! {:?}", + sibling_invalidations + ); + } + + any + } + + fn invalidate_non_slotted_descendants(&mut self, invalidations: &[Invalidation<'b>]) -> bool { + if invalidations.is_empty() { + return false; + } + + if self.processor.light_tree_only() { + let node = self.element.as_node(); + return self.invalidate_dom_descendants_of(node, invalidations); + } + + let mut any_descendant = false; + + // NOTE(emilio): This is only needed for Shadow DOM to invalidate + // correctly on :host(..) changes. Instead of doing this, we could add + // a third kind of invalidation list that walks shadow root children, + // but it's not clear it's worth it. + // + // Also, it's needed as of right now for document state invalidation, + // where we rely on iterating every element that ends up in the composed + // doc, but we could fix that invalidating per subtree. + if let Some(root) = self.element.shadow_root() { + any_descendant |= self.invalidate_dom_descendants_of(root.as_node(), invalidations); + } + + if let Some(marker) = self.element.marker_pseudo_element() { + any_descendant |= self.invalidate_pseudo_element_or_nac(marker, invalidations); + } + + if let Some(before) = self.element.before_pseudo_element() { + any_descendant |= self.invalidate_pseudo_element_or_nac(before, invalidations); + } + + let node = self.element.as_node(); + any_descendant |= self.invalidate_dom_descendants_of(node, invalidations); + + if let Some(after) = self.element.after_pseudo_element() { + any_descendant |= self.invalidate_pseudo_element_or_nac(after, invalidations); + } + + any_descendant |= self.invalidate_nac(invalidations); + + any_descendant + } + + /// Given the descendant invalidation lists, go through the current + /// element's descendants, and invalidate style on them. + fn invalidate_descendants(&mut self, invalidations: &DescendantInvalidationLists<'b>) -> bool { + if invalidations.is_empty() { + return false; + } + + debug!( + "StyleTreeInvalidator::invalidate_descendants({:?})", + self.element + ); + debug!(" > {:?}", invalidations); + + let should_process = self.processor.should_process_descendants(self.element); + + if !should_process { + return false; + } + + if let Some(checker) = self.stack_limit_checker { + if checker.limit_exceeded() { + self.processor.recursion_limit_exceeded(self.element); + return true; + } + } + + let mut any_descendant = false; + + any_descendant |= self.invalidate_non_slotted_descendants(&invalidations.dom_descendants); + any_descendant |= self.invalidate_slotted_elements(&invalidations.slotted_descendants); + any_descendant |= self.invalidate_parts(&invalidations.parts); + + any_descendant + } + + /// Process the given sibling invalidations coming from our previous + /// sibling. + /// + /// The sibling invalidations are somewhat special because they can be + /// modified on the fly. New invalidations may be added and removed. + /// + /// In particular, all descendants get the same set of invalidations from + /// the parent, but the invalidations from a given sibling depend on the + /// ones we got from the previous one. + /// + /// Returns whether invalidated the current element's style. + fn process_sibling_invalidations( + &mut self, + descendant_invalidations: &mut DescendantInvalidationLists<'b>, + sibling_invalidations: &mut InvalidationVector<'b>, + ) -> bool { + let mut i = 0; + let mut new_sibling_invalidations = InvalidationVector::new(); + let mut invalidated_self = false; + + while i < sibling_invalidations.len() { + let result = self.process_invalidation( + &sibling_invalidations[i], + descendant_invalidations, + &mut new_sibling_invalidations, + InvalidationKind::Sibling, + ); + + invalidated_self |= result.invalidated_self; + sibling_invalidations[i].matched_by_any_previous |= result.matched; + if sibling_invalidations[i].effective_for_next() { + i += 1; + } else { + sibling_invalidations.remove(i); + } + } + + sibling_invalidations.extend(new_sibling_invalidations.drain(..)); + invalidated_self + } + + /// Process a given invalidation list coming from our parent, + /// adding to `descendant_invalidations` and `sibling_invalidations` as + /// needed. + /// + /// Returns whether our style was invalidated as a result. + fn process_descendant_invalidations( + &mut self, + invalidations: &[Invalidation<'b>], + descendant_invalidations: &mut DescendantInvalidationLists<'b>, + sibling_invalidations: &mut InvalidationVector<'b>, + descendant_invalidation_kind: DescendantInvalidationKind, + ) -> bool { + let mut invalidated = false; + + for invalidation in invalidations { + let result = self.process_invalidation( + invalidation, + descendant_invalidations, + sibling_invalidations, + InvalidationKind::Descendant(descendant_invalidation_kind), + ); + + invalidated |= result.invalidated_self; + if invalidation.effective_for_next() { + let mut invalidation = invalidation.clone(); + invalidation.matched_by_any_previous |= result.matched; + debug_assert_eq!( + descendant_invalidation_kind, + DescendantInvalidationKind::Dom, + "Slotted or part invalidations don't propagate." + ); + descendant_invalidations.dom_descendants.push(invalidation); + } + } + + invalidated + } + + /// Processes a given invalidation, potentially invalidating the style of + /// the current element. + /// + /// Returns whether invalidated the style of the element, and whether the + /// invalidation should be effective to subsequent siblings or descendants + /// down in the tree. + fn process_invalidation( + &mut self, + invalidation: &Invalidation<'b>, + descendant_invalidations: &mut DescendantInvalidationLists<'b>, + sibling_invalidations: &mut InvalidationVector<'b>, + invalidation_kind: InvalidationKind, + ) -> SingleInvalidationResult { + debug!( + "TreeStyleInvalidator::process_invalidation({:?}, {:?}, {:?})", + self.element, invalidation, invalidation_kind + ); + + let matching_result = { + let context = self.processor.matching_context(); + context.current_host = invalidation.scope; + + matches_compound_selector_from( + &invalidation.dependency.selector, + invalidation.offset, + context, + &self.element, + ) + }; + + let next_invalidation = match matching_result { + CompoundSelectorMatchingResult::NotMatched => { + return SingleInvalidationResult { + invalidated_self: false, + matched: false, + } + }, + CompoundSelectorMatchingResult::FullyMatched => { + debug!(" > Invalidation matched completely"); + // We matched completely. If we're an inner selector now we need + // to go outside our selector and carry on invalidating. + let mut cur_dependency = invalidation.dependency; + loop { + cur_dependency = match cur_dependency.parent { + None => { + return SingleInvalidationResult { + invalidated_self: true, + matched: true, + } + }, + Some(ref p) => &**p, + }; + + debug!(" > Checking outer dependency {:?}", cur_dependency); + + // The inner selector changed, now check if the full + // previous part of the selector did, before keeping + // checking for descendants. + if !self + .processor + .check_outer_dependency(cur_dependency, self.element) + { + return SingleInvalidationResult { + invalidated_self: false, + matched: false, + }; + } + + if cur_dependency.invalidation_kind() == DependencyInvalidationKind::Element { + continue; + } + + debug!(" > Generating invalidation"); + break Invalidation::new(cur_dependency, invalidation.scope); + } + }, + CompoundSelectorMatchingResult::Matched { + next_combinator_offset, + } => Invalidation { + dependency: invalidation.dependency, + scope: invalidation.scope, + offset: next_combinator_offset + 1, + matched_by_any_previous: false, + }, + }; + + debug_assert_ne!( + next_invalidation.offset, 0, + "Rightmost selectors shouldn't generate more invalidations", + ); + + let mut invalidated_self = false; + let next_combinator = next_invalidation + .dependency + .selector + .combinator_at_parse_order(next_invalidation.offset - 1); + + if matches!(next_combinator, Combinator::PseudoElement) && + self.processor.invalidates_on_pseudo_element() + { + // We need to invalidate the element whenever pseudos change, for + // two reasons: + // + // * Eager pseudo styles are stored as part of the originating + // element's computed style. + // + // * Lazy pseudo-styles might be cached on the originating + // element's pseudo-style cache. + // + // This could be more fine-grained (perhaps with a RESTYLE_PSEUDOS + // hint?). + // + // Note that we'll also restyle the pseudo-element because it would + // match this invalidation. + // + // FIXME: For non-element-backed pseudos this is still not quite + // correct. For example for ::selection even though we invalidate + // the style properly there's nothing that triggers a repaint + // necessarily. Though this matches old Gecko behavior, and the + // ::selection implementation needs to change significantly anyway + // to implement https://github.com/w3c/csswg-drafts/issues/2474 for + // example. + invalidated_self = true; + } + + debug!( + " > Invalidation matched, next: {:?}, ({:?})", + next_invalidation, next_combinator + ); + + let next_invalidation_kind = next_invalidation.kind(); + + // We can skip pushing under some circumstances, and we should + // because otherwise the invalidation list could grow + // exponentially. + // + // * First of all, both invalidations need to be of the same + // kind. This is because of how we propagate them going to + // the right of the tree for sibling invalidations and going + // down the tree for children invalidations. A sibling + // invalidation that ends up generating a children + // invalidation ends up (correctly) in five different lists, + // not in the same list five different times. + // + // * Then, the invalidation needs to be matched by a previous + // ancestor/sibling, in order to know that this invalidation + // has been generated already. + // + // * Finally, the new invalidation needs to be + // `effective_for_next()`, in order for us to know that it is + // still in the list, since we remove the dependencies that + // aren't from the lists for our children / siblings. + // + // To go through an example, let's imagine we are processing a + // dom subtree like: + // + //
+ // + // And an invalidation list with a single invalidation like: + // + // [div div div] + // + // When we process the invalidation list for the outer div, we + // match it, and generate a `div div` invalidation, so for the + //
child we have: + // + // [div div div, div div] + // + // With the first of them marked as `matched`. + // + // When we process the
child, we don't match any of + // them, so both invalidations go untouched to our children. + // + // When we process the second
, we match _both_ + // invalidations. + // + // However, when matching the first, we can tell it's been + // matched, and not push the corresponding `div div` + // invalidation, since we know it's necessarily already on the + // list. + // + // Thus, without skipping the push, we'll arrive to the + // innermost
with: + // + // [div div div, div div, div div, div] + // + // While skipping it, we won't arrive here with duplicating + // dependencies: + // + // [div div div, div div, div] + // + let can_skip_pushing = next_invalidation_kind == invalidation_kind && + invalidation.matched_by_any_previous && + next_invalidation.effective_for_next(); + + if can_skip_pushing { + debug!( + " > Can avoid push, since the invalidation had \ + already been matched before" + ); + } else { + match next_invalidation_kind { + InvalidationKind::Descendant(DescendantInvalidationKind::Dom) => { + descendant_invalidations + .dom_descendants + .push(next_invalidation); + }, + InvalidationKind::Descendant(DescendantInvalidationKind::Part) => { + descendant_invalidations.parts.push(next_invalidation); + }, + InvalidationKind::Descendant(DescendantInvalidationKind::Slotted) => { + descendant_invalidations + .slotted_descendants + .push(next_invalidation); + }, + InvalidationKind::Sibling => { + sibling_invalidations.push(next_invalidation); + }, + } + } + + SingleInvalidationResult { + invalidated_self, + matched: true, + } + } +} diff --git a/servo/components/style/invalidation/element/mod.rs b/servo/components/style/invalidation/element/mod.rs new file mode 100644 index 0000000000..1f19cc54f5 --- /dev/null +++ b/servo/components/style/invalidation/element/mod.rs @@ -0,0 +1,12 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Invalidation of element styles due to attribute or style changes. + +pub mod document_state; +pub mod element_wrapper; +pub mod invalidation_map; +pub mod invalidator; +pub mod restyle_hints; +pub mod state_and_attributes; diff --git a/servo/components/style/invalidation/element/restyle_hints.rs b/servo/components/style/invalidation/element/restyle_hints.rs new file mode 100644 index 0000000000..fe89636e88 --- /dev/null +++ b/servo/components/style/invalidation/element/restyle_hints.rs @@ -0,0 +1,191 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Restyle hints: an optimization to avoid unnecessarily matching selectors. + +use crate::traversal_flags::TraversalFlags; + +bitflags! { + /// The kind of restyle we need to do for a given element. + #[repr(C)] + #[derive(Clone, Copy, Debug)] + pub struct RestyleHint: u16 { + /// Do a selector match of the element. + const RESTYLE_SELF = 1 << 0; + + /// Do a selector match of the element's pseudo-elements. Always to be combined with + /// RESTYLE_SELF. + const RESTYLE_PSEUDOS = 1 << 1; + + /// Do a selector match if the element is a pseudo-element. + const RESTYLE_SELF_IF_PSEUDO = 1 << 2; + + /// Do a selector match of the element's descendants. + const RESTYLE_DESCENDANTS = 1 << 3; + + /// Recascade the current element. + const RECASCADE_SELF = 1 << 4; + + /// Recascade the current element if it inherits any reset style. + const RECASCADE_SELF_IF_INHERIT_RESET_STYLE = 1 << 5; + + /// Recascade all descendant elements. + const RECASCADE_DESCENDANTS = 1 << 6; + + /// Replace the style data coming from CSS transitions without updating + /// any other style data. This hint is only processed in animation-only + /// traversal which is prior to normal traversal. + const RESTYLE_CSS_TRANSITIONS = 1 << 7; + + /// Replace the style data coming from CSS animations without updating + /// any other style data. This hint is only processed in animation-only + /// traversal which is prior to normal traversal. + const RESTYLE_CSS_ANIMATIONS = 1 << 8; + + /// Don't re-run selector-matching on the element, only the style + /// attribute has changed, and this change didn't have any other + /// dependencies. + const RESTYLE_STYLE_ATTRIBUTE = 1 << 9; + + /// Replace the style data coming from SMIL animations without updating + /// any other style data. This hint is only processed in animation-only + /// traversal which is prior to normal traversal. + const RESTYLE_SMIL = 1 << 10; + } +} + +impl RestyleHint { + /// Creates a new `RestyleHint` indicating that the current element and all + /// its descendants must be fully restyled. + pub fn restyle_subtree() -> Self { + RestyleHint::RESTYLE_SELF | RestyleHint::RESTYLE_DESCENDANTS + } + + /// Creates a new `RestyleHint` indicating that the current element and all + /// its descendants must be recascaded. + pub fn recascade_subtree() -> Self { + RestyleHint::RECASCADE_SELF | RestyleHint::RECASCADE_DESCENDANTS + } + + /// Returns whether this hint invalidates the element and all its + /// descendants. + pub fn contains_subtree(&self) -> bool { + self.contains(Self::restyle_subtree()) + } + + /// Returns whether we'll recascade all of the descendants. + pub fn will_recascade_subtree(&self) -> bool { + self.contains_subtree() || self.contains(Self::recascade_subtree()) + } + + /// Returns whether we need to restyle this element. + pub fn has_non_animation_invalidations(&self) -> bool { + !(*self & !Self::for_animations()).is_empty() + } + + /// Propagates this restyle hint to a child element. + pub fn propagate(&mut self, traversal_flags: &TraversalFlags) -> Self { + use std::mem; + + // In the middle of an animation only restyle, we don't need to + // propagate any restyle hints, and we need to remove ourselves. + if traversal_flags.for_animation_only() { + self.remove_animation_hints(); + return Self::empty(); + } + + debug_assert!( + !self.has_animation_hint(), + "There should not be any animation restyle hints \ + during normal traversal" + ); + + // Else we should clear ourselves, and return the propagated hint. + mem::replace(self, Self::empty()).propagate_for_non_animation_restyle() + } + + /// Returns a new `RestyleHint` appropriate for children of the current element. + fn propagate_for_non_animation_restyle(&self) -> Self { + if self.contains(RestyleHint::RESTYLE_DESCENDANTS) { + return Self::restyle_subtree(); + } + let mut result = Self::empty(); + if self.contains(RestyleHint::RESTYLE_PSEUDOS) { + result |= Self::RESTYLE_SELF_IF_PSEUDO; + } + if self.contains(RestyleHint::RECASCADE_DESCENDANTS) { + result |= Self::recascade_subtree(); + } + result + } + + /// Returns a hint that contains all the replacement hints. + pub fn replacements() -> Self { + RestyleHint::RESTYLE_STYLE_ATTRIBUTE | Self::for_animations() + } + + /// The replacements for the animation cascade levels. + #[inline] + pub fn for_animations() -> Self { + RestyleHint::RESTYLE_SMIL | + RestyleHint::RESTYLE_CSS_ANIMATIONS | + RestyleHint::RESTYLE_CSS_TRANSITIONS + } + + /// Returns whether the hint specifies that an animation cascade level must + /// be replaced. + #[inline] + pub fn has_animation_hint(&self) -> bool { + self.intersects(Self::for_animations()) + } + + /// Returns whether the hint specifies that an animation cascade level must + /// be replaced. + #[inline] + pub fn has_animation_hint_or_recascade(&self) -> bool { + self.intersects( + Self::for_animations() | + Self::RECASCADE_SELF | + Self::RECASCADE_SELF_IF_INHERIT_RESET_STYLE, + ) + } + + /// Returns whether the hint specifies some restyle work other than an + /// animation cascade level replacement. + #[inline] + pub fn has_non_animation_hint(&self) -> bool { + !(*self & !Self::for_animations()).is_empty() + } + + /// Returns whether the hint specifies that some cascade levels must be + /// replaced. + #[inline] + pub fn has_replacements(&self) -> bool { + self.intersects(Self::replacements()) + } + + /// Removes all of the animation-related hints. + #[inline] + pub fn remove_animation_hints(&mut self) { + self.remove(Self::for_animations()); + + // While RECASCADE_SELF is not animation-specific, we only ever add and process it during + // traversal. If we are here, removing animation hints, then we are in an animation-only + // traversal, and we know that any RECASCADE_SELF flag must have been set due to changes in + // inherited values after restyling for animations, and thus we want to remove it so that + // we don't later try to restyle the element during a normal restyle. + // (We could have separate RECASCADE_SELF_NORMAL and RECASCADE_SELF_ANIMATIONS flags to + // make it clear, but this isn't currently necessary.) + self.remove(Self::RECASCADE_SELF | Self::RECASCADE_SELF_IF_INHERIT_RESET_STYLE); + } +} + +impl Default for RestyleHint { + fn default() -> Self { + Self::empty() + } +} + +#[cfg(feature = "servo")] +malloc_size_of_is_0!(RestyleHint); diff --git a/servo/components/style/invalidation/element/state_and_attributes.rs b/servo/components/style/invalidation/element/state_and_attributes.rs new file mode 100644 index 0000000000..1ead300a87 --- /dev/null +++ b/servo/components/style/invalidation/element/state_and_attributes.rs @@ -0,0 +1,552 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! An invalidation processor for style changes due to state and attribute +//! changes. + +use crate::context::SharedStyleContext; +use crate::data::ElementData; +use crate::dom::{TElement, TNode}; +use crate::invalidation::element::element_wrapper::{ElementSnapshot, ElementWrapper}; +use crate::invalidation::element::invalidation_map::*; +use crate::invalidation::element::invalidator::{DescendantInvalidationLists, InvalidationVector}; +use crate::invalidation::element::invalidator::{Invalidation, InvalidationProcessor}; +use crate::invalidation::element::restyle_hints::RestyleHint; +use crate::selector_map::SelectorMap; +use crate::selector_parser::Snapshot; +use crate::stylesheets::origin::OriginSet; +use crate::{Atom, WeakAtom}; +use dom::ElementState; +use selectors::attr::CaseSensitivity; +use selectors::matching::{ + matches_selector, MatchingContext, MatchingMode, NeedsSelectorFlags, VisitedHandlingMode, +}; +use selectors::NthIndexCache; +use smallvec::SmallVec; + +/// The collector implementation. +struct Collector<'a, 'b: 'a, 'selectors: 'a, E> +where + E: TElement, +{ + element: E, + wrapper: ElementWrapper<'b, E>, + snapshot: &'a Snapshot, + matching_context: &'a mut MatchingContext<'b, E::Impl>, + lookup_element: E, + removed_id: Option<&'a WeakAtom>, + added_id: Option<&'a WeakAtom>, + classes_removed: &'a SmallVec<[Atom; 8]>, + classes_added: &'a SmallVec<[Atom; 8]>, + state_changes: ElementState, + descendant_invalidations: &'a mut DescendantInvalidationLists<'selectors>, + sibling_invalidations: &'a mut InvalidationVector<'selectors>, + invalidates_self: bool, +} + +/// An invalidation processor for style changes due to state and attribute +/// changes. +pub struct StateAndAttrInvalidationProcessor<'a, 'b: 'a, E: TElement> { + shared_context: &'a SharedStyleContext<'b>, + element: E, + data: &'a mut ElementData, + matching_context: MatchingContext<'a, E::Impl>, +} + +impl<'a, 'b: 'a, E: TElement + 'b> StateAndAttrInvalidationProcessor<'a, 'b, E> { + /// Creates a new StateAndAttrInvalidationProcessor. + pub fn new( + shared_context: &'a SharedStyleContext<'b>, + element: E, + data: &'a mut ElementData, + nth_index_cache: &'a mut NthIndexCache, + ) -> Self { + let matching_context = MatchingContext::new_for_visited( + MatchingMode::Normal, + None, + nth_index_cache, + VisitedHandlingMode::AllLinksVisitedAndUnvisited, + shared_context.quirks_mode(), + NeedsSelectorFlags::No, + ); + + Self { + shared_context, + element, + data, + matching_context, + } + } +} + +/// Checks a dependency against a given element and wrapper, to see if something +/// changed. +pub fn check_dependency( + dependency: &Dependency, + element: &E, + wrapper: &W, + context: &mut MatchingContext<'_, E::Impl>, +) -> bool +where + E: TElement, + W: selectors::Element, +{ + let matches_now = matches_selector( + &dependency.selector, + dependency.selector_offset, + None, + element, + context, + ); + + let matched_then = matches_selector( + &dependency.selector, + dependency.selector_offset, + None, + wrapper, + context, + ); + + matched_then != matches_now +} + +/// Whether we should process the descendants of a given element for style +/// invalidation. +pub fn should_process_descendants(data: &ElementData) -> bool { + !data.styles.is_display_none() && !data.hint.contains(RestyleHint::RESTYLE_DESCENDANTS) +} + +/// Propagates the bits after invalidating a descendant child. +pub fn propagate_dirty_bit_up_to(ancestor: E, child: E) +where + E: TElement, +{ + // The child may not be a flattened tree child of the current element, + // but may be arbitrarily deep. + // + // Since we keep the traversal flags in terms of the flattened tree, + // we need to propagate it as appropriate. + let mut current = child.traversal_parent(); + while let Some(parent) = current.take() { + unsafe { parent.set_dirty_descendants() }; + current = parent.traversal_parent(); + + if parent == ancestor { + return; + } + } + debug_assert!( + false, + "Should've found {:?} as an ancestor of {:?}", + ancestor, child + ); +} + +/// Propagates the bits after invalidating a descendant child, if needed. +pub fn invalidated_descendants(element: E, child: E) +where + E: TElement, +{ + if !child.has_data() { + return; + } + propagate_dirty_bit_up_to(element, child) +} + +/// Sets the appropriate restyle hint after invalidating the style of a given +/// element. +pub fn invalidated_self(element: E) -> bool +where + E: TElement, +{ + let mut data = match element.mutate_data() { + Some(data) => data, + None => return false, + }; + data.hint.insert(RestyleHint::RESTYLE_SELF); + true +} + +/// Sets the appropriate hint after invalidating the style of a sibling. +pub fn invalidated_sibling(element: E, of: E) +where + E: TElement, +{ + debug_assert_eq!( + element.as_node().parent_node(), + of.as_node().parent_node(), + "Should be siblings" + ); + if !invalidated_self(element) { + return; + } + if element.traversal_parent() != of.traversal_parent() { + let parent = element.as_node().parent_element_or_host(); + debug_assert!( + parent.is_some(), + "How can we have siblings without parent nodes?" + ); + if let Some(e) = parent { + propagate_dirty_bit_up_to(e, element) + } + } +} + +impl<'a, 'b: 'a, E: 'a> InvalidationProcessor<'a, E> + for StateAndAttrInvalidationProcessor<'a, 'b, E> +where + E: TElement, +{ + /// We need to invalidate style on pseudo-elements, in order to process + /// changes that could otherwise end up in ::before or ::after content being + /// generated, and invalidate lazy pseudo caches. + fn invalidates_on_pseudo_element(&self) -> bool { + true + } + + fn check_outer_dependency(&mut self, dependency: &Dependency, element: E) -> bool { + // We cannot assert about `element` having a snapshot here (in fact it + // most likely won't), because it may be an arbitrary descendant or + // later-sibling of the element we started invalidating with. + let wrapper = ElementWrapper::new(element, &*self.shared_context.snapshot_map); + check_dependency(dependency, &element, &wrapper, &mut self.matching_context) + } + + fn matching_context(&mut self) -> &mut MatchingContext<'a, E::Impl> { + &mut self.matching_context + } + + fn collect_invalidations( + &mut self, + element: E, + _self_invalidations: &mut InvalidationVector<'a>, + descendant_invalidations: &mut DescendantInvalidationLists<'a>, + sibling_invalidations: &mut InvalidationVector<'a>, + ) -> bool { + debug_assert_eq!(element, self.element); + debug_assert!(element.has_snapshot(), "Why bothering?"); + + let wrapper = ElementWrapper::new(element, &*self.shared_context.snapshot_map); + + let state_changes = wrapper.state_changes(); + let snapshot = wrapper.snapshot().expect("has_snapshot lied"); + + if !snapshot.has_attrs() && state_changes.is_empty() { + return false; + } + + let mut classes_removed = SmallVec::<[Atom; 8]>::new(); + let mut classes_added = SmallVec::<[Atom; 8]>::new(); + if snapshot.class_changed() { + // TODO(emilio): Do this more efficiently! + snapshot.each_class(|c| { + if !element.has_class(c, CaseSensitivity::CaseSensitive) { + classes_removed.push(c.0.clone()) + } + }); + + element.each_class(|c| { + if !snapshot.has_class(c, CaseSensitivity::CaseSensitive) { + classes_added.push(c.0.clone()) + } + }) + } + + let mut id_removed = None; + let mut id_added = None; + if snapshot.id_changed() { + let old_id = snapshot.id_attr(); + let current_id = element.id(); + + if old_id != current_id { + id_removed = old_id; + id_added = current_id; + } + } + + if log_enabled!(::log::Level::Debug) { + debug!("Collecting changes for: {:?}", element); + if !state_changes.is_empty() { + debug!(" > state: {:?}", state_changes); + } + if snapshot.id_changed() { + debug!(" > id changed: +{:?} -{:?}", id_added, id_removed); + } + if snapshot.class_changed() { + debug!( + " > class changed: +{:?} -{:?}", + classes_added, classes_removed + ); + } + let mut attributes_changed = false; + snapshot.each_attr_changed(|_| { + attributes_changed = true; + }); + if attributes_changed { + debug!( + " > attributes changed, old: {}", + snapshot.debug_list_attributes() + ) + } + } + + let lookup_element = if element.implemented_pseudo_element().is_some() { + element.pseudo_element_originating_element().unwrap() + } else { + element + }; + + let mut shadow_rule_datas = SmallVec::<[_; 3]>::new(); + let matches_document_author_rules = + element.each_applicable_non_document_style_rule_data(|data, host| { + shadow_rule_datas.push((data, host.opaque())) + }); + + let invalidated_self = { + let mut collector = Collector { + wrapper, + lookup_element, + state_changes, + element, + snapshot: &snapshot, + matching_context: &mut self.matching_context, + removed_id: id_removed, + added_id: id_added, + classes_removed: &classes_removed, + classes_added: &classes_added, + descendant_invalidations, + sibling_invalidations, + invalidates_self: false, + }; + + let document_origins = if !matches_document_author_rules { + OriginSet::ORIGIN_USER_AGENT | OriginSet::ORIGIN_USER + } else { + OriginSet::all() + }; + + for (cascade_data, origin) in self.shared_context.stylist.iter_origins() { + if document_origins.contains(origin.into()) { + collector + .collect_dependencies_in_invalidation_map(cascade_data.invalidation_map()); + } + } + + for &(ref data, ref host) in &shadow_rule_datas { + collector.matching_context.current_host = Some(host.clone()); + collector.collect_dependencies_in_invalidation_map(data.invalidation_map()); + } + + collector.invalidates_self + }; + + // If we generated a ton of descendant invalidations, it's probably not + // worth to go ahead and try to process them. + // + // Just restyle the descendants directly. + // + // This number is completely made-up, but the page that made us add this + // code generated 1960+ invalidations (bug 1420741). + // + // We don't look at slotted_descendants because those don't propagate + // down more than one level anyway. + if descendant_invalidations.dom_descendants.len() > 150 { + self.data.hint.insert(RestyleHint::RESTYLE_DESCENDANTS); + } + + if invalidated_self { + self.data.hint.insert(RestyleHint::RESTYLE_SELF); + } + + invalidated_self + } + + fn should_process_descendants(&mut self, element: E) -> bool { + if element == self.element { + return should_process_descendants(&self.data); + } + + match element.borrow_data() { + Some(d) => should_process_descendants(&d), + None => return false, + } + } + + fn recursion_limit_exceeded(&mut self, element: E) { + if element == self.element { + self.data.hint.insert(RestyleHint::RESTYLE_DESCENDANTS); + return; + } + + if let Some(mut data) = element.mutate_data() { + data.hint.insert(RestyleHint::RESTYLE_DESCENDANTS); + } + } + + fn invalidated_descendants(&mut self, element: E, child: E) { + invalidated_descendants(element, child) + } + + fn invalidated_self(&mut self, element: E) { + debug_assert_ne!(element, self.element); + invalidated_self(element); + } + + fn invalidated_sibling(&mut self, element: E, of: E) { + debug_assert_ne!(element, self.element); + invalidated_sibling(element, of); + } +} + +impl<'a, 'b, 'selectors, E> Collector<'a, 'b, 'selectors, E> +where + E: TElement, + 'selectors: 'a, +{ + fn collect_dependencies_in_invalidation_map(&mut self, map: &'selectors InvalidationMap) { + let quirks_mode = self.matching_context.quirks_mode(); + let removed_id = self.removed_id; + if let Some(ref id) = removed_id { + if let Some(deps) = map.id_to_selector.get(id, quirks_mode) { + for dep in deps { + self.scan_dependency(dep); + } + } + } + + let added_id = self.added_id; + if let Some(ref id) = added_id { + if let Some(deps) = map.id_to_selector.get(id, quirks_mode) { + for dep in deps { + self.scan_dependency(dep); + } + } + } + + for class in self.classes_added.iter().chain(self.classes_removed.iter()) { + if let Some(deps) = map.class_to_selector.get(class, quirks_mode) { + for dep in deps { + self.scan_dependency(dep); + } + } + } + + self.snapshot.each_attr_changed(|attribute| { + if let Some(deps) = map.other_attribute_affecting_selectors.get(attribute) { + for dep in deps { + self.scan_dependency(dep); + } + } + }); + + self.collect_state_dependencies(&map.state_affecting_selectors) + } + + fn collect_state_dependencies(&mut self, map: &'selectors SelectorMap) { + if self.state_changes.is_empty() { + return; + } + map.lookup_with_additional( + self.lookup_element, + self.matching_context.quirks_mode(), + self.removed_id, + self.classes_removed, + self.state_changes, + |dependency| { + if !dependency.state.intersects(self.state_changes) { + return true; + } + self.scan_dependency(&dependency.dep); + true + }, + ); + } + + /// Check whether a dependency should be taken into account. + #[inline] + fn check_dependency(&mut self, dependency: &Dependency) -> bool { + check_dependency( + dependency, + &self.element, + &self.wrapper, + &mut self.matching_context, + ) + } + + fn scan_dependency(&mut self, dependency: &'selectors Dependency) { + debug!( + "TreeStyleInvalidator::scan_dependency({:?}, {:?})", + self.element, dependency + ); + + if !self.dependency_may_be_relevant(dependency) { + return; + } + + if self.check_dependency(dependency) { + return self.note_dependency(dependency); + } + } + + fn note_dependency(&mut self, dependency: &'selectors Dependency) { + debug_assert!(self.dependency_may_be_relevant(dependency)); + + let invalidation_kind = dependency.invalidation_kind(); + if matches!(invalidation_kind, DependencyInvalidationKind::Element) { + if let Some(ref parent) = dependency.parent { + // We know something changed in the inner selector, go outwards + // now. + self.scan_dependency(parent); + } else { + self.invalidates_self = true; + } + return; + } + + debug_assert_ne!(dependency.selector_offset, 0); + debug_assert_ne!(dependency.selector_offset, dependency.selector.len()); + + let invalidation = + Invalidation::new(&dependency, self.matching_context.current_host.clone()); + + match invalidation_kind { + DependencyInvalidationKind::Element => unreachable!(), + DependencyInvalidationKind::ElementAndDescendants => { + self.invalidates_self = true; + self.descendant_invalidations + .dom_descendants + .push(invalidation); + }, + DependencyInvalidationKind::Descendants => { + self.descendant_invalidations + .dom_descendants + .push(invalidation); + }, + DependencyInvalidationKind::Siblings => { + self.sibling_invalidations.push(invalidation); + }, + DependencyInvalidationKind::Parts => { + self.descendant_invalidations.parts.push(invalidation); + }, + DependencyInvalidationKind::SlottedElements => { + self.descendant_invalidations + .slotted_descendants + .push(invalidation); + }, + } + } + + /// Returns whether `dependency` may cause us to invalidate the style of + /// more elements than what we've already invalidated. + fn dependency_may_be_relevant(&self, dependency: &Dependency) -> bool { + match dependency.invalidation_kind() { + DependencyInvalidationKind::Element => !self.invalidates_self, + DependencyInvalidationKind::SlottedElements => self.element.is_html_slot_element(), + DependencyInvalidationKind::Parts => self.element.shadow_root().is_some(), + DependencyInvalidationKind::ElementAndDescendants | + DependencyInvalidationKind::Siblings | + DependencyInvalidationKind::Descendants => true, + } + } +} diff --git a/servo/components/style/invalidation/media_queries.rs b/servo/components/style/invalidation/media_queries.rs new file mode 100644 index 0000000000..6928b29d3d --- /dev/null +++ b/servo/components/style/invalidation/media_queries.rs @@ -0,0 +1,130 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Code related to the invalidation of media-query-affected rules. + +use crate::context::QuirksMode; +use crate::media_queries::Device; +use crate::shared_lock::SharedRwLockReadGuard; +use crate::stylesheets::{DocumentRule, ImportRule, MediaRule}; +use crate::stylesheets::{NestedRuleIterationCondition, StylesheetContents, SupportsRule}; +use fxhash::FxHashSet; + +/// A key for a given media query result. +/// +/// NOTE: It happens to be the case that all the media lists we care about +/// happen to have a stable address, so we can just use an opaque pointer to +/// represent them. +/// +/// Also, note that right now when a rule or stylesheet is removed, we do a full +/// style flush, so there's no need to worry about other item created with the +/// same pointer address. +/// +/// If this changes, though, we may need to remove the item from the cache if +/// present before it goes away. +#[derive(Clone, Copy, Debug, Eq, Hash, MallocSizeOf, PartialEq)] +pub struct MediaListKey(usize); + +impl MediaListKey { + /// Create a MediaListKey from a raw usize. + pub fn from_raw(k: usize) -> Self { + MediaListKey(k) + } +} + +/// A trait to get a given `MediaListKey` for a given item that can hold a +/// `MediaList`. +pub trait ToMediaListKey: Sized { + /// Get a `MediaListKey` for this item. This key needs to uniquely identify + /// the item. + fn to_media_list_key(&self) -> MediaListKey { + MediaListKey(self as *const Self as usize) + } +} + +impl ToMediaListKey for StylesheetContents {} +impl ToMediaListKey for ImportRule {} +impl ToMediaListKey for MediaRule {} + +/// A struct that holds the result of a media query evaluation pass for the +/// media queries that evaluated successfully. +#[derive(Clone, Debug, MallocSizeOf, PartialEq)] +pub struct EffectiveMediaQueryResults { + /// The set of media lists that matched last time. + set: FxHashSet, +} + +impl EffectiveMediaQueryResults { + /// Trivially constructs an empty `EffectiveMediaQueryResults`. + pub fn new() -> Self { + Self { + set: FxHashSet::default(), + } + } + + /// Resets the results, using an empty key. + pub fn clear(&mut self) { + self.set.clear() + } + + /// Returns whether a given item was known to be effective when the results + /// were cached. + pub fn was_effective(&self, item: &T) -> bool + where + T: ToMediaListKey, + { + self.set.contains(&item.to_media_list_key()) + } + + /// Notices that an effective item has been seen, and caches it as matching. + pub fn saw_effective(&mut self, item: &T) + where + T: ToMediaListKey, + { + // NOTE(emilio): We can't assert that we don't cache the same item twice + // because of stylesheet reusing... shrug. + self.set.insert(item.to_media_list_key()); + } +} + +/// A filter that filters over effective rules, but allowing all potentially +/// effective `@media` rules. +pub struct PotentiallyEffectiveMediaRules; + +impl NestedRuleIterationCondition for PotentiallyEffectiveMediaRules { + fn process_import( + _: &SharedRwLockReadGuard, + _: &Device, + _: QuirksMode, + _: &ImportRule, + ) -> bool { + true + } + + fn process_media(_: &SharedRwLockReadGuard, _: &Device, _: QuirksMode, _: &MediaRule) -> bool { + true + } + + /// Whether we should process the nested rules in a given `@-moz-document` rule. + fn process_document( + guard: &SharedRwLockReadGuard, + device: &Device, + quirks_mode: QuirksMode, + rule: &DocumentRule, + ) -> bool { + use crate::stylesheets::EffectiveRules; + EffectiveRules::process_document(guard, device, quirks_mode, rule) + } + + /// Whether we should process the nested rules in a given `@supports` rule. + fn process_supports( + guard: &SharedRwLockReadGuard, + device: &Device, + quirks_mode: QuirksMode, + rule: &SupportsRule, + ) -> bool { + use crate::stylesheets::EffectiveRules; + EffectiveRules::process_supports(guard, device, quirks_mode, rule) + } +} diff --git a/servo/components/style/invalidation/mod.rs b/servo/components/style/invalidation/mod.rs new file mode 100644 index 0000000000..12b0d06853 --- /dev/null +++ b/servo/components/style/invalidation/mod.rs @@ -0,0 +1,10 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Different bits of code related to invalidating style. + +pub mod element; +pub mod media_queries; +pub mod stylesheets; +pub mod viewport_units; diff --git a/servo/components/style/invalidation/stylesheets.rs b/servo/components/style/invalidation/stylesheets.rs new file mode 100644 index 0000000000..5fc8ec4b76 --- /dev/null +++ b/servo/components/style/invalidation/stylesheets.rs @@ -0,0 +1,656 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! A collection of invalidations due to changes in which stylesheets affect a +//! document. + +#![deny(unsafe_code)] + +use crate::context::QuirksMode; +use crate::dom::{TDocument, TElement, TNode}; +use crate::invalidation::element::element_wrapper::{ElementSnapshot, ElementWrapper}; +use crate::invalidation::element::restyle_hints::RestyleHint; +use crate::media_queries::Device; +use crate::selector_map::{MaybeCaseInsensitiveHashMap, PrecomputedHashMap}; +use crate::selector_parser::{SelectorImpl, Snapshot, SnapshotMap}; +use crate::shared_lock::SharedRwLockReadGuard; +use crate::stylesheets::{CssRule, StylesheetInDocument}; +use crate::stylesheets::{EffectiveRules, EffectiveRulesIterator}; +use crate::values::AtomIdent; +use crate::LocalName as SelectorLocalName; +use crate::{Atom, ShrinkIfNeeded}; +use selectors::parser::{Component, LocalName, Selector}; + +/// The kind of change that happened for a given rule. +#[repr(u32)] +#[derive(Clone, Copy, Debug, Eq, Hash, MallocSizeOf, PartialEq)] +pub enum RuleChangeKind { + /// The rule was inserted. + Insertion, + /// The rule was removed. + Removal, + /// Some change in the rule which we don't know about, and could have made + /// the rule change in any way. + Generic, + /// A change in the declarations of a style rule. + StyleRuleDeclarations, +} + +/// A style sheet invalidation represents a kind of element or subtree that may +/// need to be restyled. Whether it represents a whole subtree or just a single +/// element is determined by the given InvalidationKind in +/// StylesheetInvalidationSet's maps. +#[derive(Debug, Eq, Hash, MallocSizeOf, PartialEq)] +enum Invalidation { + /// An element with a given id. + ID(AtomIdent), + /// An element with a given class name. + Class(AtomIdent), + /// An element with a given local name. + LocalName { + name: SelectorLocalName, + lower_name: SelectorLocalName, + }, +} + +impl Invalidation { + fn is_id(&self) -> bool { + matches!(*self, Invalidation::ID(..)) + } + + fn is_id_or_class(&self) -> bool { + matches!(*self, Invalidation::ID(..) | Invalidation::Class(..)) + } +} + +/// Whether we should invalidate just the element, or the whole subtree within +/// it. +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, Ord, PartialEq, PartialOrd)] +enum InvalidationKind { + None = 0, + Element, + Scope, +} + +impl std::ops::BitOrAssign for InvalidationKind { + #[inline] + fn bitor_assign(&mut self, other: Self) { + *self = std::cmp::max(*self, other); + } +} + +impl InvalidationKind { + #[inline] + fn is_scope(self) -> bool { + matches!(self, Self::Scope) + } + + #[inline] + fn add(&mut self, other: Option<&InvalidationKind>) { + if let Some(other) = other { + *self |= *other; + } + } +} + +/// A set of invalidations due to stylesheet additions. +/// +/// TODO(emilio): We might be able to do the same analysis for media query +/// changes too (or even selector changes?). +#[derive(Debug, Default, MallocSizeOf)] +pub struct StylesheetInvalidationSet { + classes: MaybeCaseInsensitiveHashMap, + ids: MaybeCaseInsensitiveHashMap, + local_names: PrecomputedHashMap, + fully_invalid: bool, +} + +impl StylesheetInvalidationSet { + /// Create an empty `StylesheetInvalidationSet`. + pub fn new() -> Self { + Default::default() + } + + /// Mark the DOM tree styles' as fully invalid. + pub fn invalidate_fully(&mut self) { + debug!("StylesheetInvalidationSet::invalidate_fully"); + self.clear(); + self.fully_invalid = true; + } + + fn shrink_if_needed(&mut self) { + if self.fully_invalid { + return; + } + self.classes.shrink_if_needed(); + self.ids.shrink_if_needed(); + self.local_names.shrink_if_needed(); + } + + /// Analyze the given stylesheet, and collect invalidations from their + /// rules, in order to avoid doing a full restyle when we style the document + /// next time. + pub fn collect_invalidations_for( + &mut self, + device: &Device, + stylesheet: &S, + guard: &SharedRwLockReadGuard, + ) where + S: StylesheetInDocument, + { + debug!("StylesheetInvalidationSet::collect_invalidations_for"); + if self.fully_invalid { + debug!(" > Fully invalid already"); + return; + } + + if !stylesheet.enabled() || !stylesheet.is_effective_for_device(device, guard) { + debug!(" > Stylesheet was not effective"); + return; // Nothing to do here. + } + + let quirks_mode = device.quirks_mode(); + for rule in stylesheet.effective_rules(device, guard) { + self.collect_invalidations_for_rule(rule, guard, device, quirks_mode); + if self.fully_invalid { + break; + } + } + + self.shrink_if_needed(); + + debug!(" > resulting class invalidations: {:?}", self.classes); + debug!(" > resulting id invalidations: {:?}", self.ids); + debug!( + " > resulting local name invalidations: {:?}", + self.local_names + ); + debug!(" > fully_invalid: {}", self.fully_invalid); + } + + /// Clears the invalidation set, invalidating elements as needed if + /// `document_element` is provided. + /// + /// Returns true if any invalidations ocurred. + pub fn flush(&mut self, document_element: Option, snapshots: Option<&SnapshotMap>) -> bool + where + E: TElement, + { + debug!( + "Stylist::flush({:?}, snapshots: {})", + document_element, + snapshots.is_some() + ); + let have_invalidations = match document_element { + Some(e) => self.process_invalidations(e, snapshots), + None => false, + }; + self.clear(); + have_invalidations + } + + /// Returns whether there's no invalidation to process. + pub fn is_empty(&self) -> bool { + !self.fully_invalid && + self.classes.is_empty() && + self.ids.is_empty() && + self.local_names.is_empty() + } + + fn invalidation_kind_for( + &self, + element: E, + snapshot: Option<&Snapshot>, + quirks_mode: QuirksMode, + ) -> InvalidationKind + where + E: TElement, + { + debug_assert!(!self.fully_invalid); + + let mut kind = InvalidationKind::None; + + if !self.classes.is_empty() { + element.each_class(|c| { + kind.add(self.classes.get(c, quirks_mode)); + }); + + if kind.is_scope() { + return kind; + } + + if let Some(snapshot) = snapshot { + snapshot.each_class(|c| { + kind.add(self.classes.get(c, quirks_mode)); + }); + + if kind.is_scope() { + return kind; + } + } + } + + if !self.ids.is_empty() { + if let Some(ref id) = element.id() { + kind.add(self.ids.get(id, quirks_mode)); + if kind.is_scope() { + return kind; + } + } + + if let Some(ref old_id) = snapshot.and_then(|s| s.id_attr()) { + kind.add(self.ids.get(old_id, quirks_mode)); + if kind.is_scope() { + return kind; + } + } + } + + if !self.local_names.is_empty() { + kind.add(self.local_names.get(element.local_name())); + } + + kind + } + + /// Clears the invalidation set without processing. + pub fn clear(&mut self) { + self.classes.clear(); + self.ids.clear(); + self.local_names.clear(); + self.fully_invalid = false; + debug_assert!(self.is_empty()); + } + + fn process_invalidations(&self, element: E, snapshots: Option<&SnapshotMap>) -> bool + where + E: TElement, + { + debug!("Stylist::process_invalidations({:?}, {:?})", element, self); + + { + let mut data = match element.mutate_data() { + Some(data) => data, + None => return false, + }; + + if self.fully_invalid { + debug!("process_invalidations: fully_invalid({:?})", element); + data.hint.insert(RestyleHint::restyle_subtree()); + return true; + } + } + + if self.is_empty() { + debug!("process_invalidations: empty invalidation set"); + return false; + } + + let quirks_mode = element.as_node().owner_doc().quirks_mode(); + self.process_invalidations_in_subtree(element, snapshots, quirks_mode) + } + + /// Process style invalidations in a given subtree. This traverses the + /// subtree looking for elements that match the invalidations in our hash + /// map members. + /// + /// Returns whether it invalidated at least one element's style. + #[allow(unsafe_code)] + fn process_invalidations_in_subtree( + &self, + element: E, + snapshots: Option<&SnapshotMap>, + quirks_mode: QuirksMode, + ) -> bool + where + E: TElement, + { + debug!("process_invalidations_in_subtree({:?})", element); + let mut data = match element.mutate_data() { + Some(data) => data, + None => return false, + }; + + if !data.has_styles() { + return false; + } + + if data.hint.contains_subtree() { + debug!( + "process_invalidations_in_subtree: {:?} was already invalid", + element + ); + return false; + } + + let element_wrapper = snapshots.map(|s| ElementWrapper::new(element, s)); + let snapshot = element_wrapper.as_ref().and_then(|e| e.snapshot()); + + match self.invalidation_kind_for(element, snapshot, quirks_mode) { + InvalidationKind::None => {}, + InvalidationKind::Element => { + debug!( + "process_invalidations_in_subtree: {:?} matched self", + element + ); + data.hint.insert(RestyleHint::RESTYLE_SELF); + }, + InvalidationKind::Scope => { + debug!( + "process_invalidations_in_subtree: {:?} matched subtree", + element + ); + data.hint.insert(RestyleHint::restyle_subtree()); + return true; + }, + } + + let mut any_children_invalid = false; + + for child in element.traversal_children() { + let child = match child.as_element() { + Some(e) => e, + None => continue, + }; + + any_children_invalid |= + self.process_invalidations_in_subtree(child, snapshots, quirks_mode); + } + + if any_children_invalid { + debug!( + "Children of {:?} changed, setting dirty descendants", + element + ); + unsafe { element.set_dirty_descendants() } + } + + data.hint.contains(RestyleHint::RESTYLE_SELF) || any_children_invalid + } + + /// TODO(emilio): Reuse the bucket stuff from selectormap? That handles + /// :is() / :where() etc. + fn scan_component( + component: &Component, + invalidation: &mut Option, + ) { + match *component { + Component::LocalName(LocalName { + ref name, + ref lower_name, + }) => { + if invalidation.is_none() { + *invalidation = Some(Invalidation::LocalName { + name: name.clone(), + lower_name: lower_name.clone(), + }); + } + }, + Component::Class(ref class) => { + if invalidation.as_ref().map_or(true, |s| !s.is_id_or_class()) { + *invalidation = Some(Invalidation::Class(class.clone())); + } + }, + Component::ID(ref id) => { + if invalidation.as_ref().map_or(true, |s| !s.is_id()) { + *invalidation = Some(Invalidation::ID(id.clone())); + } + }, + _ => { + // Ignore everything else, at least for now. + }, + } + } + + /// Collect invalidations for a given selector. + /// + /// We look at the outermost local name, class, or ID selector to the left + /// of an ancestor combinator, in order to restyle only a given subtree. + /// + /// If the selector has no ancestor combinator, then we do the same for + /// the only sequence it has, but record it as an element invalidation + /// instead of a subtree invalidation. + /// + /// We prefer IDs to classs, and classes to local names, on the basis + /// that the former should be more specific than the latter. We also + /// prefer to generate subtree invalidations for the outermost part + /// of the selector, to reduce the amount of traversal we need to do + /// when flushing invalidations. + fn collect_invalidations( + &mut self, + selector: &Selector, + quirks_mode: QuirksMode, + ) { + debug!( + "StylesheetInvalidationSet::collect_invalidations({:?})", + selector + ); + + let mut element_invalidation: Option = None; + let mut subtree_invalidation: Option = None; + + let mut scan_for_element_invalidation = true; + let mut scan_for_subtree_invalidation = false; + + let mut iter = selector.iter(); + + loop { + for component in &mut iter { + if scan_for_element_invalidation { + Self::scan_component(component, &mut element_invalidation); + } else if scan_for_subtree_invalidation { + Self::scan_component(component, &mut subtree_invalidation); + } + } + match iter.next_sequence() { + None => break, + Some(combinator) => { + scan_for_subtree_invalidation = combinator.is_ancestor(); + }, + } + scan_for_element_invalidation = false; + } + + if let Some(s) = subtree_invalidation { + debug!(" > Found subtree invalidation: {:?}", s); + if self.insert_invalidation(s, InvalidationKind::Scope, quirks_mode) { + return; + } + } + + if let Some(s) = element_invalidation { + debug!(" > Found element invalidation: {:?}", s); + if self.insert_invalidation(s, InvalidationKind::Element, quirks_mode) { + return; + } + } + + // The selector was of a form that we can't handle. Any element could + // match it, so let's just bail out. + debug!(" > Can't handle selector or OOMd, marking fully invalid"); + self.invalidate_fully() + } + + fn insert_invalidation( + &mut self, + invalidation: Invalidation, + kind: InvalidationKind, + quirks_mode: QuirksMode, + ) -> bool { + match invalidation { + Invalidation::Class(c) => { + let entry = match self.classes.try_entry(c.0, quirks_mode) { + Ok(e) => e, + Err(..) => return false, + }; + *entry.or_insert(InvalidationKind::None) |= kind; + }, + Invalidation::ID(i) => { + let entry = match self.ids.try_entry(i.0, quirks_mode) { + Ok(e) => e, + Err(..) => return false, + }; + *entry.or_insert(InvalidationKind::None) |= kind; + }, + Invalidation::LocalName { name, lower_name } => { + let insert_lower = name != lower_name; + if self.local_names.try_reserve(1).is_err() { + return false; + } + let entry = self.local_names.entry(name); + *entry.or_insert(InvalidationKind::None) |= kind; + if insert_lower { + if self.local_names.try_reserve(1).is_err() { + return false; + } + let entry = self.local_names.entry(lower_name); + *entry.or_insert(InvalidationKind::None) |= kind; + } + }, + } + + true + } + + /// Collects invalidations for a given CSS rule, if not fully invalid + /// already. + /// + /// TODO(emilio): we can't check whether the rule is inside a non-effective + /// subtree, we potentially could do that. + pub fn rule_changed( + &mut self, + stylesheet: &S, + rule: &CssRule, + guard: &SharedRwLockReadGuard, + device: &Device, + quirks_mode: QuirksMode, + change_kind: RuleChangeKind, + ) where + S: StylesheetInDocument, + { + use crate::stylesheets::CssRule::*; + + debug!("StylesheetInvalidationSet::rule_changed"); + if self.fully_invalid { + return; + } + + if !stylesheet.enabled() || !stylesheet.is_effective_for_device(device, guard) { + debug!(" > Stylesheet was not effective"); + return; // Nothing to do here. + } + + let is_generic_change = change_kind == RuleChangeKind::Generic; + + match *rule { + Namespace(..) => { + // It's not clear what handling changes for this correctly would + // look like. + }, + CounterStyle(..) | + Page(..) | + Property(..) | + Viewport(..) | + FontFeatureValues(..) | + FontPaletteValues(..) | + LayerStatement(..) | + FontFace(..) | + Keyframes(..) | + Container(..) | + Style(..) => { + if is_generic_change { + // TODO(emilio): We need to do this for selector / keyframe + // name / font-face changes, because we don't have the old + // selector / name. If we distinguish those changes + // specially, then we can at least use this invalidation for + // style declaration changes. + return self.invalidate_fully(); + } + + self.collect_invalidations_for_rule(rule, guard, device, quirks_mode) + }, + Document(..) | Import(..) | Media(..) | Supports(..) | LayerBlock(..) => { + if !is_generic_change && + !EffectiveRules::is_effective(guard, device, quirks_mode, rule) + { + return; + } + + let rules = + EffectiveRulesIterator::effective_children(device, quirks_mode, guard, rule); + for rule in rules { + self.collect_invalidations_for_rule(rule, guard, device, quirks_mode); + if self.fully_invalid { + break; + } + } + }, + } + } + + /// Collects invalidations for a given CSS rule. + fn collect_invalidations_for_rule( + &mut self, + rule: &CssRule, + guard: &SharedRwLockReadGuard, + device: &Device, + quirks_mode: QuirksMode, + ) { + use crate::stylesheets::CssRule::*; + debug!("StylesheetInvalidationSet::collect_invalidations_for_rule"); + debug_assert!(!self.fully_invalid, "Not worth to be here!"); + + match *rule { + Style(ref lock) => { + let style_rule = lock.read_with(guard); + for selector in &style_rule.selectors.0 { + self.collect_invalidations(selector, quirks_mode); + if self.fully_invalid { + return; + } + } + }, + Document(..) | Namespace(..) | Import(..) | Media(..) | Supports(..) | + Container(..) | LayerStatement(..) | LayerBlock(..) => { + // Do nothing, relevant nested rules are visited as part of the + // iteration. + }, + FontFace(..) => { + // Do nothing, @font-face doesn't affect computed style + // information. We'll restyle when the font face loads, if + // needed. + }, + Keyframes(ref lock) => { + let keyframes_rule = lock.read_with(guard); + if device.animation_name_may_be_referenced(&keyframes_rule.name) { + debug!( + " > Found @keyframes rule potentially referenced \ + from the page, marking the whole tree invalid." + ); + self.fully_invalid = true; + } else { + // Do nothing, this animation can't affect the style of + // existing elements. + } + }, + CounterStyle(..) | + Page(..) | + Property(..) | + Viewport(..) | + FontFeatureValues(..) | + FontPaletteValues(..) => { + debug!( + " > Found unsupported rule, marking the whole subtree \ + invalid." + ); + + // TODO(emilio): Can we do better here? + // + // At least in `@page`, we could check the relevant media, I + // guess. + self.fully_invalid = true; + }, + } + } +} diff --git a/servo/components/style/invalidation/viewport_units.rs b/servo/components/style/invalidation/viewport_units.rs new file mode 100644 index 0000000000..06faeb14c4 --- /dev/null +++ b/servo/components/style/invalidation/viewport_units.rs @@ -0,0 +1,71 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Invalidates style of all elements that depend on viewport units. + +use crate::data::ViewportUnitUsage; +use crate::dom::{TElement, TNode}; +use crate::invalidation::element::restyle_hints::RestyleHint; + +/// Invalidates style of all elements that depend on viewport units. +/// +/// Returns whether any element was invalidated. +pub fn invalidate(root: E) -> bool +where + E: TElement, +{ + debug!("invalidation::viewport_units::invalidate({:?})", root); + invalidate_recursively(root) +} + +fn invalidate_recursively(element: E) -> bool +where + E: TElement, +{ + let mut data = match element.mutate_data() { + Some(data) => data, + None => return false, + }; + + if data.hint.will_recascade_subtree() { + debug!("invalidate_recursively: {:?} was already invalid", element); + return false; + } + + let usage = data.styles.viewport_unit_usage(); + let uses_viewport_units = usage != ViewportUnitUsage::None; + if uses_viewport_units { + debug!( + "invalidate_recursively: {:?} uses viewport units {:?}", + element, usage + ); + } + + match usage { + ViewportUnitUsage::None => {}, + ViewportUnitUsage::FromQuery => { + data.hint.insert(RestyleHint::RESTYLE_SELF); + }, + ViewportUnitUsage::FromDeclaration => { + data.hint.insert(RestyleHint::RECASCADE_SELF); + }, + } + + let mut any_children_invalid = false; + for child in element.traversal_children() { + if let Some(child) = child.as_element() { + any_children_invalid |= invalidate_recursively(child); + } + } + + if any_children_invalid { + debug!( + "invalidate_recursively: Children of {:?} changed, setting dirty descendants", + element + ); + unsafe { element.set_dirty_descendants() } + } + + uses_viewport_units || any_children_invalid +} diff --git a/servo/components/style/lib.rs b/servo/components/style/lib.rs new file mode 100644 index 0000000000..b7ea3caff9 --- /dev/null +++ b/servo/components/style/lib.rs @@ -0,0 +1,330 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Calculate [specified][specified] and [computed values][computed] from a +//! tree of DOM nodes and a set of stylesheets. +//! +//! [computed]: https://drafts.csswg.org/css-cascade/#computed +//! [specified]: https://drafts.csswg.org/css-cascade/#specified +//! +//! In particular, this crate contains the definitions of supported properties, +//! the code to parse them into specified values and calculate the computed +//! values based on the specified values, as well as the code to serialize both +//! specified and computed values. +//! +//! The main entry point is [`recalc_style_at`][recalc_style_at]. +//! +//! [recalc_style_at]: traversal/fn.recalc_style_at.html +//! +//! Major dependencies are the [cssparser][cssparser] and [selectors][selectors] +//! crates. +//! +//! [cssparser]: ../cssparser/index.html +//! [selectors]: ../selectors/index.html + +#![deny(missing_docs)] + +#[macro_use] +extern crate bitflags; +#[macro_use] +extern crate cssparser; +#[macro_use] +extern crate debug_unreachable; +#[macro_use] +extern crate derive_more; +#[macro_use] +extern crate gecko_profiler; +#[cfg(feature = "gecko")] +#[macro_use] +pub mod gecko_string_cache; +#[cfg(feature = "servo")] +#[macro_use] +extern crate html5ever; +#[macro_use] +extern crate lazy_static; +#[macro_use] +extern crate log; +#[macro_use] +extern crate malloc_size_of; +#[macro_use] +extern crate malloc_size_of_derive; +#[allow(unused_extern_crates)] +#[macro_use] +extern crate matches; +#[cfg(feature = "gecko")] +pub use nsstring; +#[cfg(feature = "gecko")] +extern crate num_cpus; +#[macro_use] +extern crate num_derive; +#[macro_use] +extern crate serde; +pub use servo_arc; +#[cfg(feature = "servo")] +#[macro_use] +extern crate servo_atoms; +#[macro_use] +extern crate static_assertions; +#[macro_use] +extern crate style_derive; +#[macro_use] +extern crate to_shmem_derive; + +#[macro_use] +mod macros; + +pub mod animation; +pub mod applicable_declarations; +#[allow(missing_docs)] // TODO. +#[cfg(feature = "servo")] +pub mod attr; +pub mod author_styles; +pub mod bezier; +pub mod bloom; +pub mod color; +#[path = "properties/computed_value_flags.rs"] +pub mod computed_value_flags; +pub mod context; +pub mod counter_style; +pub mod custom_properties; +pub mod data; +pub mod dom; +pub mod dom_apis; +pub mod driver; +#[cfg(feature = "servo")] +mod encoding_support; +pub mod error_reporting; +pub mod font_face; +pub mod font_metrics; +#[cfg(feature = "gecko")] +#[allow(unsafe_code)] +pub mod gecko_bindings; +pub mod global_style_data; +pub mod invalidation; +#[allow(missing_docs)] // TODO. +pub mod logical_geometry; +pub mod matching; +pub mod media_queries; +pub mod parallel; +pub mod parser; +pub mod piecewise_linear; +pub mod properties_and_values; +#[macro_use] +pub mod queries; +pub mod rule_cache; +pub mod rule_collector; +pub mod rule_tree; +pub mod scoped_tls; +pub mod selector_map; +pub mod selector_parser; +pub mod shared_lock; +pub mod sharing; +pub mod str; +pub mod style_adjuster; +pub mod style_resolver; +pub mod stylesheet_set; +pub mod stylesheets; +pub mod stylist; +pub mod thread_state; +pub mod traversal; +pub mod traversal_flags; +pub mod use_counters; +#[macro_use] +#[allow(non_camel_case_types)] +pub mod values; + +#[cfg(feature = "gecko")] +pub use crate::gecko_string_cache as string_cache; +#[cfg(feature = "gecko")] +pub use crate::gecko_string_cache::Atom; +/// The namespace prefix type for Gecko, which is just an atom. +#[cfg(feature = "gecko")] +pub type Prefix = crate::values::AtomIdent; +/// The local name of an element for Gecko, which is just an atom. +#[cfg(feature = "gecko")] +pub type LocalName = crate::values::AtomIdent; +#[cfg(feature = "gecko")] +pub use crate::gecko_string_cache::Namespace; + +#[cfg(feature = "servo")] +pub use servo_atoms::Atom; + +#[cfg(feature = "servo")] +#[allow(missing_docs)] +pub type LocalName = crate::values::GenericAtomIdent; +#[cfg(feature = "servo")] +#[allow(missing_docs)] +pub type Namespace = crate::values::GenericAtomIdent; +#[cfg(feature = "servo")] +#[allow(missing_docs)] +pub type Prefix = crate::values::GenericAtomIdent; + +pub use style_traits::arc_slice::ArcSlice; +pub use style_traits::owned_slice::OwnedSlice; +pub use style_traits::owned_str::OwnedStr; + +use std::hash::{BuildHasher, Hash}; + +pub mod properties; + +#[cfg(feature = "gecko")] +#[allow(unsafe_code)] +pub mod gecko; + +// uses a macro from properties +#[cfg(feature = "servo")] +#[allow(unsafe_code)] +pub mod servo; + +macro_rules! reexport_computed_values { + ( $( { $name: ident } )+ ) => { + /// Types for [computed values][computed]. + /// + /// [computed]: https://drafts.csswg.org/css-cascade/#computed + pub mod computed_values { + $( + pub use crate::properties::longhands::$name::computed_value as $name; + )+ + // Don't use a side-specific name needlessly: + pub use crate::properties::longhands::border_top_style::computed_value as border_style; + } + } +} +longhand_properties_idents!(reexport_computed_values); +#[cfg(feature = "gecko")] +use crate::gecko_string_cache::WeakAtom; +#[cfg(feature = "servo")] +use servo_atoms::Atom as WeakAtom; + +/// Extension methods for selectors::attr::CaseSensitivity +pub trait CaseSensitivityExt { + /// Return whether two atoms compare equal according to this case sensitivity. + fn eq_atom(self, a: &WeakAtom, b: &WeakAtom) -> bool; +} + +impl CaseSensitivityExt for selectors::attr::CaseSensitivity { + fn eq_atom(self, a: &WeakAtom, b: &WeakAtom) -> bool { + match self { + selectors::attr::CaseSensitivity::CaseSensitive => a == b, + selectors::attr::CaseSensitivity::AsciiCaseInsensitive => a.eq_ignore_ascii_case(b), + } + } +} + +/// A trait pretty much similar to num_traits::Zero, but without the need of +/// implementing `Add`. +pub trait Zero { + /// Returns the zero value. + fn zero() -> Self; + + /// Returns whether this value is zero. + fn is_zero(&self) -> bool; +} + +impl Zero for T +where + T: num_traits::Zero, +{ + fn zero() -> Self { + ::zero() + } + + fn is_zero(&self) -> bool { + ::is_zero(self) + } +} + +/// A trait implementing a function to tell if the number is zero without a percent +pub trait ZeroNoPercent { + /// So, `0px` should return `true`, but `0%` or `1px` should return `false` + fn is_zero_no_percent(&self) -> bool; +} + +/// A trait pretty much similar to num_traits::One, but without the need of +/// implementing `Mul`. +pub trait One { + /// Reutrns the one value. + fn one() -> Self; + + /// Returns whether this value is one. + fn is_one(&self) -> bool; +} + +impl One for T +where + T: num_traits::One + PartialEq, +{ + fn one() -> Self { + ::one() + } + + fn is_one(&self) -> bool { + *self == One::one() + } +} + +/// An allocation error. +/// +/// TODO(emilio): Would be nice to have more information here, or for SmallVec +/// to return the standard error type (and then we can just return that). +/// +/// But given we use these mostly to bail out and ignore them, it's not a big +/// deal. +#[derive(Debug)] +pub struct AllocErr; + +impl From for AllocErr { + #[inline] + fn from(_: smallvec::CollectionAllocErr) -> Self { + Self + } +} + +impl From for AllocErr { + #[inline] + fn from(_: std::collections::TryReserveError) -> Self { + Self + } +} + +/// Shrink the capacity of the collection if needed. +pub(crate) trait ShrinkIfNeeded { + fn shrink_if_needed(&mut self); +} + +/// We shrink the capacity of a collection if we're wasting more than a 25% of +/// its capacity, and if the collection is arbitrarily big enough +/// (>= CAPACITY_THRESHOLD entries). +#[inline] +fn should_shrink(len: usize, capacity: usize) -> bool { + const CAPACITY_THRESHOLD: usize = 64; + capacity >= CAPACITY_THRESHOLD && len + capacity / 4 < capacity +} + +impl ShrinkIfNeeded for std::collections::HashMap +where + K: Eq + Hash, + H: BuildHasher, +{ + fn shrink_if_needed(&mut self) { + if should_shrink(self.len(), self.capacity()) { + self.shrink_to_fit(); + } + } +} + +impl ShrinkIfNeeded for std::collections::HashSet +where + T: Eq + Hash, + H: BuildHasher, +{ + fn shrink_if_needed(&mut self) { + if should_shrink(self.len(), self.capacity()) { + self.shrink_to_fit(); + } + } +} + +// TODO(emilio): Measure and see if we're wasting a lot of memory on Vec / +// SmallVec, and if so consider shrinking those as well. diff --git a/servo/components/style/logical_geometry.rs b/servo/components/style/logical_geometry.rs new file mode 100644 index 0000000000..b8a7927dbb --- /dev/null +++ b/servo/components/style/logical_geometry.rs @@ -0,0 +1,1472 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Geometry in flow-relative space. + +use crate::properties::style_structs; +use euclid::default::{Point2D, Rect, SideOffsets2D, Size2D}; +use euclid::num::Zero; +use std::cmp::{max, min}; +use std::fmt::{self, Debug, Error, Formatter}; +use std::ops::{Add, Sub}; +use unicode_bidi as bidi; + +pub enum BlockFlowDirection { + TopToBottom, + RightToLeft, + LeftToRight, +} + +pub enum InlineBaseDirection { + LeftToRight, + RightToLeft, +} + +// TODO: improve the readability of the WritingMode serialization, refer to the Debug:fmt() +bitflags!( + #[derive(Clone, Copy, Eq, MallocSizeOf, PartialEq, Serialize)] + #[repr(C)] + pub struct WritingMode: u8 { + /// A vertical writing mode; writing-mode is vertical-rl, + /// vertical-lr, sideways-lr, or sideways-rl. + const VERTICAL = 1 << 0; + /// The inline flow direction is reversed against the physical + /// direction (i.e. right-to-left or bottom-to-top); writing-mode is + /// sideways-lr or direction is rtl (but not both). + /// + /// (This bit can be derived from the others, but we store it for + /// convenience.) + const INLINE_REVERSED = 1 << 1; + /// A vertical writing mode whose block progression direction is left- + /// to-right; writing-mode is vertical-lr or sideways-lr. + /// + /// Never set without VERTICAL. + const VERTICAL_LR = 1 << 2; + /// The line-over/line-under sides are inverted with respect to the + /// block-start/block-end edge; writing-mode is vertical-lr. + /// + /// Never set without VERTICAL and VERTICAL_LR. + const LINE_INVERTED = 1 << 3; + /// direction is rtl. + const RTL = 1 << 4; + /// All text within a vertical writing mode is displayed sideways + /// and runs top-to-bottom or bottom-to-top; set in these cases: + /// + /// * writing-mode: sideways-rl; + /// * writing-mode: sideways-lr; + /// + /// Never set without VERTICAL. + const VERTICAL_SIDEWAYS = 1 << 5; + /// Similar to VERTICAL_SIDEWAYS, but is set via text-orientation; + /// set in these cases: + /// + /// * writing-mode: vertical-rl; text-orientation: sideways; + /// * writing-mode: vertical-lr; text-orientation: sideways; + /// + /// Never set without VERTICAL. + const TEXT_SIDEWAYS = 1 << 6; + /// Horizontal text within a vertical writing mode is displayed with each + /// glyph upright; set in these cases: + /// + /// * writing-mode: vertical-rl; text-orientation: upright; + /// * writing-mode: vertical-lr: text-orientation: upright; + /// + /// Never set without VERTICAL. + const UPRIGHT = 1 << 7; + } +); + +impl WritingMode { + /// Return a WritingMode bitflags from the relevant CSS properties. + pub fn new(inheritedbox_style: &style_structs::InheritedBox) -> Self { + use crate::properties::longhands::direction::computed_value::T as Direction; + use crate::properties::longhands::writing_mode::computed_value::T as SpecifiedWritingMode; + + let mut flags = WritingMode::empty(); + + let direction = inheritedbox_style.clone_direction(); + let writing_mode = inheritedbox_style.clone_writing_mode(); + + match direction { + Direction::Ltr => {}, + Direction::Rtl => { + flags.insert(WritingMode::RTL); + }, + } + + match writing_mode { + SpecifiedWritingMode::HorizontalTb => { + if direction == Direction::Rtl { + flags.insert(WritingMode::INLINE_REVERSED); + } + }, + SpecifiedWritingMode::VerticalRl => { + flags.insert(WritingMode::VERTICAL); + if direction == Direction::Rtl { + flags.insert(WritingMode::INLINE_REVERSED); + } + }, + SpecifiedWritingMode::VerticalLr => { + flags.insert(WritingMode::VERTICAL); + flags.insert(WritingMode::VERTICAL_LR); + flags.insert(WritingMode::LINE_INVERTED); + if direction == Direction::Rtl { + flags.insert(WritingMode::INLINE_REVERSED); + } + }, + #[cfg(feature = "gecko")] + SpecifiedWritingMode::SidewaysRl => { + flags.insert(WritingMode::VERTICAL); + flags.insert(WritingMode::VERTICAL_SIDEWAYS); + if direction == Direction::Rtl { + flags.insert(WritingMode::INLINE_REVERSED); + } + }, + #[cfg(feature = "gecko")] + SpecifiedWritingMode::SidewaysLr => { + flags.insert(WritingMode::VERTICAL); + flags.insert(WritingMode::VERTICAL_LR); + flags.insert(WritingMode::VERTICAL_SIDEWAYS); + if direction == Direction::Ltr { + flags.insert(WritingMode::INLINE_REVERSED); + } + }, + } + + #[cfg(feature = "gecko")] + { + use crate::properties::longhands::text_orientation::computed_value::T as TextOrientation; + + // text-orientation only has an effect for vertical-rl and + // vertical-lr values of writing-mode. + match writing_mode { + SpecifiedWritingMode::VerticalRl | SpecifiedWritingMode::VerticalLr => { + match inheritedbox_style.clone_text_orientation() { + TextOrientation::Mixed => {}, + TextOrientation::Upright => { + flags.insert(WritingMode::UPRIGHT); + + // https://drafts.csswg.org/css-writing-modes-3/#valdef-text-orientation-upright: + // + // > This value causes the used value of direction + // > to be ltr, and for the purposes of bidi + // > reordering, causes all characters to be treated + // > as strong LTR. + flags.remove(WritingMode::RTL); + flags.remove(WritingMode::INLINE_REVERSED); + }, + TextOrientation::Sideways => { + flags.insert(WritingMode::TEXT_SIDEWAYS); + }, + } + }, + _ => {}, + } + } + + flags + } + + /// Returns the `horizontal-tb` value. + pub fn horizontal_tb() -> Self { + Self::empty() + } + + #[inline] + pub fn is_vertical(&self) -> bool { + self.intersects(WritingMode::VERTICAL) + } + + #[inline] + pub fn is_horizontal(&self) -> bool { + !self.is_vertical() + } + + /// Assuming .is_vertical(), does the block direction go left to right? + #[inline] + pub fn is_vertical_lr(&self) -> bool { + self.intersects(WritingMode::VERTICAL_LR) + } + + /// Assuming .is_vertical(), does the inline direction go top to bottom? + #[inline] + pub fn is_inline_tb(&self) -> bool { + // https://drafts.csswg.org/css-writing-modes-3/#logical-to-physical + !self.intersects(WritingMode::INLINE_REVERSED) + } + + #[inline] + pub fn is_bidi_ltr(&self) -> bool { + !self.intersects(WritingMode::RTL) + } + + #[inline] + pub fn is_sideways(&self) -> bool { + self.intersects(WritingMode::VERTICAL_SIDEWAYS | WritingMode::TEXT_SIDEWAYS) + } + + #[inline] + pub fn is_upright(&self) -> bool { + self.intersects(WritingMode::UPRIGHT) + } + + /// https://drafts.csswg.org/css-writing-modes/#logical-to-physical + /// + /// | Return | line-left is… | line-right is… | + /// |---------|---------------|----------------| + /// | `true` | inline-start | inline-end | + /// | `false` | inline-end | inline-start | + #[inline] + pub fn line_left_is_inline_start(&self) -> bool { + // https://drafts.csswg.org/css-writing-modes/#inline-start + // “For boxes with a used direction value of ltr, this means the line-left side. + // For boxes with a used direction value of rtl, this means the line-right side.” + self.is_bidi_ltr() + } + + #[inline] + pub fn inline_start_physical_side(&self) -> PhysicalSide { + match (self.is_vertical(), self.is_inline_tb(), self.is_bidi_ltr()) { + (false, _, true) => PhysicalSide::Left, + (false, _, false) => PhysicalSide::Right, + (true, true, _) => PhysicalSide::Top, + (true, false, _) => PhysicalSide::Bottom, + } + } + + #[inline] + pub fn inline_end_physical_side(&self) -> PhysicalSide { + match (self.is_vertical(), self.is_inline_tb(), self.is_bidi_ltr()) { + (false, _, true) => PhysicalSide::Right, + (false, _, false) => PhysicalSide::Left, + (true, true, _) => PhysicalSide::Bottom, + (true, false, _) => PhysicalSide::Top, + } + } + + #[inline] + pub fn block_start_physical_side(&self) -> PhysicalSide { + match (self.is_vertical(), self.is_vertical_lr()) { + (false, _) => PhysicalSide::Top, + (true, true) => PhysicalSide::Left, + (true, false) => PhysicalSide::Right, + } + } + + #[inline] + pub fn block_end_physical_side(&self) -> PhysicalSide { + match (self.is_vertical(), self.is_vertical_lr()) { + (false, _) => PhysicalSide::Bottom, + (true, true) => PhysicalSide::Right, + (true, false) => PhysicalSide::Left, + } + } + + #[inline] + fn physical_sides_to_corner( + block_side: PhysicalSide, + inline_side: PhysicalSide, + ) -> PhysicalCorner { + match (block_side, inline_side) { + (PhysicalSide::Top, PhysicalSide::Left) | (PhysicalSide::Left, PhysicalSide::Top) => { + PhysicalCorner::TopLeft + }, + (PhysicalSide::Top, PhysicalSide::Right) | (PhysicalSide::Right, PhysicalSide::Top) => { + PhysicalCorner::TopRight + }, + (PhysicalSide::Bottom, PhysicalSide::Right) | + (PhysicalSide::Right, PhysicalSide::Bottom) => PhysicalCorner::BottomRight, + (PhysicalSide::Bottom, PhysicalSide::Left) | + (PhysicalSide::Left, PhysicalSide::Bottom) => PhysicalCorner::BottomLeft, + _ => unreachable!("block and inline sides must be orthogonal"), + } + } + + #[inline] + pub fn start_start_physical_corner(&self) -> PhysicalCorner { + WritingMode::physical_sides_to_corner( + self.block_start_physical_side(), + self.inline_start_physical_side(), + ) + } + + #[inline] + pub fn start_end_physical_corner(&self) -> PhysicalCorner { + WritingMode::physical_sides_to_corner( + self.block_start_physical_side(), + self.inline_end_physical_side(), + ) + } + + #[inline] + pub fn end_start_physical_corner(&self) -> PhysicalCorner { + WritingMode::physical_sides_to_corner( + self.block_end_physical_side(), + self.inline_start_physical_side(), + ) + } + + #[inline] + pub fn end_end_physical_corner(&self) -> PhysicalCorner { + WritingMode::physical_sides_to_corner( + self.block_end_physical_side(), + self.inline_end_physical_side(), + ) + } + + #[inline] + pub fn block_flow_direction(&self) -> BlockFlowDirection { + match (self.is_vertical(), self.is_vertical_lr()) { + (false, _) => BlockFlowDirection::TopToBottom, + (true, true) => BlockFlowDirection::LeftToRight, + (true, false) => BlockFlowDirection::RightToLeft, + } + } + + #[inline] + pub fn inline_base_direction(&self) -> InlineBaseDirection { + if self.intersects(WritingMode::RTL) { + InlineBaseDirection::RightToLeft + } else { + InlineBaseDirection::LeftToRight + } + } + + #[inline] + /// The default bidirectional embedding level for this writing mode. + /// + /// Returns bidi level 0 if the mode is LTR, or 1 otherwise. + pub fn to_bidi_level(&self) -> bidi::Level { + if self.is_bidi_ltr() { + bidi::Level::ltr() + } else { + bidi::Level::rtl() + } + } +} + +impl fmt::Display for WritingMode { + fn fmt(&self, formatter: &mut Formatter) -> Result<(), Error> { + if self.is_vertical() { + write!(formatter, "V")?; + if self.is_vertical_lr() { + write!(formatter, " LR")?; + } else { + write!(formatter, " RL")?; + } + if self.is_sideways() { + write!(formatter, " Sideways")?; + } + if self.intersects(WritingMode::LINE_INVERTED) { + write!(formatter, " Inverted")?; + } + } else { + write!(formatter, "H")?; + } + if self.is_bidi_ltr() { + write!(formatter, " LTR") + } else { + write!(formatter, " RTL") + } + } +} + +/// Wherever logical geometry is used, the writing mode is known based on context: +/// every method takes a `mode` parameter. +/// However, this context is easy to get wrong. +/// In debug builds only, logical geometry objects store their writing mode +/// (in addition to taking it as a parameter to methods) and check it. +/// In non-debug builds, make this storage zero-size and the checks no-ops. +#[cfg(not(debug_assertions))] +#[derive(Clone, Copy, Eq, PartialEq)] +#[cfg_attr(feature = "servo", derive(Serialize))] +struct DebugWritingMode; + +#[cfg(debug_assertions)] +#[derive(Clone, Copy, Eq, PartialEq)] +#[cfg_attr(feature = "servo", derive(Serialize))] +struct DebugWritingMode { + mode: WritingMode, +} + +#[cfg(not(debug_assertions))] +impl DebugWritingMode { + #[inline] + fn check(&self, _other: WritingMode) {} + + #[inline] + fn check_debug(&self, _other: DebugWritingMode) {} + + #[inline] + fn new(_mode: WritingMode) -> DebugWritingMode { + DebugWritingMode + } +} + +#[cfg(debug_assertions)] +impl DebugWritingMode { + #[inline] + fn check(&self, other: WritingMode) { + assert_eq!(self.mode, other) + } + + #[inline] + fn check_debug(&self, other: DebugWritingMode) { + assert_eq!(self.mode, other.mode) + } + + #[inline] + fn new(mode: WritingMode) -> DebugWritingMode { + DebugWritingMode { mode } + } +} + +impl Debug for DebugWritingMode { + #[cfg(not(debug_assertions))] + fn fmt(&self, formatter: &mut Formatter) -> Result<(), Error> { + write!(formatter, "?") + } + + #[cfg(debug_assertions)] + fn fmt(&self, formatter: &mut Formatter) -> Result<(), Error> { + write!(formatter, "{}", self.mode) + } +} + +// Used to specify the logical direction. +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "servo", derive(Serialize))] +pub enum Direction { + Inline, + Block, +} + +/// A 2D size in flow-relative dimensions +#[derive(Clone, Copy, Eq, PartialEq)] +#[cfg_attr(feature = "servo", derive(Serialize))] +pub struct LogicalSize { + pub inline: T, // inline-size, a.k.a. logical width, a.k.a. measure + pub block: T, // block-size, a.k.a. logical height, a.k.a. extent + debug_writing_mode: DebugWritingMode, +} + +impl Debug for LogicalSize { + fn fmt(&self, formatter: &mut Formatter) -> Result<(), Error> { + write!( + formatter, + "LogicalSize({:?}, i{:?}×b{:?})", + self.debug_writing_mode, self.inline, self.block + ) + } +} + +// Can not implement the Zero trait: its zero() method does not have the `mode` parameter. +impl LogicalSize { + #[inline] + pub fn zero(mode: WritingMode) -> LogicalSize { + LogicalSize { + inline: Zero::zero(), + block: Zero::zero(), + debug_writing_mode: DebugWritingMode::new(mode), + } + } +} + +impl LogicalSize { + #[inline] + pub fn new(mode: WritingMode, inline: T, block: T) -> LogicalSize { + LogicalSize { + inline: inline, + block: block, + debug_writing_mode: DebugWritingMode::new(mode), + } + } + + #[inline] + pub fn from_physical(mode: WritingMode, size: Size2D) -> LogicalSize { + if mode.is_vertical() { + LogicalSize::new(mode, size.height, size.width) + } else { + LogicalSize::new(mode, size.width, size.height) + } + } +} + +impl LogicalSize { + #[inline] + pub fn width(&self, mode: WritingMode) -> T { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + self.block + } else { + self.inline + } + } + + #[inline] + pub fn set_width(&mut self, mode: WritingMode, width: T) { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + self.block = width + } else { + self.inline = width + } + } + + #[inline] + pub fn height(&self, mode: WritingMode) -> T { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + self.inline + } else { + self.block + } + } + + #[inline] + pub fn set_height(&mut self, mode: WritingMode, height: T) { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + self.inline = height + } else { + self.block = height + } + } + + #[inline] + pub fn to_physical(&self, mode: WritingMode) -> Size2D { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + Size2D::new(self.block, self.inline) + } else { + Size2D::new(self.inline, self.block) + } + } + + #[inline] + pub fn convert(&self, mode_from: WritingMode, mode_to: WritingMode) -> LogicalSize { + if mode_from == mode_to { + self.debug_writing_mode.check(mode_from); + *self + } else { + LogicalSize::from_physical(mode_to, self.to_physical(mode_from)) + } + } +} + +impl> Add for LogicalSize { + type Output = LogicalSize; + + #[inline] + fn add(self, other: LogicalSize) -> LogicalSize { + self.debug_writing_mode + .check_debug(other.debug_writing_mode); + LogicalSize { + debug_writing_mode: self.debug_writing_mode, + inline: self.inline + other.inline, + block: self.block + other.block, + } + } +} + +impl> Sub for LogicalSize { + type Output = LogicalSize; + + #[inline] + fn sub(self, other: LogicalSize) -> LogicalSize { + self.debug_writing_mode + .check_debug(other.debug_writing_mode); + LogicalSize { + debug_writing_mode: self.debug_writing_mode, + inline: self.inline - other.inline, + block: self.block - other.block, + } + } +} + +/// A 2D point in flow-relative dimensions +#[derive(Clone, Copy, Eq, PartialEq)] +#[cfg_attr(feature = "servo", derive(Serialize))] +pub struct LogicalPoint { + /// inline-axis coordinate + pub i: T, + /// block-axis coordinate + pub b: T, + debug_writing_mode: DebugWritingMode, +} + +impl Debug for LogicalPoint { + fn fmt(&self, formatter: &mut Formatter) -> Result<(), Error> { + write!( + formatter, + "LogicalPoint({:?} (i{:?}, b{:?}))", + self.debug_writing_mode, self.i, self.b + ) + } +} + +// Can not implement the Zero trait: its zero() method does not have the `mode` parameter. +impl LogicalPoint { + #[inline] + pub fn zero(mode: WritingMode) -> LogicalPoint { + LogicalPoint { + i: Zero::zero(), + b: Zero::zero(), + debug_writing_mode: DebugWritingMode::new(mode), + } + } +} + +impl LogicalPoint { + #[inline] + pub fn new(mode: WritingMode, i: T, b: T) -> LogicalPoint { + LogicalPoint { + i: i, + b: b, + debug_writing_mode: DebugWritingMode::new(mode), + } + } +} + +impl> LogicalPoint { + #[inline] + pub fn from_physical( + mode: WritingMode, + point: Point2D, + container_size: Size2D, + ) -> LogicalPoint { + if mode.is_vertical() { + LogicalPoint { + i: if mode.is_inline_tb() { + point.y + } else { + container_size.height - point.y + }, + b: if mode.is_vertical_lr() { + point.x + } else { + container_size.width - point.x + }, + debug_writing_mode: DebugWritingMode::new(mode), + } + } else { + LogicalPoint { + i: if mode.is_bidi_ltr() { + point.x + } else { + container_size.width - point.x + }, + b: point.y, + debug_writing_mode: DebugWritingMode::new(mode), + } + } + } + + #[inline] + pub fn x(&self, mode: WritingMode, container_size: Size2D) -> T { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + if mode.is_vertical_lr() { + self.b + } else { + container_size.width - self.b + } + } else { + if mode.is_bidi_ltr() { + self.i + } else { + container_size.width - self.i + } + } + } + + #[inline] + pub fn set_x(&mut self, mode: WritingMode, x: T, container_size: Size2D) { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + self.b = if mode.is_vertical_lr() { + x + } else { + container_size.width - x + } + } else { + self.i = if mode.is_bidi_ltr() { + x + } else { + container_size.width - x + } + } + } + + #[inline] + pub fn y(&self, mode: WritingMode, container_size: Size2D) -> T { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + if mode.is_inline_tb() { + self.i + } else { + container_size.height - self.i + } + } else { + self.b + } + } + + #[inline] + pub fn set_y(&mut self, mode: WritingMode, y: T, container_size: Size2D) { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + self.i = if mode.is_inline_tb() { + y + } else { + container_size.height - y + } + } else { + self.b = y + } + } + + #[inline] + pub fn to_physical(&self, mode: WritingMode, container_size: Size2D) -> Point2D { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + Point2D::new( + if mode.is_vertical_lr() { + self.b + } else { + container_size.width - self.b + }, + if mode.is_inline_tb() { + self.i + } else { + container_size.height - self.i + }, + ) + } else { + Point2D::new( + if mode.is_bidi_ltr() { + self.i + } else { + container_size.width - self.i + }, + self.b, + ) + } + } + + #[inline] + pub fn convert( + &self, + mode_from: WritingMode, + mode_to: WritingMode, + container_size: Size2D, + ) -> LogicalPoint { + if mode_from == mode_to { + self.debug_writing_mode.check(mode_from); + *self + } else { + LogicalPoint::from_physical( + mode_to, + self.to_physical(mode_from, container_size), + container_size, + ) + } + } +} + +impl> LogicalPoint { + /// This doesn’t really makes sense, + /// but happens when dealing with multiple origins. + #[inline] + pub fn add_point(&self, other: &LogicalPoint) -> LogicalPoint { + self.debug_writing_mode + .check_debug(other.debug_writing_mode); + LogicalPoint { + debug_writing_mode: self.debug_writing_mode, + i: self.i + other.i, + b: self.b + other.b, + } + } +} + +impl> Add> for LogicalPoint { + type Output = LogicalPoint; + + #[inline] + fn add(self, other: LogicalSize) -> LogicalPoint { + self.debug_writing_mode + .check_debug(other.debug_writing_mode); + LogicalPoint { + debug_writing_mode: self.debug_writing_mode, + i: self.i + other.inline, + b: self.b + other.block, + } + } +} + +impl> Sub> for LogicalPoint { + type Output = LogicalPoint; + + #[inline] + fn sub(self, other: LogicalSize) -> LogicalPoint { + self.debug_writing_mode + .check_debug(other.debug_writing_mode); + LogicalPoint { + debug_writing_mode: self.debug_writing_mode, + i: self.i - other.inline, + b: self.b - other.block, + } + } +} + +/// A "margin" in flow-relative dimensions +/// Represents the four sides of the margins, borders, or padding of a CSS box, +/// or a combination of those. +/// A positive "margin" can be added to a rectangle to obtain a bigger rectangle. +#[derive(Clone, Copy, Eq, PartialEq)] +#[cfg_attr(feature = "servo", derive(Serialize))] +pub struct LogicalMargin { + pub block_start: T, + pub inline_end: T, + pub block_end: T, + pub inline_start: T, + debug_writing_mode: DebugWritingMode, +} + +impl Debug for LogicalMargin { + fn fmt(&self, formatter: &mut Formatter) -> Result<(), Error> { + let writing_mode_string = if cfg!(debug_assertions) { + format!("{:?}, ", self.debug_writing_mode) + } else { + "".to_owned() + }; + + write!( + formatter, + "LogicalMargin({}i:{:?}..{:?} b:{:?}..{:?})", + writing_mode_string, + self.inline_start, + self.inline_end, + self.block_start, + self.block_end + ) + } +} + +impl LogicalMargin { + #[inline] + pub fn zero(mode: WritingMode) -> LogicalMargin { + LogicalMargin { + block_start: Zero::zero(), + inline_end: Zero::zero(), + block_end: Zero::zero(), + inline_start: Zero::zero(), + debug_writing_mode: DebugWritingMode::new(mode), + } + } +} + +impl LogicalMargin { + #[inline] + pub fn new( + mode: WritingMode, + block_start: T, + inline_end: T, + block_end: T, + inline_start: T, + ) -> LogicalMargin { + LogicalMargin { + block_start, + inline_end, + block_end, + inline_start, + debug_writing_mode: DebugWritingMode::new(mode), + } + } + + #[inline] + pub fn from_physical(mode: WritingMode, offsets: SideOffsets2D) -> LogicalMargin { + let block_start; + let inline_end; + let block_end; + let inline_start; + if mode.is_vertical() { + if mode.is_vertical_lr() { + block_start = offsets.left; + block_end = offsets.right; + } else { + block_start = offsets.right; + block_end = offsets.left; + } + if mode.is_inline_tb() { + inline_start = offsets.top; + inline_end = offsets.bottom; + } else { + inline_start = offsets.bottom; + inline_end = offsets.top; + } + } else { + block_start = offsets.top; + block_end = offsets.bottom; + if mode.is_bidi_ltr() { + inline_start = offsets.left; + inline_end = offsets.right; + } else { + inline_start = offsets.right; + inline_end = offsets.left; + } + } + LogicalMargin::new(mode, block_start, inline_end, block_end, inline_start) + } +} + +impl LogicalMargin { + #[inline] + pub fn new_all_same(mode: WritingMode, value: T) -> LogicalMargin { + LogicalMargin::new(mode, value, value, value, value) + } + + #[inline] + pub fn top(&self, mode: WritingMode) -> T { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + if mode.is_inline_tb() { + self.inline_start + } else { + self.inline_end + } + } else { + self.block_start + } + } + + #[inline] + pub fn set_top(&mut self, mode: WritingMode, top: T) { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + if mode.is_inline_tb() { + self.inline_start = top + } else { + self.inline_end = top + } + } else { + self.block_start = top + } + } + + #[inline] + pub fn right(&self, mode: WritingMode) -> T { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + if mode.is_vertical_lr() { + self.block_end + } else { + self.block_start + } + } else { + if mode.is_bidi_ltr() { + self.inline_end + } else { + self.inline_start + } + } + } + + #[inline] + pub fn set_right(&mut self, mode: WritingMode, right: T) { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + if mode.is_vertical_lr() { + self.block_end = right + } else { + self.block_start = right + } + } else { + if mode.is_bidi_ltr() { + self.inline_end = right + } else { + self.inline_start = right + } + } + } + + #[inline] + pub fn bottom(&self, mode: WritingMode) -> T { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + if mode.is_inline_tb() { + self.inline_end + } else { + self.inline_start + } + } else { + self.block_end + } + } + + #[inline] + pub fn set_bottom(&mut self, mode: WritingMode, bottom: T) { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + if mode.is_inline_tb() { + self.inline_end = bottom + } else { + self.inline_start = bottom + } + } else { + self.block_end = bottom + } + } + + #[inline] + pub fn left(&self, mode: WritingMode) -> T { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + if mode.is_vertical_lr() { + self.block_start + } else { + self.block_end + } + } else { + if mode.is_bidi_ltr() { + self.inline_start + } else { + self.inline_end + } + } + } + + #[inline] + pub fn set_left(&mut self, mode: WritingMode, left: T) { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + if mode.is_vertical_lr() { + self.block_start = left + } else { + self.block_end = left + } + } else { + if mode.is_bidi_ltr() { + self.inline_start = left + } else { + self.inline_end = left + } + } + } + + #[inline] + pub fn to_physical(&self, mode: WritingMode) -> SideOffsets2D { + self.debug_writing_mode.check(mode); + let top; + let right; + let bottom; + let left; + if mode.is_vertical() { + if mode.is_vertical_lr() { + left = self.block_start; + right = self.block_end; + } else { + right = self.block_start; + left = self.block_end; + } + if mode.is_inline_tb() { + top = self.inline_start; + bottom = self.inline_end; + } else { + bottom = self.inline_start; + top = self.inline_end; + } + } else { + top = self.block_start; + bottom = self.block_end; + if mode.is_bidi_ltr() { + left = self.inline_start; + right = self.inline_end; + } else { + right = self.inline_start; + left = self.inline_end; + } + } + SideOffsets2D::new(top, right, bottom, left) + } + + #[inline] + pub fn convert(&self, mode_from: WritingMode, mode_to: WritingMode) -> LogicalMargin { + if mode_from == mode_to { + self.debug_writing_mode.check(mode_from); + *self + } else { + LogicalMargin::from_physical(mode_to, self.to_physical(mode_from)) + } + } +} + +impl LogicalMargin { + #[inline] + pub fn is_zero(&self) -> bool { + self.block_start == Zero::zero() && + self.inline_end == Zero::zero() && + self.block_end == Zero::zero() && + self.inline_start == Zero::zero() + } +} + +impl> LogicalMargin { + #[inline] + pub fn inline_start_end(&self) -> T { + self.inline_start + self.inline_end + } + + #[inline] + pub fn block_start_end(&self) -> T { + self.block_start + self.block_end + } + + #[inline] + pub fn start_end(&self, direction: Direction) -> T { + match direction { + Direction::Inline => self.inline_start + self.inline_end, + Direction::Block => self.block_start + self.block_end, + } + } + + #[inline] + pub fn top_bottom(&self, mode: WritingMode) -> T { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + self.inline_start_end() + } else { + self.block_start_end() + } + } + + #[inline] + pub fn left_right(&self, mode: WritingMode) -> T { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + self.block_start_end() + } else { + self.inline_start_end() + } + } +} + +impl> Add for LogicalMargin { + type Output = LogicalMargin; + + #[inline] + fn add(self, other: LogicalMargin) -> LogicalMargin { + self.debug_writing_mode + .check_debug(other.debug_writing_mode); + LogicalMargin { + debug_writing_mode: self.debug_writing_mode, + block_start: self.block_start + other.block_start, + inline_end: self.inline_end + other.inline_end, + block_end: self.block_end + other.block_end, + inline_start: self.inline_start + other.inline_start, + } + } +} + +impl> Sub for LogicalMargin { + type Output = LogicalMargin; + + #[inline] + fn sub(self, other: LogicalMargin) -> LogicalMargin { + self.debug_writing_mode + .check_debug(other.debug_writing_mode); + LogicalMargin { + debug_writing_mode: self.debug_writing_mode, + block_start: self.block_start - other.block_start, + inline_end: self.inline_end - other.inline_end, + block_end: self.block_end - other.block_end, + inline_start: self.inline_start - other.inline_start, + } + } +} + +/// A rectangle in flow-relative dimensions +#[derive(Clone, Copy, Eq, PartialEq)] +#[cfg_attr(feature = "servo", derive(Serialize))] +pub struct LogicalRect { + pub start: LogicalPoint, + pub size: LogicalSize, + debug_writing_mode: DebugWritingMode, +} + +impl Debug for LogicalRect { + fn fmt(&self, formatter: &mut Formatter) -> Result<(), Error> { + let writing_mode_string = if cfg!(debug_assertions) { + format!("{:?}, ", self.debug_writing_mode) + } else { + "".to_owned() + }; + + write!( + formatter, + "LogicalRect({}i{:?}×b{:?}, @ (i{:?},b{:?}))", + writing_mode_string, self.size.inline, self.size.block, self.start.i, self.start.b + ) + } +} + +impl LogicalRect { + #[inline] + pub fn zero(mode: WritingMode) -> LogicalRect { + LogicalRect { + start: LogicalPoint::zero(mode), + size: LogicalSize::zero(mode), + debug_writing_mode: DebugWritingMode::new(mode), + } + } +} + +impl LogicalRect { + #[inline] + pub fn new( + mode: WritingMode, + inline_start: T, + block_start: T, + inline: T, + block: T, + ) -> LogicalRect { + LogicalRect { + start: LogicalPoint::new(mode, inline_start, block_start), + size: LogicalSize::new(mode, inline, block), + debug_writing_mode: DebugWritingMode::new(mode), + } + } + + #[inline] + pub fn from_point_size( + mode: WritingMode, + start: LogicalPoint, + size: LogicalSize, + ) -> LogicalRect { + start.debug_writing_mode.check(mode); + size.debug_writing_mode.check(mode); + LogicalRect { + start: start, + size: size, + debug_writing_mode: DebugWritingMode::new(mode), + } + } +} + +impl + Sub> LogicalRect { + #[inline] + pub fn from_physical( + mode: WritingMode, + rect: Rect, + container_size: Size2D, + ) -> LogicalRect { + let inline_start; + let block_start; + let inline; + let block; + if mode.is_vertical() { + inline = rect.size.height; + block = rect.size.width; + if mode.is_vertical_lr() { + block_start = rect.origin.x; + } else { + block_start = container_size.width - (rect.origin.x + rect.size.width); + } + if mode.is_inline_tb() { + inline_start = rect.origin.y; + } else { + inline_start = container_size.height - (rect.origin.y + rect.size.height); + } + } else { + inline = rect.size.width; + block = rect.size.height; + block_start = rect.origin.y; + if mode.is_bidi_ltr() { + inline_start = rect.origin.x; + } else { + inline_start = container_size.width - (rect.origin.x + rect.size.width); + } + } + LogicalRect { + start: LogicalPoint::new(mode, inline_start, block_start), + size: LogicalSize::new(mode, inline, block), + debug_writing_mode: DebugWritingMode::new(mode), + } + } + + #[inline] + pub fn inline_end(&self) -> T { + self.start.i + self.size.inline + } + + #[inline] + pub fn block_end(&self) -> T { + self.start.b + self.size.block + } + + #[inline] + pub fn to_physical(&self, mode: WritingMode, container_size: Size2D) -> Rect { + self.debug_writing_mode.check(mode); + let x; + let y; + let width; + let height; + if mode.is_vertical() { + width = self.size.block; + height = self.size.inline; + if mode.is_vertical_lr() { + x = self.start.b; + } else { + x = container_size.width - self.block_end(); + } + if mode.is_inline_tb() { + y = self.start.i; + } else { + y = container_size.height - self.inline_end(); + } + } else { + width = self.size.inline; + height = self.size.block; + y = self.start.b; + if mode.is_bidi_ltr() { + x = self.start.i; + } else { + x = container_size.width - self.inline_end(); + } + } + Rect { + origin: Point2D::new(x, y), + size: Size2D::new(width, height), + } + } + + #[inline] + pub fn convert( + &self, + mode_from: WritingMode, + mode_to: WritingMode, + container_size: Size2D, + ) -> LogicalRect { + if mode_from == mode_to { + self.debug_writing_mode.check(mode_from); + *self + } else { + LogicalRect::from_physical( + mode_to, + self.to_physical(mode_from, container_size), + container_size, + ) + } + } + + pub fn translate_by_size(&self, offset: LogicalSize) -> LogicalRect { + LogicalRect { + start: self.start + offset, + ..*self + } + } + + pub fn translate(&self, offset: &LogicalPoint) -> LogicalRect { + LogicalRect { + start: self.start + + LogicalSize { + inline: offset.i, + block: offset.b, + debug_writing_mode: offset.debug_writing_mode, + }, + size: self.size, + debug_writing_mode: self.debug_writing_mode, + } + } +} + +impl + Sub> LogicalRect { + #[inline] + pub fn union(&self, other: &LogicalRect) -> LogicalRect { + self.debug_writing_mode + .check_debug(other.debug_writing_mode); + + let inline_start = min(self.start.i, other.start.i); + let block_start = min(self.start.b, other.start.b); + LogicalRect { + start: LogicalPoint { + i: inline_start, + b: block_start, + debug_writing_mode: self.debug_writing_mode, + }, + size: LogicalSize { + inline: max(self.inline_end(), other.inline_end()) - inline_start, + block: max(self.block_end(), other.block_end()) - block_start, + debug_writing_mode: self.debug_writing_mode, + }, + debug_writing_mode: self.debug_writing_mode, + } + } +} + +impl + Sub> Add> for LogicalRect { + type Output = LogicalRect; + + #[inline] + fn add(self, other: LogicalMargin) -> LogicalRect { + self.debug_writing_mode + .check_debug(other.debug_writing_mode); + LogicalRect { + start: LogicalPoint { + // Growing a rectangle on the start side means pushing its + // start point on the negative direction. + i: self.start.i - other.inline_start, + b: self.start.b - other.block_start, + debug_writing_mode: self.debug_writing_mode, + }, + size: LogicalSize { + inline: self.size.inline + other.inline_start_end(), + block: self.size.block + other.block_start_end(), + debug_writing_mode: self.debug_writing_mode, + }, + debug_writing_mode: self.debug_writing_mode, + } + } +} + +impl + Sub> Sub> for LogicalRect { + type Output = LogicalRect; + + #[inline] + fn sub(self, other: LogicalMargin) -> LogicalRect { + self.debug_writing_mode + .check_debug(other.debug_writing_mode); + LogicalRect { + start: LogicalPoint { + // Shrinking a rectangle on the start side means pushing its + // start point on the positive direction. + i: self.start.i + other.inline_start, + b: self.start.b + other.block_start, + debug_writing_mode: self.debug_writing_mode, + }, + size: LogicalSize { + inline: self.size.inline - other.inline_start_end(), + block: self.size.block - other.block_start_end(), + debug_writing_mode: self.debug_writing_mode, + }, + debug_writing_mode: self.debug_writing_mode, + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum PhysicalSide { + Top, + Right, + Bottom, + Left, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum PhysicalCorner { + TopLeft, + TopRight, + BottomRight, + BottomLeft, +} diff --git a/servo/components/style/macros.rs b/servo/components/style/macros.rs new file mode 100644 index 0000000000..5f3a1ea463 --- /dev/null +++ b/servo/components/style/macros.rs @@ -0,0 +1,98 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Various macro helpers. + +macro_rules! exclusive_value { + (($value:ident, $set:expr) => $ident:path) => { + if $value.intersects($set) { + return Err(()); + } else { + $ident + } + }; +} + +#[cfg(feature = "gecko")] +macro_rules! impl_gecko_keyword_conversions { + ($name:ident, $utype:ty) => { + impl From<$utype> for $name { + fn from(bits: $utype) -> $name { + $name::from_gecko_keyword(bits) + } + } + + impl From<$name> for $utype { + fn from(v: $name) -> $utype { + v.to_gecko_keyword() + } + } + }; +} + +macro_rules! trivial_to_computed_value { + ($name:ty) => { + impl $crate::values::computed::ToComputedValue for $name { + type ComputedValue = $name; + + fn to_computed_value(&self, _: &$crate::values::computed::Context) -> Self { + self.clone() + } + + fn from_computed_value(other: &Self) -> Self { + other.clone() + } + } + }; +} + +/// A macro to parse an identifier, or return an `UnexpectedIdent` error +/// otherwise. +/// +/// FIXME(emilio): The fact that `UnexpectedIdent` is a `SelectorParseError` +/// doesn't make a lot of sense to me. +macro_rules! try_match_ident_ignore_ascii_case { + ($input:expr, $( $match_body:tt )*) => {{ + let location = $input.current_source_location(); + let ident = $input.expect_ident_cloned()?; + match_ignore_ascii_case! { &ident, + $( $match_body )* + _ => return Err(location.new_custom_error( + ::selectors::parser::SelectorParseErrorKind::UnexpectedIdent(ident.clone()) + )) + } + }} +} + +#[cfg(feature = "servo")] +macro_rules! local_name { + ($s:tt) => { + $crate::values::GenericAtomIdent(html5ever::local_name!($s)) + }; +} + +#[cfg(feature = "servo")] +macro_rules! ns { + () => { + $crate::values::GenericAtomIdent(html5ever::ns!()) + }; + ($s:tt) => { + $crate::values::GenericAtomIdent(html5ever::ns!($s)) + }; +} + +#[cfg(feature = "gecko")] +macro_rules! local_name { + ($s:tt) => { + $crate::values::AtomIdent(atom!($s)) + }; +} + +/// Asserts the size of a type at compile time. +macro_rules! size_of_test { + ($t: ty, $expected_size: expr) => { + #[cfg(target_pointer_width = "64")] + const_assert_eq!(std::mem::size_of::<$t>(), $expected_size); + }; +} diff --git a/servo/components/style/matching.rs b/servo/components/style/matching.rs new file mode 100644 index 0000000000..4a2e72b6c8 --- /dev/null +++ b/servo/components/style/matching.rs @@ -0,0 +1,1091 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! High-level interface to CSS selector matching. + +#![allow(unsafe_code)] +#![deny(missing_docs)] + +use crate::computed_value_flags::ComputedValueFlags; +use crate::context::{CascadeInputs, ElementCascadeInputs, QuirksMode}; +use crate::context::{SharedStyleContext, StyleContext}; +use crate::data::{ElementData, ElementStyles}; +use crate::dom::TElement; +#[cfg(feature = "servo")] +use crate::dom::TNode; +use crate::invalidation::element::restyle_hints::RestyleHint; +use crate::properties::longhands::display::computed_value::T as Display; +use crate::properties::ComputedValues; +use crate::properties::PropertyDeclarationBlock; +use crate::rule_tree::{CascadeLevel, StrongRuleNode}; +use crate::selector_parser::{PseudoElement, RestyleDamage}; +use crate::shared_lock::Locked; +use crate::style_resolver::ResolvedElementStyles; +use crate::style_resolver::{PseudoElementResolution, StyleResolverForElement}; +use crate::stylesheets::layer_rule::LayerOrder; +use crate::stylist::RuleInclusion; +use crate::traversal_flags::TraversalFlags; +use servo_arc::{Arc, ArcBorrow}; + +/// Represents the result of comparing an element's old and new style. +#[derive(Debug)] +pub struct StyleDifference { + /// The resulting damage. + pub damage: RestyleDamage, + + /// Whether any styles changed. + pub change: StyleChange, +} + +/// Represents whether or not the style of an element has changed. +#[derive(Clone, Copy, Debug)] +pub enum StyleChange { + /// The style hasn't changed. + Unchanged, + /// The style has changed. + Changed { + /// Whether only reset structs changed. + reset_only: bool, + }, +} + +/// Whether or not newly computed values for an element need to be cascaded to +/// children (or children might need to be re-matched, e.g., for container +/// queries). +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub enum ChildRestyleRequirement { + /// Old and new computed values were the same, or we otherwise know that + /// we won't bother recomputing style for children, so we can skip cascading + /// the new values into child elements. + CanSkipCascade = 0, + /// The same as `MustCascadeChildren`, but we only need to actually + /// recascade if the child inherits any explicit reset style. + MustCascadeChildrenIfInheritResetStyle = 1, + /// Old and new computed values were different, so we must cascade the + /// new values to children. + MustCascadeChildren = 2, + /// The same as `MustCascadeChildren`, but for the entire subtree. This is + /// used to handle root font-size updates needing to recascade the whole + /// document. + MustCascadeDescendants = 3, + /// We need to re-match the whole subttree. This is used to handle container + /// query relative unit changes for example. Container query size changes + /// also trigger re-match, but after layout. + MustMatchDescendants = 4, +} + +/// Determines which styles are being cascaded currently. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum CascadeVisitedMode { + /// Cascade the regular, unvisited styles. + Unvisited, + /// Cascade the styles used when an element's relevant link is visited. A + /// "relevant link" is the element being matched if it is a link or the + /// nearest ancestor link. + Visited, +} + +trait PrivateMatchMethods: TElement { + fn replace_single_rule_node( + context: &SharedStyleContext, + level: CascadeLevel, + layer_order: LayerOrder, + pdb: Option>>, + path: &mut StrongRuleNode, + ) -> bool { + let stylist = &context.stylist; + let guards = &context.guards; + + let mut important_rules_changed = false; + let new_node = stylist.rule_tree().update_rule_at_level( + level, + layer_order, + pdb, + path, + guards, + &mut important_rules_changed, + ); + if let Some(n) = new_node { + *path = n; + } + important_rules_changed + } + + /// Updates the rule nodes without re-running selector matching, using just + /// the rule tree, for a specific visited mode. + /// + /// Returns true if an !important rule was replaced. + fn replace_rules_internal( + &self, + replacements: RestyleHint, + context: &mut StyleContext, + cascade_visited: CascadeVisitedMode, + cascade_inputs: &mut ElementCascadeInputs, + ) -> bool { + debug_assert!( + replacements.intersects(RestyleHint::replacements()) && + (replacements & !RestyleHint::replacements()).is_empty() + ); + + let primary_rules = match cascade_visited { + CascadeVisitedMode::Unvisited => cascade_inputs.primary.rules.as_mut(), + CascadeVisitedMode::Visited => cascade_inputs.primary.visited_rules.as_mut(), + }; + + let primary_rules = match primary_rules { + Some(r) => r, + None => return false, + }; + + if !context.shared.traversal_flags.for_animation_only() { + let mut result = false; + if replacements.contains(RestyleHint::RESTYLE_STYLE_ATTRIBUTE) { + let style_attribute = self.style_attribute(); + result |= Self::replace_single_rule_node( + context.shared, + CascadeLevel::same_tree_author_normal(), + LayerOrder::root(), + style_attribute, + primary_rules, + ); + result |= Self::replace_single_rule_node( + context.shared, + CascadeLevel::same_tree_author_important(), + LayerOrder::root(), + style_attribute, + primary_rules, + ); + // FIXME(emilio): Still a hack! + self.unset_dirty_style_attribute(); + } + return result; + } + + // Animation restyle hints are processed prior to other restyle + // hints in the animation-only traversal. + // + // Non-animation restyle hints will be processed in a subsequent + // normal traversal. + if replacements.intersects(RestyleHint::for_animations()) { + debug_assert!(context.shared.traversal_flags.for_animation_only()); + + if replacements.contains(RestyleHint::RESTYLE_SMIL) { + Self::replace_single_rule_node( + context.shared, + CascadeLevel::SMILOverride, + LayerOrder::root(), + self.smil_override(), + primary_rules, + ); + } + + if replacements.contains(RestyleHint::RESTYLE_CSS_TRANSITIONS) { + Self::replace_single_rule_node( + context.shared, + CascadeLevel::Transitions, + LayerOrder::root(), + self.transition_rule(&context.shared) + .as_ref() + .map(|a| a.borrow_arc()), + primary_rules, + ); + } + + if replacements.contains(RestyleHint::RESTYLE_CSS_ANIMATIONS) { + Self::replace_single_rule_node( + context.shared, + CascadeLevel::Animations, + LayerOrder::root(), + self.animation_rule(&context.shared) + .as_ref() + .map(|a| a.borrow_arc()), + primary_rules, + ); + } + } + + false + } + + /// If there is no transition rule in the ComputedValues, it returns None. + fn after_change_style( + &self, + context: &mut StyleContext, + primary_style: &Arc, + ) -> Option> { + let rule_node = primary_style.rules(); + let without_transition_rules = context + .shared + .stylist + .rule_tree() + .remove_transition_rule_if_applicable(rule_node); + if without_transition_rules == *rule_node { + // We don't have transition rule in this case, so return None to let + // the caller use the original ComputedValues. + return None; + } + + // FIXME(bug 868975): We probably need to transition visited style as + // well. + let inputs = CascadeInputs { + rules: Some(without_transition_rules), + visited_rules: primary_style.visited_rules().cloned(), + flags: primary_style.flags.for_cascade_inputs(), + }; + + // Actually `PseudoElementResolution` doesn't really matter. + let style = StyleResolverForElement::new( + *self, + context, + RuleInclusion::All, + PseudoElementResolution::IfApplicable, + ) + .cascade_style_and_visited_with_default_parents(inputs); + + Some(style.0) + } + + fn needs_animations_update( + &self, + context: &mut StyleContext, + old_style: Option<&ComputedValues>, + new_style: &ComputedValues, + pseudo_element: Option, + ) -> bool { + let new_ui_style = new_style.get_ui(); + let new_style_specifies_animations = new_ui_style.specifies_animations(); + + let has_animations = self.has_css_animations(&context.shared, pseudo_element); + if !new_style_specifies_animations && !has_animations { + return false; + } + + let old_style = match old_style { + Some(old) => old, + // If we have no old style but have animations, we may be a + // pseudo-element which was re-created without style changes. + // + // This can happen when we reframe the pseudo-element without + // restyling it (due to content insertion on a flex container or + // such, for example). See bug 1564366. + // + // FIXME(emilio): The really right fix for this is keeping the + // pseudo-element itself around on reframes, but that's a bit + // harder. If we do that we can probably remove quite a lot of the + // EffectSet complexity though, since right now it's stored on the + // parent element for pseudo-elements given we need to keep it + // around... + None => { + return new_style_specifies_animations || new_style.is_pseudo_style(); + }, + }; + + let old_ui_style = old_style.get_ui(); + + let keyframes_could_have_changed = context + .shared + .traversal_flags + .contains(TraversalFlags::ForCSSRuleChanges); + + // If the traversal is triggered due to changes in CSS rules changes, we + // need to try to update all CSS animations on the element if the + // element has or will have CSS animation style regardless of whether + // the animation is running or not. + // + // TODO: We should check which @keyframes were added/changed/deleted and + // update only animations corresponding to those @keyframes. + if keyframes_could_have_changed { + return true; + } + + // If the animations changed, well... + if !old_ui_style.animations_equals(new_ui_style) { + return true; + } + + let old_display = old_style.clone_display(); + let new_display = new_style.clone_display(); + + // If we were display: none, we may need to trigger animations. + if old_display == Display::None && new_display != Display::None { + return new_style_specifies_animations; + } + + // If we are becoming display: none, we may need to stop animations. + if old_display != Display::None && new_display == Display::None { + return has_animations; + } + + // We might need to update animations if writing-mode or direction + // changed, and any of the animations contained logical properties. + // + // We may want to be more granular, but it's probably not worth it. + if new_style.writing_mode != old_style.writing_mode { + return has_animations; + } + + false + } + + fn might_need_transitions_update( + &self, + context: &StyleContext, + old_style: Option<&ComputedValues>, + new_style: &ComputedValues, + pseudo_element: Option, + ) -> bool { + let old_style = match old_style { + Some(v) => v, + None => return false, + }; + + if !self.has_css_transitions(context.shared, pseudo_element) && + !new_style.get_ui().specifies_transitions() + { + return false; + } + + if old_style.clone_display().is_none() { + return false; + } + + return true; + } + + /// Create a SequentialTask for resolving descendants in a SMIL display + /// property animation if the display property changed from none. + #[cfg(feature = "gecko")] + fn handle_display_change_for_smil_if_needed( + &self, + context: &mut StyleContext, + old_values: Option<&ComputedValues>, + new_values: &ComputedValues, + restyle_hints: RestyleHint, + ) { + use crate::context::PostAnimationTasks; + + if !restyle_hints.intersects(RestyleHint::RESTYLE_SMIL) { + return; + } + + if new_values.is_display_property_changed_from_none(old_values) { + // When display value is changed from none to other, we need to + // traverse descendant elements in a subsequent normal + // traversal (we can't traverse them in this animation-only restyle + // since we have no way to know whether the decendants + // need to be traversed at the beginning of the animation-only + // restyle). + let task = crate::context::SequentialTask::process_post_animation( + *self, + PostAnimationTasks::DISPLAY_CHANGED_FROM_NONE_FOR_SMIL, + ); + context.thread_local.tasks.push(task); + } + } + + #[cfg(feature = "gecko")] + fn process_animations( + &self, + context: &mut StyleContext, + old_styles: &mut ElementStyles, + new_styles: &mut ResolvedElementStyles, + restyle_hint: RestyleHint, + important_rules_changed: bool, + ) { + use crate::context::UpdateAnimationsTasks; + + let new_values = new_styles.primary_style_mut(); + let old_values = &old_styles.primary; + if context.shared.traversal_flags.for_animation_only() { + self.handle_display_change_for_smil_if_needed( + context, + old_values.as_deref(), + new_values, + restyle_hint, + ); + return; + } + + // Bug 868975: These steps should examine and update the visited styles + // in addition to the unvisited styles. + + let mut tasks = UpdateAnimationsTasks::empty(); + + if old_values.as_deref().map_or_else( + || new_values.get_ui().specifies_scroll_timelines(), + |old| !old.get_ui().scroll_timelines_equals(new_values.get_ui()), + ) { + tasks.insert(UpdateAnimationsTasks::SCROLL_TIMELINES); + } + + if old_values.as_deref().map_or_else( + || new_values.get_ui().specifies_view_timelines(), + |old| !old.get_ui().view_timelines_equals(new_values.get_ui()), + ) { + tasks.insert(UpdateAnimationsTasks::VIEW_TIMELINES); + } + + if self.needs_animations_update( + context, + old_values.as_deref(), + new_values, + /* pseudo_element = */ None, + ) { + tasks.insert(UpdateAnimationsTasks::CSS_ANIMATIONS); + } + + let before_change_style = if self.might_need_transitions_update( + context, + old_values.as_deref(), + new_values, + /* pseudo_element = */ None, + ) { + let after_change_style = + if self.has_css_transitions(context.shared, /* pseudo_element = */ None) { + self.after_change_style(context, new_values) + } else { + None + }; + + // In order to avoid creating a SequentialTask for transitions which + // may not be updated, we check it per property to make sure Gecko + // side will really update transition. + let needs_transitions_update = { + // We borrow new_values here, so need to add a scope to make + // sure we release it before assigning a new value to it. + let after_change_style_ref = after_change_style.as_ref().unwrap_or(&new_values); + + self.needs_transitions_update(old_values.as_ref().unwrap(), after_change_style_ref) + }; + + if needs_transitions_update { + if let Some(values_without_transitions) = after_change_style { + *new_values = values_without_transitions; + } + tasks.insert(UpdateAnimationsTasks::CSS_TRANSITIONS); + + // We need to clone old_values into SequentialTask, so we can + // use it later. + old_values.clone() + } else { + None + } + } else { + None + }; + + if self.has_animations(&context.shared) { + tasks.insert(UpdateAnimationsTasks::EFFECT_PROPERTIES); + if important_rules_changed { + tasks.insert(UpdateAnimationsTasks::CASCADE_RESULTS); + } + if new_values.is_display_property_changed_from_none(old_values.as_deref()) { + tasks.insert(UpdateAnimationsTasks::DISPLAY_CHANGED_FROM_NONE); + } + } + + if !tasks.is_empty() { + let task = crate::context::SequentialTask::update_animations( + *self, + before_change_style, + tasks, + ); + context.thread_local.tasks.push(task); + } + } + + #[cfg(feature = "servo")] + fn process_animations( + &self, + context: &mut StyleContext, + old_styles: &mut ElementStyles, + new_resolved_styles: &mut ResolvedElementStyles, + _restyle_hint: RestyleHint, + _important_rules_changed: bool, + ) { + use crate::animation::AnimationSetKey; + use crate::dom::TDocument; + + let style_changed = self.process_animations_for_style( + context, + &mut old_styles.primary, + new_resolved_styles.primary_style_mut(), + /* pseudo_element = */ None, + ); + + // If we have modified animation or transitions, we recascade style for this node. + if style_changed { + let mut rule_node = new_resolved_styles.primary_style().rules().clone(); + let declarations = context.shared.animations.get_all_declarations( + &AnimationSetKey::new_for_non_pseudo(self.as_node().opaque()), + context.shared.current_time_for_animations, + self.as_node().owner_doc().shared_lock(), + ); + Self::replace_single_rule_node( + &context.shared, + CascadeLevel::Transitions, + declarations.transitions.as_ref().map(|a| a.borrow_arc()), + &mut rule_node, + ); + Self::replace_single_rule_node( + &context.shared, + CascadeLevel::Animations, + declarations.animations.as_ref().map(|a| a.borrow_arc()), + &mut rule_node, + ); + + if rule_node != *new_resolved_styles.primary_style().rules() { + let inputs = CascadeInputs { + rules: Some(rule_node), + visited_rules: new_resolved_styles.primary_style().visited_rules().cloned(), + }; + + new_resolved_styles.primary.style = StyleResolverForElement::new( + *self, + context, + RuleInclusion::All, + PseudoElementResolution::IfApplicable, + ) + .cascade_style_and_visited_with_default_parents(inputs); + } + } + + self.process_animations_for_pseudo( + context, + old_styles, + new_resolved_styles, + PseudoElement::Before, + ); + self.process_animations_for_pseudo( + context, + old_styles, + new_resolved_styles, + PseudoElement::After, + ); + } + + #[cfg(feature = "servo")] + fn process_animations_for_pseudo( + &self, + context: &mut StyleContext, + old_styles: &mut ElementStyles, + new_resolved_styles: &mut ResolvedElementStyles, + pseudo_element: PseudoElement, + ) { + use crate::animation::AnimationSetKey; + use crate::dom::TDocument; + + let key = AnimationSetKey::new_for_pseudo(self.as_node().opaque(), pseudo_element.clone()); + let mut style = match new_resolved_styles.pseudos.get(&pseudo_element) { + Some(style) => Arc::clone(style), + None => { + context + .shared + .animations + .cancel_all_animations_for_key(&key); + return; + }, + }; + + let mut old_style = old_styles.pseudos.get(&pseudo_element).cloned(); + self.process_animations_for_style( + context, + &mut old_style, + &mut style, + Some(pseudo_element.clone()), + ); + + let declarations = context.shared.animations.get_all_declarations( + &key, + context.shared.current_time_for_animations, + self.as_node().owner_doc().shared_lock(), + ); + if declarations.is_empty() { + return; + } + + let mut rule_node = style.rules().clone(); + Self::replace_single_rule_node( + &context.shared, + CascadeLevel::Transitions, + LayerOrder::root(), + declarations.transitions.as_ref().map(|a| a.borrow_arc()), + &mut rule_node, + ); + Self::replace_single_rule_node( + &context.shared, + CascadeLevel::Animations, + LayerOrder::root(), + declarations.animations.as_ref().map(|a| a.borrow_arc()), + &mut rule_node, + ); + if rule_node == *style.rules() { + return; + } + + let inputs = CascadeInputs { + rules: Some(rule_node), + visited_rules: style.visited_rules().cloned(), + }; + + let new_style = StyleResolverForElement::new( + *self, + context, + RuleInclusion::All, + PseudoElementResolution::IfApplicable, + ) + .cascade_style_and_visited_for_pseudo_with_default_parents( + inputs, + &pseudo_element, + &new_resolved_styles.primary, + ); + + new_resolved_styles + .pseudos + .set(&pseudo_element, new_style.0); + } + + #[cfg(feature = "servo")] + fn process_animations_for_style( + &self, + context: &mut StyleContext, + old_values: &mut Option>, + new_values: &mut Arc, + pseudo_element: Option, + ) -> bool { + use crate::animation::{AnimationSetKey, AnimationState}; + + // We need to call this before accessing the `ElementAnimationSet` from the + // map because this call will do a RwLock::read(). + let needs_animations_update = self.needs_animations_update( + context, + old_values.as_deref(), + new_values, + pseudo_element, + ); + + let might_need_transitions_update = self.might_need_transitions_update( + context, + old_values.as_deref(), + new_values, + pseudo_element, + ); + + let mut after_change_style = None; + if might_need_transitions_update { + after_change_style = self.after_change_style(context, new_values); + } + + let key = AnimationSetKey::new(self.as_node().opaque(), pseudo_element); + let shared_context = context.shared; + let mut animation_set = shared_context + .animations + .sets + .write() + .remove(&key) + .unwrap_or_default(); + + // Starting animations is expensive, because we have to recalculate the style + // for all the keyframes. We only want to do this if we think that there's a + // chance that the animations really changed. + if needs_animations_update { + let mut resolver = StyleResolverForElement::new( + *self, + context, + RuleInclusion::All, + PseudoElementResolution::IfApplicable, + ); + + animation_set.update_animations_for_new_style::( + *self, + &shared_context, + &new_values, + &mut resolver, + ); + } + + animation_set.update_transitions_for_new_style( + might_need_transitions_update, + &shared_context, + old_values.as_ref(), + after_change_style.as_ref().unwrap_or(new_values), + ); + + // We clear away any finished transitions, but retain animations, because they + // might still be used for proper calculation of `animation-fill-mode`. This + // should change the computed values in the style, so we don't need to mark + // this set as dirty. + animation_set + .transitions + .retain(|transition| transition.state != AnimationState::Finished); + + // If the ElementAnimationSet is empty, and don't store it in order to + // save memory and to avoid extra processing later. + let changed_animations = animation_set.dirty; + if !animation_set.is_empty() { + animation_set.dirty = false; + shared_context + .animations + .sets + .write() + .insert(key, animation_set); + } + + changed_animations + } + + /// Computes and applies non-redundant damage. + fn accumulate_damage_for( + &self, + shared_context: &SharedStyleContext, + damage: &mut RestyleDamage, + old_values: &ComputedValues, + new_values: &ComputedValues, + pseudo: Option<&PseudoElement>, + ) -> ChildRestyleRequirement { + debug!("accumulate_damage_for: {:?}", self); + debug_assert!(!shared_context + .traversal_flags + .contains(TraversalFlags::FinalAnimationTraversal)); + + let difference = self.compute_style_difference(old_values, new_values, pseudo); + + *damage |= difference.damage; + + debug!(" > style difference: {:?}", difference); + + // We need to cascade the children in order to ensure the correct + // propagation of inherited computed value flags. + if old_values.flags.maybe_inherited() != new_values.flags.maybe_inherited() { + debug!( + " > flags changed: {:?} != {:?}", + old_values.flags, new_values.flags + ); + return ChildRestyleRequirement::MustCascadeChildren; + } + + match difference.change { + StyleChange::Unchanged => return ChildRestyleRequirement::CanSkipCascade, + StyleChange::Changed { reset_only } => { + // If inherited properties changed, the best we can do is + // cascade the children. + if !reset_only { + return ChildRestyleRequirement::MustCascadeChildren; + } + }, + } + + let old_display = old_values.clone_display(); + let new_display = new_values.clone_display(); + + if old_display != new_display { + // If we used to be a display: none element, and no longer are, our + // children need to be restyled because they're unstyled. + if old_display == Display::None { + return ChildRestyleRequirement::MustCascadeChildren; + } + // Blockification of children may depend on our display value, + // so we need to actually do the recascade. We could potentially + // do better, but it doesn't seem worth it. + if old_display.is_item_container() != new_display.is_item_container() { + return ChildRestyleRequirement::MustCascadeChildren; + } + // We may also need to blockify and un-blockify descendants if our + // display goes from / to display: contents, since the "layout + // parent style" changes. + if old_display.is_contents() || new_display.is_contents() { + return ChildRestyleRequirement::MustCascadeChildren; + } + // Line break suppression may also be affected if the display + // type changes from ruby to non-ruby. + #[cfg(feature = "gecko")] + { + if old_display.is_ruby_type() != new_display.is_ruby_type() { + return ChildRestyleRequirement::MustCascadeChildren; + } + } + } + + // Children with justify-items: auto may depend on our + // justify-items property value. + // + // Similarly, we could potentially do better, but this really + // seems not common enough to care about. + #[cfg(feature = "gecko")] + { + use crate::values::specified::align::AlignFlags; + + let old_justify_items = old_values.get_position().clone_justify_items(); + let new_justify_items = new_values.get_position().clone_justify_items(); + + let was_legacy_justify_items = + old_justify_items.computed.0.contains(AlignFlags::LEGACY); + + let is_legacy_justify_items = new_justify_items.computed.0.contains(AlignFlags::LEGACY); + + if is_legacy_justify_items != was_legacy_justify_items { + return ChildRestyleRequirement::MustCascadeChildren; + } + + if was_legacy_justify_items && old_justify_items.computed != new_justify_items.computed + { + return ChildRestyleRequirement::MustCascadeChildren; + } + } + + #[cfg(feature = "servo")] + { + // We may need to set or propagate the CAN_BE_FRAGMENTED bit + // on our children. + if old_values.is_multicol() != new_values.is_multicol() { + return ChildRestyleRequirement::MustCascadeChildren; + } + } + + // We could prove that, if our children don't inherit reset + // properties, we can stop the cascade. + ChildRestyleRequirement::MustCascadeChildrenIfInheritResetStyle + } +} + +impl PrivateMatchMethods for E {} + +/// The public API that elements expose for selector matching. +pub trait MatchMethods: TElement { + /// Returns the closest parent element that doesn't have a display: contents + /// style (and thus generates a box). + /// + /// This is needed to correctly handle blockification of flex and grid + /// items. + /// + /// Returns itself if the element has no parent. In practice this doesn't + /// happen because the root element is blockified per spec, but it could + /// happen if we decide to not blockify for roots of disconnected subtrees, + /// which is a kind of dubious behavior. + fn layout_parent(&self) -> Self { + let mut current = self.clone(); + loop { + current = match current.traversal_parent() { + Some(el) => el, + None => return current, + }; + + let is_display_contents = current + .borrow_data() + .unwrap() + .styles + .primary() + .is_display_contents(); + + if !is_display_contents { + return current; + } + } + } + + /// Updates the styles with the new ones, diffs them, and stores the restyle + /// damage. + fn finish_restyle( + &self, + context: &mut StyleContext, + data: &mut ElementData, + mut new_styles: ResolvedElementStyles, + important_rules_changed: bool, + ) -> ChildRestyleRequirement { + use std::cmp; + + self.process_animations( + context, + &mut data.styles, + &mut new_styles, + data.hint, + important_rules_changed, + ); + + // First of all, update the styles. + let old_styles = data.set_styles(new_styles); + + let new_primary_style = data.styles.primary.as_ref().unwrap(); + + let mut restyle_requirement = ChildRestyleRequirement::CanSkipCascade; + let is_root = new_primary_style + .flags + .contains(ComputedValueFlags::IS_ROOT_ELEMENT_STYLE); + let is_container = !new_primary_style + .get_box() + .clone_container_type() + .is_normal(); + if is_root || is_container { + let new_font_size = new_primary_style.get_font().clone_font_size(); + let old_font_size = old_styles + .primary + .as_ref() + .map(|s| s.get_font().clone_font_size()); + + if old_font_size != Some(new_font_size) { + if is_root { + let device = context.shared.stylist.device(); + debug_assert!(self.owner_doc_matches_for_testing(device)); + device.set_root_font_size(new_font_size.computed_size().into()); + if device.used_root_font_size() { + // If the root font-size changed since last time, and something + // in the document did use rem units, ensure we recascade the + // entire tree. + restyle_requirement = ChildRestyleRequirement::MustCascadeDescendants; + } + } + + if is_container && old_font_size.is_some() { + // TODO(emilio): Maybe only do this if we were matched + // against relative font sizes? + // Also, maybe we should do this as well for font-family / + // etc changes (for ex/ch/ic units to work correctly)? We + // should probably do the optimization mentioned above if + // so. + restyle_requirement = ChildRestyleRequirement::MustMatchDescendants; + } + } + } + + if context.shared.stylist.quirks_mode() == QuirksMode::Quirks { + if self.is_html_document_body_element() { + // NOTE(emilio): We _could_ handle dynamic changes to it if it + // changes and before we reach our children the cascade stops, + // but we don't track right now whether we use the document body + // color, and nobody else handles that properly anyway. + let device = context.shared.stylist.device(); + + // Needed for the "inherit from body" quirk. + let text_color = new_primary_style.get_inherited_text().clone_color(); + device.set_body_text_color(text_color); + } + } + + // Don't accumulate damage if we're in the final animation traversal. + if context + .shared + .traversal_flags + .contains(TraversalFlags::FinalAnimationTraversal) + { + return ChildRestyleRequirement::MustCascadeChildren; + } + + // Also, don't do anything if there was no style. + let old_primary_style = match old_styles.primary { + Some(s) => s, + None => return ChildRestyleRequirement::MustCascadeChildren, + }; + + let old_container_type = old_primary_style.clone_container_type(); + let new_container_type = new_primary_style.clone_container_type(); + if old_container_type != new_container_type && !new_container_type.is_size_container_type() + { + // Stopped being a size container. Re-evaluate container queries and units on all our descendants. + // Changes into and between different size containment is handled in `UpdateContainerQueryStyles`. + restyle_requirement = ChildRestyleRequirement::MustMatchDescendants; + } else if old_container_type.is_size_container_type() && + !old_primary_style.is_display_contents() && + new_primary_style.is_display_contents() + { + // Also re-evaluate when a container gets 'display: contents', since size queries will now evaluate to unknown. + // Other displays like 'inline' will keep generating a box, so they are handled in `UpdateContainerQueryStyles`. + restyle_requirement = ChildRestyleRequirement::MustMatchDescendants; + } + + restyle_requirement = cmp::max( + restyle_requirement, + self.accumulate_damage_for( + context.shared, + &mut data.damage, + &old_primary_style, + new_primary_style, + None, + ), + ); + + if data.styles.pseudos.is_empty() && old_styles.pseudos.is_empty() { + // This is the common case; no need to examine pseudos here. + return restyle_requirement; + } + + let pseudo_styles = old_styles + .pseudos + .as_array() + .iter() + .zip(data.styles.pseudos.as_array().iter()); + + for (i, (old, new)) in pseudo_styles.enumerate() { + match (old, new) { + (&Some(ref old), &Some(ref new)) => { + self.accumulate_damage_for( + context.shared, + &mut data.damage, + old, + new, + Some(&PseudoElement::from_eager_index(i)), + ); + }, + (&None, &None) => {}, + _ => { + // It's possible that we're switching from not having + // ::before/::after at all to having styles for them but not + // actually having a useful pseudo-element. Check for that + // case. + let pseudo = PseudoElement::from_eager_index(i); + let new_pseudo_should_exist = + new.as_ref().map_or(false, |s| pseudo.should_exist(s)); + let old_pseudo_should_exist = + old.as_ref().map_or(false, |s| pseudo.should_exist(s)); + if new_pseudo_should_exist != old_pseudo_should_exist { + data.damage |= RestyleDamage::reconstruct(); + return restyle_requirement; + } + }, + } + } + + restyle_requirement + } + + /// Updates the rule nodes without re-running selector matching, using just + /// the rule tree. + /// + /// Returns true if an !important rule was replaced. + fn replace_rules( + &self, + replacements: RestyleHint, + context: &mut StyleContext, + cascade_inputs: &mut ElementCascadeInputs, + ) -> bool { + let mut result = false; + result |= self.replace_rules_internal( + replacements, + context, + CascadeVisitedMode::Unvisited, + cascade_inputs, + ); + result |= self.replace_rules_internal( + replacements, + context, + CascadeVisitedMode::Visited, + cascade_inputs, + ); + result + } + + /// Given the old and new style of this element, and whether it's a + /// pseudo-element, compute the restyle damage used to determine which + /// kind of layout or painting operations we'll need. + fn compute_style_difference( + &self, + old_values: &ComputedValues, + new_values: &ComputedValues, + pseudo: Option<&PseudoElement>, + ) -> StyleDifference { + debug_assert!(pseudo.map_or(true, |p| p.is_eager())); + RestyleDamage::compute_style_difference(old_values, new_values) + } +} + +impl MatchMethods for E {} diff --git a/servo/components/style/media_queries/media_list.rs b/servo/components/style/media_queries/media_list.rs new file mode 100644 index 0000000000..3c2ba9ee5c --- /dev/null +++ b/servo/components/style/media_queries/media_list.rs @@ -0,0 +1,150 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! A media query list: +//! +//! https://drafts.csswg.org/mediaqueries/#typedef-media-query-list + +use super::{Device, MediaQuery, Qualifier}; +use crate::context::QuirksMode; +use crate::error_reporting::ContextualParseError; +use crate::parser::ParserContext; +use crate::queries::condition::KleeneValue; +use crate::values::computed; +use cssparser::{Delimiter, Parser}; +use cssparser::{ParserInput, Token}; + +/// A type that encapsulates a media query list. +#[derive(Clone, MallocSizeOf, ToCss, ToShmem)] +#[css(comma, derive_debug)] +pub struct MediaList { + /// The list of media queries. + #[css(iterable)] + pub media_queries: Vec, +} + +impl MediaList { + /// Parse a media query list from CSS. + /// + /// Always returns a media query list. If any invalid media query is + /// found, the media query list is only filled with the equivalent of + /// "not all", see: + /// + /// + pub fn parse(context: &ParserContext, input: &mut Parser) -> Self { + if input.is_exhausted() { + return Self::empty(); + } + + let mut media_queries = vec![]; + loop { + let start_position = input.position(); + match input.parse_until_before(Delimiter::Comma, |i| MediaQuery::parse(context, i)) { + Ok(mq) => { + media_queries.push(mq); + }, + Err(err) => { + media_queries.push(MediaQuery::never_matching()); + let location = err.location; + let error = ContextualParseError::InvalidMediaRule( + input.slice_from(start_position), + err, + ); + context.log_css_error(location, error); + }, + } + + match input.next() { + Ok(&Token::Comma) => {}, + Ok(_) => unreachable!(), + Err(_) => break, + } + } + + MediaList { media_queries } + } + + /// Create an empty MediaList. + pub fn empty() -> Self { + MediaList { + media_queries: vec![], + } + } + + /// Evaluate a whole `MediaList` against `Device`. + pub fn evaluate(&self, device: &Device, quirks_mode: QuirksMode) -> bool { + // Check if it is an empty media query list or any queries match. + // https://drafts.csswg.org/mediaqueries-4/#mq-list + if self.media_queries.is_empty() { + return true; + } + + computed::Context::for_media_query_evaluation(device, quirks_mode, |context| { + self.media_queries.iter().any(|mq| { + let mut query_match = if mq.media_type.matches(device.media_type()) { + mq.condition + .as_ref() + .map_or(KleeneValue::True, |c| c.matches(context)) + } else { + KleeneValue::False + }; + + // Apply the logical NOT qualifier to the result + if matches!(mq.qualifier, Some(Qualifier::Not)) { + query_match = !query_match; + } + query_match.to_bool(/* unknown = */ false) + }) + }) + } + + /// Whether this `MediaList` contains no media queries. + pub fn is_empty(&self) -> bool { + self.media_queries.is_empty() + } + + /// Whether this `MediaList` depends on the viewport size. + pub fn is_viewport_dependent(&self) -> bool { + self.media_queries.iter().any(|q| q.is_viewport_dependent()) + } + + /// Append a new media query item to the media list. + /// + /// + /// Returns true if added, false if fail to parse the medium string. + pub fn append_medium(&mut self, context: &ParserContext, new_medium: &str) -> bool { + let mut input = ParserInput::new(new_medium); + let mut parser = Parser::new(&mut input); + let new_query = match MediaQuery::parse(&context, &mut parser) { + Ok(query) => query, + Err(_) => { + return false; + }, + }; + // This algorithm doesn't actually matches the current spec, + // but it matches the behavior of Gecko and Edge. + // See https://github.com/w3c/csswg-drafts/issues/697 + self.media_queries.retain(|query| query != &new_query); + self.media_queries.push(new_query); + true + } + + /// Delete a media query from the media list. + /// + /// + /// Returns true if found and deleted, false otherwise. + pub fn delete_medium(&mut self, context: &ParserContext, old_medium: &str) -> bool { + let mut input = ParserInput::new(old_medium); + let mut parser = Parser::new(&mut input); + let old_query = match MediaQuery::parse(context, &mut parser) { + Ok(query) => query, + Err(_) => { + return false; + }, + }; + let old_len = self.media_queries.len(); + self.media_queries.retain(|query| query != &old_query); + old_len != self.media_queries.len() + } +} diff --git a/servo/components/style/media_queries/media_query.rs b/servo/components/style/media_queries/media_query.rs new file mode 100644 index 0000000000..c30a445393 --- /dev/null +++ b/servo/components/style/media_queries/media_query.rs @@ -0,0 +1,193 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! A media query: +//! +//! https://drafts.csswg.org/mediaqueries/#typedef-media-query + +use crate::parser::ParserContext; +use crate::queries::{FeatureFlags, FeatureType, QueryCondition}; +use crate::str::string_as_ascii_lowercase; +use crate::values::CustomIdent; +use crate::Atom; +use cssparser::Parser; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError, ToCss}; + +/// +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, ToCss, ToShmem)] +pub enum Qualifier { + /// Hide a media query from legacy UAs: + /// + Only, + /// Negate a media query: + /// + Not, +} + +/// +#[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)] +pub struct MediaType(pub CustomIdent); + +impl MediaType { + /// The `screen` media type. + pub fn screen() -> Self { + MediaType(CustomIdent(atom!("screen"))) + } + + /// The `print` media type. + pub fn print() -> Self { + MediaType(CustomIdent(atom!("print"))) + } + + fn parse(name: &str) -> Result { + // From https://drafts.csswg.org/mediaqueries/#mq-syntax: + // + // The production does not include the keywords only, not, and, or, and layer. + // + // Here we also perform the to-ascii-lowercase part of the serialization + // algorithm: https://drafts.csswg.org/cssom/#serializing-media-queries + match_ignore_ascii_case! { name, + "not" | "or" | "and" | "only" | "layer" => Err(()), + _ => Ok(MediaType(CustomIdent(Atom::from(string_as_ascii_lowercase(name))))), + } + } +} + +/// A [media query][mq]. +/// +/// [mq]: https://drafts.csswg.org/mediaqueries/ +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] +pub struct MediaQuery { + /// The qualifier for this query. + pub qualifier: Option, + /// The media type for this query, that can be known, unknown, or "all". + pub media_type: MediaQueryType, + /// The condition that this media query contains. This cannot have `or` + /// in the first level. + pub condition: Option, +} + +impl ToCss for MediaQuery { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: Write, + { + if let Some(qual) = self.qualifier { + qual.to_css(dest)?; + dest.write_char(' ')?; + } + + match self.media_type { + MediaQueryType::All => { + // We need to print "all" if there's a qualifier, or there's + // just an empty list of expressions. + // + // Otherwise, we'd serialize media queries like "(min-width: + // 40px)" in "all (min-width: 40px)", which is unexpected. + if self.qualifier.is_some() || self.condition.is_none() { + dest.write_str("all")?; + } + }, + MediaQueryType::Concrete(MediaType(ref desc)) => desc.to_css(dest)?, + } + + let condition = match self.condition { + Some(ref c) => c, + None => return Ok(()), + }; + + if self.media_type != MediaQueryType::All || self.qualifier.is_some() { + dest.write_str(" and ")?; + } + + condition.to_css(dest) + } +} + +impl MediaQuery { + /// Return a media query that never matches, used for when we fail to parse + /// a given media query. + pub fn never_matching() -> Self { + Self { + qualifier: Some(Qualifier::Not), + media_type: MediaQueryType::All, + condition: None, + } + } + + /// Returns whether this media query depends on the viewport. + pub fn is_viewport_dependent(&self) -> bool { + self.condition.as_ref().map_or(false, |c| { + return c + .cumulative_flags() + .contains(FeatureFlags::VIEWPORT_DEPENDENT); + }) + } + + /// Parse a media query given css input. + /// + /// Returns an error if any of the expressions is unknown. + pub fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + let (qualifier, explicit_media_type) = input + .try_parse(|input| -> Result<_, ()> { + let qualifier = input.try_parse(Qualifier::parse).ok(); + let ident = input.expect_ident().map_err(|_| ())?; + let media_type = MediaQueryType::parse(&ident)?; + Ok((qualifier, Some(media_type))) + }) + .unwrap_or_default(); + + let condition = if explicit_media_type.is_none() { + Some(QueryCondition::parse(context, input, FeatureType::Media)?) + } else if input.try_parse(|i| i.expect_ident_matching("and")).is_ok() { + Some(QueryCondition::parse_disallow_or( + context, + input, + FeatureType::Media, + )?) + } else { + None + }; + + let media_type = explicit_media_type.unwrap_or(MediaQueryType::All); + Ok(Self { + qualifier, + media_type, + condition, + }) + } +} + +/// +#[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)] +pub enum MediaQueryType { + /// A media type that matches every device. + All, + /// A specific media type. + Concrete(MediaType), +} + +impl MediaQueryType { + fn parse(ident: &str) -> Result { + match_ignore_ascii_case! { ident, + "all" => return Ok(MediaQueryType::All), + _ => (), + }; + + // If parseable, accept this type as a concrete type. + MediaType::parse(ident).map(MediaQueryType::Concrete) + } + + /// Returns whether this media query type matches a MediaType. + pub fn matches(&self, other: MediaType) -> bool { + match *self { + MediaQueryType::All => true, + MediaQueryType::Concrete(ref known_type) => *known_type == other, + } + } +} diff --git a/servo/components/style/media_queries/mod.rs b/servo/components/style/media_queries/mod.rs new file mode 100644 index 0000000000..833f6f53cb --- /dev/null +++ b/servo/components/style/media_queries/mod.rs @@ -0,0 +1,18 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! [Media queries][mq]. +//! +//! [mq]: https://drafts.csswg.org/mediaqueries/ + +mod media_list; +mod media_query; + +pub use self::media_list::MediaList; +pub use self::media_query::{MediaQuery, MediaQueryType, MediaType, Qualifier}; + +#[cfg(feature = "gecko")] +pub use crate::gecko::media_queries::Device; +#[cfg(feature = "servo")] +pub use crate::servo::media_queries::Device; diff --git a/servo/components/style/parallel.rs b/servo/components/style/parallel.rs new file mode 100644 index 0000000000..ade40aa7b1 --- /dev/null +++ b/servo/components/style/parallel.rs @@ -0,0 +1,287 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Implements parallel traversal over the DOM tree. +//! +//! This traversal is based on Rayon, and therefore its safety is largely +//! verified by the type system. +//! +//! The primary trickiness and fine print for the above relates to the +//! thread safety of the DOM nodes themselves. Accessing a DOM element +//! concurrently on multiple threads is actually mostly "safe", since all +//! the mutable state is protected by an AtomicRefCell, and so we'll +//! generally panic if something goes wrong. Still, we try to to enforce our +//! thread invariants at compile time whenever possible. As such, TNode and +//! TElement are not Send, so ordinary style system code cannot accidentally +//! share them with other threads. In the parallel traversal, we explicitly +//! invoke |unsafe { SendNode::new(n) }| to put nodes in containers that may +//! be sent to other threads. This occurs in only a handful of places and is +//! easy to grep for. At the time of this writing, there is no other unsafe +//! code in the parallel traversal. + +#![deny(missing_docs)] + +use crate::context::{StyleContext, ThreadLocalStyleContext}; +use crate::dom::{OpaqueNode, SendNode, TElement}; +use crate::scoped_tls::ScopedTLS; +use crate::traversal::{DomTraversal, PerLevelTraversalData}; +use rayon; +use smallvec::SmallVec; + +/// The minimum stack size for a thread in the styling pool, in kilobytes. +pub const STYLE_THREAD_STACK_SIZE_KB: usize = 256; + +/// The stack margin. If we get this deep in the stack, we will skip recursive +/// optimizations to ensure that there is sufficient room for non-recursive work. +/// +/// We allocate large safety margins because certain OS calls can use very large +/// amounts of stack space [1]. Reserving a larger-than-necessary stack costs us +/// address space, but if we keep our safety margin big, we will generally avoid +/// committing those extra pages, and only use them in edge cases that would +/// otherwise cause crashes. +/// +/// When measured with 128KB stacks and 40KB margin, we could support 53 +/// levels of recursion before the limiter kicks in, on x86_64-Linux [2]. When +/// we doubled the stack size, we added it all to the safety margin, so we should +/// be able to get the same amount of recursion. +/// +/// [1] https://bugzilla.mozilla.org/show_bug.cgi?id=1395708#c15 +/// [2] See Gecko bug 1376883 for more discussion on the measurements. +/// +pub const STACK_SAFETY_MARGIN_KB: usize = 168; + +/// See documentation of the pref for performance characteristics. +pub fn work_unit_max() -> usize { + static_prefs::pref!("layout.css.stylo-work-unit-size") as usize +} + +/// A callback to create our thread local context. This needs to be +/// out of line so we don't allocate stack space for the entire struct +/// in the caller. +#[inline(never)] +fn create_thread_local_context<'scope, E>(slot: &mut Option>) +where + E: TElement + 'scope, +{ + *slot = Some(ThreadLocalStyleContext::new()); +} + +/// A parallel top-down DOM traversal. +/// +/// This algorithm traverses the DOM in a breadth-first, top-down manner. The +/// goals are: +/// * Never process a child before its parent (since child style depends on +/// parent style). If this were to happen, the styling algorithm would panic. +/// * Prioritize discovering nodes as quickly as possible to maximize +/// opportunities for parallelism. But this needs to be weighed against +/// styling cousins on a single thread to improve sharing. +/// * Style all the children of a given node (i.e. all sibling nodes) on +/// a single thread (with an upper bound to handle nodes with an +/// abnormally large number of children). This is important because we use +/// a thread-local cache to share styles between siblings. +#[inline(always)] +#[allow(unsafe_code)] +fn top_down_dom<'a, 'scope, E, D>( + nodes: &'a [SendNode], + root: OpaqueNode, + mut traversal_data: PerLevelTraversalData, + scope: &'a rayon::ScopeFifo<'scope>, + pool: &'scope rayon::ThreadPool, + traversal: &'scope D, + tls: &'scope ScopedTLS<'scope, ThreadLocalStyleContext>, +) where + E: TElement + 'scope, + D: DomTraversal, +{ + let work_unit_max = work_unit_max(); + debug_assert!(nodes.len() <= work_unit_max); + + // We set this below, when we have a borrow of the thread-local-context + // available. + let recursion_ok; + + // Collect all the children of the elements in our work unit. This will + // contain the combined children of up to work_unit_max nodes, which may + // be numerous. As such, we store it in a large SmallVec to minimize heap- + // spilling, and never move it. + let mut discovered_child_nodes = SmallVec::<[SendNode; 128]>::new(); + { + // Scope the borrow of the TLS so that the borrow is dropped before + // a potential recursive call when we pass TailCall. + let mut tlc = tls.ensure(|slot: &mut Option>| { + create_thread_local_context(slot) + }); + + // Check that we're not in danger of running out of stack. + recursion_ok = !tlc.stack_limit_checker.limit_exceeded(); + + let mut context = StyleContext { + shared: traversal.shared_context(), + thread_local: &mut *tlc, + }; + + for n in nodes { + // If the last node we processed produced children, we may want to + // spawn them off into a work item. We do this at the beginning of + // the loop (rather than at the end) so that we can traverse our + // last bits of work directly on this thread without a spawn call. + // + // This has the important effect of removing the allocation and + // context-switching overhead of the parallel traversal for perfectly + // linear regions of the DOM, i.e.: + // + // + // + // which are not at all uncommon. + // + // There's a tension here between spawning off a work item as soon + // as discovered_child_nodes is nonempty and waiting until we have a + // full work item to do so. The former optimizes for speed of + // discovery (we'll start discovering the kids of the things in + // "nodes" ASAP). The latter gives us better sharing (e.g. we can + // share between cousins much better, because we don't hand them off + // as separate work items, which are likely to end up on separate + // threads) and gives us a chance to just handle everything on this + // thread for small DOM subtrees, as in the linear example above. + // + // There are performance and "number of ComputedValues" + // measurements for various testcases in + // https://bugzilla.mozilla.org/show_bug.cgi?id=1385982#c10 and + // following. + // + // The worst case behavior for waiting until we have a full work + // item is a deep tree which has work_unit_max "linear" branches, + // hence work_unit_max elements at each level. Such a tree would + // end up getting processed entirely sequentially, because we would + // process each level one at a time as a single work unit, whether + // via our end-of-loop tail call or not. If we kicked off a + // traversal as soon as we discovered kids, we would instead + // process such a tree more or less with a thread-per-branch, + // multiplexed across our actual threadpool. + if discovered_child_nodes.len() >= work_unit_max { + let mut traversal_data_copy = traversal_data.clone(); + traversal_data_copy.current_dom_depth += 1; + traverse_nodes( + &discovered_child_nodes, + DispatchMode::NotTailCall, + recursion_ok, + root, + traversal_data_copy, + scope, + pool, + traversal, + tls, + ); + discovered_child_nodes.clear(); + } + + let node = **n; + let mut children_to_process = 0isize; + traversal.process_preorder(&traversal_data, &mut context, node, |n| { + children_to_process += 1; + let send_n = unsafe { SendNode::new(n) }; + discovered_child_nodes.push(send_n); + }); + + traversal.handle_postorder_traversal(&mut context, root, node, children_to_process); + } + } + + // Handle whatever elements we have queued up but not kicked off traversals + // for yet. If any exist, we can process them (or at least one work unit's + // worth of them) directly on this thread by passing TailCall. + if !discovered_child_nodes.is_empty() { + traversal_data.current_dom_depth += 1; + traverse_nodes( + &discovered_child_nodes, + DispatchMode::TailCall, + recursion_ok, + root, + traversal_data, + scope, + pool, + traversal, + tls, + ); + } +} + +/// Controls whether traverse_nodes may make a recursive call to continue +/// doing work, or whether it should always dispatch work asynchronously. +#[derive(Clone, Copy, PartialEq)] +pub enum DispatchMode { + /// This is the last operation by the caller. + TailCall, + /// This is not the last operation by the caller. + NotTailCall, +} + +impl DispatchMode { + fn is_tail_call(&self) -> bool { + matches!(*self, DispatchMode::TailCall) + } +} + +/// Enqueues |nodes| for processing, possibly on this thread if the tail call +/// conditions are met. +#[inline] +pub fn traverse_nodes<'a, 'scope, E, D>( + nodes: &[SendNode], + mode: DispatchMode, + recursion_ok: bool, + root: OpaqueNode, + traversal_data: PerLevelTraversalData, + scope: &'a rayon::ScopeFifo<'scope>, + pool: &'scope rayon::ThreadPool, + traversal: &'scope D, + tls: &'scope ScopedTLS<'scope, ThreadLocalStyleContext>, +) where + E: TElement + 'scope, + D: DomTraversal, +{ + debug_assert_ne!(nodes.len(), 0); + + // This is a tail call from the perspective of the caller. However, we only + // want to actually dispatch the job as a tail call if there's nothing left + // in our local queue. Otherwise we need to return to it to maintain proper + // breadth-first ordering. We also need to take care to avoid stack + // overflow due to excessive tail recursion. The stack overflow avoidance + // isn't observable to content -- we're still completely correct, just not + // using tail recursion any more. See Gecko bugs 1368302 and 1376883. + let may_dispatch_tail = + mode.is_tail_call() && recursion_ok && !pool.current_thread_has_pending_tasks().unwrap(); + + let work_unit_max = work_unit_max(); + // In the common case, our children fit within a single work unit, in which case we can pass + // the nodes directly and avoid extra allocation. + if nodes.len() <= work_unit_max { + if may_dispatch_tail { + top_down_dom(&nodes, root, traversal_data, scope, pool, traversal, tls); + } else { + let work = nodes.to_vec(); + scope.spawn_fifo(move |scope| { + gecko_profiler_label!(Layout, StyleComputation); + top_down_dom(&work, root, traversal_data, scope, pool, traversal, tls); + }); + } + } else { + for chunk in nodes.chunks(work_unit_max) { + let work = chunk.to_vec(); + let traversal_data_copy = traversal_data.clone(); + scope.spawn_fifo(move |scope| { + gecko_profiler_label!(Layout, StyleComputation); + let work = work; + top_down_dom( + &work, + root, + traversal_data_copy, + scope, + pool, + traversal, + tls, + ) + }); + } + } +} diff --git a/servo/components/style/parser.rs b/servo/components/style/parser.rs new file mode 100644 index 0000000000..8d8d408f53 --- /dev/null +++ b/servo/components/style/parser.rs @@ -0,0 +1,210 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! The context within which CSS code is parsed. + +use crate::context::QuirksMode; +use crate::error_reporting::{ContextualParseError, ParseErrorReporter}; +use crate::stylesheets::{CssRuleType, CssRuleTypes, Namespaces, Origin, UrlExtraData}; +use crate::use_counters::UseCounters; +use cssparser::{Parser, SourceLocation, UnicodeRange}; +use std::borrow::Cow; +use style_traits::{OneOrMoreSeparated, ParseError, ParsingMode, Separator}; + +/// Asserts that all ParsingMode flags have a matching ParsingMode value in gecko. +#[cfg(feature = "gecko")] +#[inline] +pub fn assert_parsing_mode_match() { + use crate::gecko_bindings::structs; + + macro_rules! check_parsing_modes { + ( $( $a:ident => $b:path ),*, ) => { + if cfg!(debug_assertions) { + let mut modes = ParsingMode::all(); + $( + assert_eq!(structs::$a as usize, $b.bits() as usize, stringify!($b)); + modes.remove($b); + )* + assert_eq!(modes, ParsingMode::empty(), "all ParsingMode bits should have an assertion"); + } + } + } + + check_parsing_modes! { + ParsingMode_Default => ParsingMode::DEFAULT, + ParsingMode_AllowUnitlessLength => ParsingMode::ALLOW_UNITLESS_LENGTH, + ParsingMode_AllowAllNumericValues => ParsingMode::ALLOW_ALL_NUMERIC_VALUES, + } +} + +/// The data that the parser needs from outside in order to parse a stylesheet. +pub struct ParserContext<'a> { + /// The `Origin` of the stylesheet, whether it's a user, author or + /// user-agent stylesheet. + pub stylesheet_origin: Origin, + /// The extra data we need for resolving url values. + pub url_data: &'a UrlExtraData, + /// The current rule types, if any. + pub rule_types: CssRuleTypes, + /// The mode to use when parsing. + pub parsing_mode: ParsingMode, + /// The quirks mode of this stylesheet. + pub quirks_mode: QuirksMode, + /// The active error reporter, or none if error reporting is disabled. + error_reporter: Option<&'a dyn ParseErrorReporter>, + /// The currently active namespaces. + pub namespaces: Cow<'a, Namespaces>, + /// The use counters we want to record while parsing style rules, if any. + pub use_counters: Option<&'a UseCounters>, +} + +impl<'a> ParserContext<'a> { + /// Create a parser context. + #[inline] + pub fn new( + stylesheet_origin: Origin, + url_data: &'a UrlExtraData, + rule_type: Option, + parsing_mode: ParsingMode, + quirks_mode: QuirksMode, + namespaces: Cow<'a, Namespaces>, + error_reporter: Option<&'a dyn ParseErrorReporter>, + use_counters: Option<&'a UseCounters>, + ) -> Self { + Self { + stylesheet_origin, + url_data, + rule_types: rule_type.map(CssRuleTypes::from).unwrap_or_default(), + parsing_mode, + quirks_mode, + error_reporter, + namespaces, + use_counters, + } + } + + /// Temporarily sets the rule_type and executes the callback function, returning its result. + pub fn nest_for_rule( + &mut self, + rule_type: CssRuleType, + cb: impl FnOnce(&mut Self) -> R, + ) -> R { + let old_rule_types = self.rule_types; + self.rule_types.insert(rule_type); + let r = cb(self); + self.rule_types = old_rule_types; + r + } + + /// Whether we're in a @page rule. + #[inline] + pub fn in_page_rule(&self) -> bool { + self.rule_types.contains(CssRuleType::Page) + } + + /// Get the rule type, which assumes that one is available. + pub fn rule_types(&self) -> CssRuleTypes { + self.rule_types + } + + /// Returns whether CSS error reporting is enabled. + #[inline] + pub fn error_reporting_enabled(&self) -> bool { + self.error_reporter.is_some() + } + + /// Record a CSS parse error with this context’s error reporting. + pub fn log_css_error(&self, location: SourceLocation, error: ContextualParseError) { + let error_reporter = match self.error_reporter { + Some(r) => r, + None => return, + }; + + error_reporter.report_error(self.url_data, location, error) + } + + /// Whether we're in a user-agent stylesheet. + #[inline] + pub fn in_ua_sheet(&self) -> bool { + self.stylesheet_origin == Origin::UserAgent + } + + /// Returns whether chrome-only rules should be parsed. + #[inline] + pub fn chrome_rules_enabled(&self) -> bool { + self.url_data.chrome_rules_enabled() || self.stylesheet_origin == Origin::User + } + + /// Whether we're in a user-agent stylesheet or chrome rules are enabled. + #[inline] + pub fn in_ua_or_chrome_sheet(&self) -> bool { + self.in_ua_sheet() || self.chrome_rules_enabled() + } +} + +/// A trait to abstract parsing of a specified value given a `ParserContext` and +/// CSS input. +/// +/// This can be derived on keywords with `#[derive(Parse)]`. +/// +/// The derive code understands the following attributes on each of the variants: +/// +/// * `#[parse(aliases = "foo,bar")]` can be used to alias a value with another +/// at parse-time. +/// +/// * `#[parse(condition = "function")]` can be used to make the parsing of the +/// value conditional on `function`, which needs to fulfill +/// `fn(&ParserContext) -> bool`. +pub trait Parse: Sized { + /// Parse a value of this type. + /// + /// Returns an error on failure. + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result>; +} + +impl Parse for Vec +where + T: Parse + OneOrMoreSeparated, + ::S: Separator, +{ + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + ::S::parse(input, |i| T::parse(context, i)) + } +} + +impl Parse for Box +where + T: Parse, +{ + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + T::parse(context, input).map(Box::new) + } +} + +impl Parse for crate::OwnedStr { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + Ok(input.expect_string()?.as_ref().to_owned().into()) + } +} + +impl Parse for UnicodeRange { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + Ok(UnicodeRange::parse(input)?) + } +} diff --git a/servo/components/style/piecewise_linear.rs b/servo/components/style/piecewise_linear.rs new file mode 100644 index 0000000000..84ccb7061c --- /dev/null +++ b/servo/components/style/piecewise_linear.rs @@ -0,0 +1,293 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! A piecewise linear function, following CSS linear easing +use crate::values::computed::Percentage; +use core::slice::Iter; +/// draft as in https://github.com/w3c/csswg-drafts/pull/6533. +use euclid::approxeq::ApproxEq; +use itertools::Itertools; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ToCss}; + +use crate::values::CSSFloat; + +type ValueType = CSSFloat; +/// a single entry in a piecewise linear function. +#[allow(missing_docs)] +#[derive( + Clone, + Copy, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToResolvedValue, + Serialize, + Deserialize, +)] +#[repr(C)] +pub struct PiecewiseLinearFunctionEntry { + pub x: ValueType, + pub y: ValueType, +} + +impl ToCss for PiecewiseLinearFunctionEntry { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: fmt::Write, + { + self.y.to_css(dest)?; + dest.write_char(' ')?; + Percentage(self.x).to_css(dest) + } +} + +/// Representation of a piecewise linear function, a series of linear functions. +#[derive( + Default, + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToResolvedValue, + ToCss, + Serialize, + Deserialize, +)] +#[repr(C)] +#[css(comma)] +pub struct PiecewiseLinearFunction { + #[css(iterable)] + entries: crate::OwnedSlice, +} + +/// Parameters to define one linear stop. +pub type PiecewiseLinearFunctionBuildParameters = (CSSFloat, Option); + +impl PiecewiseLinearFunction { + /// Interpolate y value given x and two points. The linear function will be rooted at the asymptote. + fn interpolate( + x: ValueType, + prev: PiecewiseLinearFunctionEntry, + next: PiecewiseLinearFunctionEntry, + asymptote: &PiecewiseLinearFunctionEntry, + ) -> ValueType { + // Short circuit if the x is on prev or next. + // `next` point is preferred as per spec. + if x.approx_eq(&next.x) { + return next.y; + } + if x.approx_eq(&prev.x) { + return prev.y; + } + // Avoid division by zero. + if prev.x.approx_eq(&next.x) { + return next.y; + } + let slope = (next.y - prev.y) / (next.x - prev.x); + return slope * (x - asymptote.x) + asymptote.y; + } + + /// Get the y value of the piecewise linear function given the x value, as per + /// https://drafts.csswg.org/css-easing-2/#linear-easing-function-output + pub fn at(&self, x: ValueType) -> ValueType { + if !x.is_finite() { + return if x > 0.0 { 1.0 } else { 0.0 }; + } + if self.entries.is_empty() { + // Implied y = x, as per spec. + return x; + } + if self.entries.len() == 1 { + // Implied y = , as per spec. + return self.entries[0].y; + } + // Spec dictates the valid input domain is [0, 1]. Outside of this range, the output + // should be calculated as if the slopes at start and end extend to infinity. However, if the + // start/end have two points of the same position, the line should extend along the x-axis. + // The function doesn't have to cover the input domain, in which case the extension logic + // applies even if the input falls in the input domain. + // Also, we're guaranteed to have at least two elements at this point. + if x < self.entries[0].x { + return Self::interpolate(x, self.entries[0], self.entries[1], &self.entries[0]); + } + let mut rev_iter = self.entries.iter().rev(); + let last = rev_iter.next().unwrap(); + if x >= last.x { + let second_last = rev_iter.next().unwrap(); + return Self::interpolate(x, *second_last, *last, last); + } + + // Now we know the input sits within the domain explicitly defined by our function. + for (point_b, point_a) in self.entries.iter().rev().tuple_windows() { + // Need to let point A be the _last_ point where its x is less than the input x, + // hence the reverse traversal. + if x < point_a.x { + continue; + } + return Self::interpolate(x, *point_a, *point_b, point_a); + } + unreachable!("Input is supposed to be within the entries' min & max!"); + } + + /// Create the piecewise linear function from an iterator that generates the parameter tuple. + pub fn from_iter(iter: Iter) -> Self + where + Iter: Iterator + ExactSizeIterator, + { + let mut builder = PiecewiseLinearFunctionBuilder::with_capacity(iter.len()); + for (y, x_start) in iter { + builder = builder.push(y, x_start); + } + builder.build() + } + + #[allow(missing_docs)] + pub fn iter(&self) -> Iter { + self.entries.iter() + } +} + +/// Entry of a piecewise linear function while building, where the calculation of x value can be deferred. +#[derive(Clone, Copy)] +struct BuildEntry { + x: Option, + y: ValueType, +} + +/// Builder object to generate a linear function. +#[derive(Default)] +pub struct PiecewiseLinearFunctionBuilder { + largest_x: Option, + smallest_x: Option, + entries: Vec, +} + +impl PiecewiseLinearFunctionBuilder { + #[allow(missing_docs)] + pub fn new() -> Self { + PiecewiseLinearFunctionBuilder::default() + } + + /// Create a builder for a known amount of linear stop entries. + pub fn with_capacity(len: usize) -> Self { + PiecewiseLinearFunctionBuilder { + largest_x: None, + smallest_x: None, + entries: Vec::with_capacity(len), + } + } + + fn create_entry(&mut self, y: ValueType, x: Option) { + let x = match x { + Some(x) if x.is_finite() => x, + _ if self.entries.is_empty() => 0.0, // First x is 0 if not specified (Or not finite) + _ => { + self.entries.push(BuildEntry { x: None, y }); + return; + }, + }; + // Specified x value cannot regress, as per spec. + let x = match self.largest_x { + Some(largest_x) => x.max(largest_x), + None => x, + }; + self.largest_x = Some(x); + // Whatever we see the earliest is the smallest value. + if self.smallest_x.is_none() { + self.smallest_x = Some(x); + } + self.entries.push(BuildEntry { x: Some(x), y }); + } + + /// Add a new entry into the piecewise linear function with specified y value. + /// If the start x value is given, that is where the x value will be. Otherwise, + /// the x value is calculated later. If the end x value is specified, a flat segment + /// is generated. If start x value is not specified but end x is, it is treated as + /// start x. + pub fn push(mut self, y: CSSFloat, x_start: Option) -> Self { + self.create_entry(y, x_start); + self + } + + /// Finish building the piecewise linear function by resolving all undefined x values, + /// then return the result. + pub fn build(mut self) -> PiecewiseLinearFunction { + if self.entries.is_empty() { + return PiecewiseLinearFunction::default(); + } + if self.entries.len() == 1 { + // Don't bother resolving anything. + return PiecewiseLinearFunction { + entries: crate::OwnedSlice::from_slice(&[PiecewiseLinearFunctionEntry { + x: 0., + y: self.entries[0].y, + }]), + }; + } + // Guaranteed at least two elements. + // Start element's x value should've been assigned when the first value was pushed. + debug_assert!( + self.entries[0].x.is_some(), + "Expected an entry with x defined!" + ); + // Spec asserts that if the last entry does not have an x value, it is assigned the largest seen x value. + self.entries + .last_mut() + .unwrap() + .x + .get_or_insert(self.largest_x.filter(|x| x > &1.0).unwrap_or(1.0)); + // Now we have at least two elements with x values, with start & end x values guaranteed. + + let mut result = Vec::with_capacity(self.entries.len()); + result.push(PiecewiseLinearFunctionEntry { + x: self.entries[0].x.unwrap(), + y: self.entries[0].y, + }); + for (i, e) in self.entries.iter().enumerate().skip(1) { + if e.x.is_none() { + // Need to calculate x values by first finding an entry with the first + // defined x value (Guaranteed to exist as the list end has it defined). + continue; + } + // x is defined for this element. + let divisor = i - result.len() + 1; + // Any element(s) with undefined x to assign? + if divisor != 1 { + // Have at least one element in result at all times. + let start_x = result.last().unwrap().x; + let increment = (e.x.unwrap() - start_x) / divisor as ValueType; + // Grab every element with undefined x to this point, which starts at the end of the result + // array, and ending right before the current index. Then, assigned the evenly divided + // x values. + result.extend( + self.entries[result.len()..i] + .iter() + .enumerate() + .map(|(j, e)| { + debug_assert!(e.x.is_none(), "Expected an entry with x undefined!"); + PiecewiseLinearFunctionEntry { + x: increment * (j + 1) as ValueType + start_x, + y: e.y, + } + }), + ); + } + result.push(PiecewiseLinearFunctionEntry { + x: e.x.unwrap(), + y: e.y, + }); + } + debug_assert_eq!( + result.len(), + self.entries.len(), + "Should've mapped one-to-one!" + ); + PiecewiseLinearFunction { + entries: result.into(), + } + } +} diff --git a/servo/components/style/properties/Mako-1.1.2-py2.py3-none-any.whl b/servo/components/style/properties/Mako-1.1.2-py2.py3-none-any.whl new file mode 100644 index 0000000000..9593025a47 Binary files /dev/null and b/servo/components/style/properties/Mako-1.1.2-py2.py3-none-any.whl differ diff --git a/servo/components/style/properties/build.py b/servo/components/style/properties/build.py new file mode 100644 index 0000000000..6c3ee0cf66 --- /dev/null +++ b/servo/components/style/properties/build.py @@ -0,0 +1,176 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +import json +import os.path +import re +import sys + +BASE = os.path.dirname(__file__.replace("\\", "/")) +sys.path.insert(0, os.path.join(BASE, "Mako-1.1.2-py2.py3-none-any.whl")) +sys.path.insert(0, BASE) # For importing `data.py` + +from mako import exceptions +from mako.lookup import TemplateLookup +from mako.template import Template + +import data + +RE_PYTHON_ADDR = re.compile(r"<.+? object at 0x[0-9a-fA-F]+>") + +OUT_DIR = os.environ.get("OUT_DIR", "") + +STYLE_STRUCT_LIST = [ + "background", + "border", + "box", + "column", + "counters", + "effects", + "font", + "inherited_box", + "inherited_svg", + "inherited_table", + "inherited_text", + "inherited_ui", + "list", + "margin", + "outline", + "page", + "padding", + "position", + "svg", + "table", + "text", + "ui", + "xul", +] + + +def main(): + usage = ( + "Usage: %s [ servo-2013 | servo-2020 | gecko ] [ style-crate | geckolib