From 2aa4a82499d4becd2284cdb482213d541b8804dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 28 Apr 2024 16:29:10 +0200 Subject: Adding upstream version 86.0.1. Signed-off-by: Daniel Baumann --- servo/components/style/Cargo.toml | 90 + servo/components/style/README.md | 6 + servo/components/style/animation.rs | 1407 +++++++ servo/components/style/applicable_declarations.rs | 120 + servo/components/style/attr.rs | 598 +++ servo/components/style/author_styles.rs | 78 + servo/components/style/bezier.rs | 126 + servo/components/style/bloom.rs | 385 ++ servo/components/style/build.rs | 91 + servo/components/style/build_gecko.rs | 411 ++ servo/components/style/context.rs | 803 ++++ servo/components/style/counter_style/mod.rs | 672 ++++ servo/components/style/counter_style/predefined.rs | 61 + .../style/counter_style/update_predefined.py | 35 + servo/components/style/custom_properties.rs | 1076 +++++ servo/components/style/data.rs | 451 +++ servo/components/style/dom.rs | 986 +++++ servo/components/style/dom_apis.rs | 671 ++++ servo/components/style/driver.rs | 193 + servo/components/style/element_state.rs | 152 + servo/components/style/encoding_support.rs | 105 + servo/components/style/error_reporting.rs | 258 ++ servo/components/style/font_face.rs | 594 +++ servo/components/style/font_metrics.rs | 92 + servo/components/style/gecko/arc_types.rs | 138 + servo/components/style/gecko/boxed_types.rs | 31 + servo/components/style/gecko/conversions.rs | 59 + servo/components/style/gecko/data.rs | 202 + servo/components/style/gecko/media_features.rs | 856 ++++ servo/components/style/gecko/media_queries.rs | 363 ++ servo/components/style/gecko/mod.rs | 26 + .../style/gecko/non_ts_pseudo_class_list.rs | 104 + servo/components/style/gecko/profiler.rs | 75 + servo/components/style/gecko/pseudo_element.rs | 219 ++ .../style/gecko/pseudo_element_definition.mako.rs | 264 ++ servo/components/style/gecko/regen_atoms.py | 215 + servo/components/style/gecko/restyle_damage.rs | 121 + servo/components/style/gecko/selector_parser.rs | 453 +++ servo/components/style/gecko/snapshot.rs | 238 ++ servo/components/style/gecko/snapshot_helpers.rs | 167 + servo/components/style/gecko/traversal.rs | 53 + servo/components/style/gecko/url.rs | 381 ++ servo/components/style/gecko/values.rs | 85 + servo/components/style/gecko/wrapper.rs | 2242 +++++++++++ servo/components/style/gecko_bindings/mod.rs | 25 + servo/components/style/gecko_bindings/sugar/mod.rs | 13 + .../style/gecko_bindings/sugar/ns_com_ptr.rs | 25 + .../style/gecko_bindings/sugar/ns_compatibility.rs | 19 + .../gecko_bindings/sugar/ns_style_auto_array.rs | 84 + .../style/gecko_bindings/sugar/ns_t_array.rs | 144 + .../style/gecko_bindings/sugar/origin_flags.rs | 31 + .../style/gecko_bindings/sugar/ownership.rs | 348 ++ .../style/gecko_bindings/sugar/refptr.rs | 331 ++ servo/components/style/gecko_string_cache/mod.rs | 525 +++ .../style/gecko_string_cache/namespace.rs | 105 + servo/components/style/global_style_data.rs | 173 + servo/components/style/hash.rs | 31 + .../style/invalidation/element/document_state.rs | 130 + .../style/invalidation/element/element_wrapper.rs | 408 ++ .../style/invalidation/element/invalidation_map.rs | 541 +++ .../style/invalidation/element/invalidator.rs | 1054 +++++ servo/components/style/invalidation/element/mod.rs | 12 + .../style/invalidation/element/restyle_hints.rs | 187 + .../invalidation/element/state_and_attributes.rs | 524 +++ .../components/style/invalidation/media_queries.rs | 130 + servo/components/style/invalidation/mod.rs | 9 + servo/components/style/invalidation/stylesheets.rs | 635 +++ servo/components/style/lib.rs | 261 ++ servo/components/style/logical_geometry.rs | 1467 +++++++ servo/components/style/macros.rs | 142 + servo/components/style/matching.rs | 1083 +++++ .../style/media_queries/media_condition.rs | 185 + .../style/media_queries/media_feature.rs | 180 + .../media_queries/media_feature_expression.rs | 522 +++ servo/components/style/media_queries/media_list.rs | 138 + .../components/style/media_queries/media_query.rs | 180 + servo/components/style/media_queries/mod.rs | 24 + servo/components/style/parallel.rs | 296 ++ servo/components/style/parser.rs | 217 + .../properties/Mako-1.1.2-py2.py3-none-any.whl | Bin 0 -> 75521 bytes servo/components/style/properties/build.py | 175 + servo/components/style/properties/cascade.rs | 1076 +++++ .../style/properties/computed_value_flags.rs | 126 + .../style/properties/counted_unknown_properties.py | 127 + servo/components/style/properties/data.py | 901 +++++ .../style/properties/declaration_block.rs | 1520 +++++++ servo/components/style/properties/gecko.mako.rs | 2121 ++++++++++ servo/components/style/properties/helpers.mako.rs | 1176 ++++++ .../properties/helpers/animated_properties.mako.rs | 846 ++++ .../style/properties/longhands/background.mako.rs | 115 + .../style/properties/longhands/border.mako.rs | 161 + .../style/properties/longhands/box.mako.rs | 709 ++++ .../style/properties/longhands/column.mako.rs | 92 + .../style/properties/longhands/counters.mako.rs | 48 + .../style/properties/longhands/effects.mako.rs | 90 + .../style/properties/longhands/font.mako.rs | 526 +++ .../properties/longhands/inherited_box.mako.rs | 93 + .../properties/longhands/inherited_svg.mako.rs | 216 + .../properties/longhands/inherited_table.mako.rs | 50 + .../properties/longhands/inherited_text.mako.rs | 409 ++ .../properties/longhands/inherited_ui.mako.rs | 94 + .../style/properties/longhands/list.mako.rs | 95 + .../style/properties/longhands/margin.mako.rs | 42 + .../style/properties/longhands/outline.mako.rs | 67 + .../style/properties/longhands/padding.mako.rs | 41 + .../style/properties/longhands/position.mako.rs | 464 +++ .../style/properties/longhands/svg.mako.rs | 250 ++ .../style/properties/longhands/table.mako.rs | 28 + .../style/properties/longhands/text.mako.rs | 79 + .../style/properties/longhands/ui.mako.rs | 107 + .../style/properties/longhands/xul.mako.rs | 79 + .../style/properties/properties.html.mako | 31 + .../components/style/properties/properties.mako.rs | 4129 ++++++++++++++++++++ .../style/properties/shorthands/background.mako.rs | 253 ++ .../style/properties/shorthands/border.mako.rs | 481 +++ .../style/properties/shorthands/box.mako.rs | 499 +++ .../style/properties/shorthands/column.mako.rs | 104 + .../style/properties/shorthands/font.mako.rs | 414 ++ .../properties/shorthands/inherited_svg.mako.rs | 38 + .../properties/shorthands/inherited_text.mako.rs | 91 + .../style/properties/shorthands/list.mako.rs | 107 + .../style/properties/shorthands/margin.mako.rs | 59 + .../style/properties/shorthands/outline.mako.rs | 105 + .../style/properties/shorthands/padding.mako.rs | 58 + .../style/properties/shorthands/position.mako.rs | 863 ++++ .../style/properties/shorthands/svg.mako.rs | 258 ++ .../style/properties/shorthands/text.mako.rs | 119 + servo/components/style/rule_cache.rs | 187 + servo/components/style/rule_collector.rs | 493 +++ servo/components/style/rule_tree/core.rs | 761 ++++ servo/components/style/rule_tree/level.rs | 277 ++ servo/components/style/rule_tree/map.rs | 201 + servo/components/style/rule_tree/mod.rs | 425 ++ 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 | 675 ++++ servo/components/style/selector_parser.rs | 231 ++ servo/components/style/servo/media_queries.rs | 222 ++ servo/components/style/servo/mod.rs | 12 + servo/components/style/servo/restyle_damage.rs | 268 ++ servo/components/style/servo/selector_parser.rs | 804 ++++ servo/components/style/servo/url.rs | 237 ++ servo/components/style/shared_lock.rs | 361 ++ servo/components/style/sharing/checks.rs | 175 + servo/components/style/sharing/mod.rs | 899 +++++ servo/components/style/str.rs | 181 + servo/components/style/style_adjuster.rs | 888 +++++ servo/components/style/style_resolver.rs | 576 +++ servo/components/style/stylesheet_set.rs | 701 ++++ .../style/stylesheets/cascading_at_rule.rs | 70 + .../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 | 481 +++ servo/components/style/stylesheets/import_rule.rs | 223 ++ .../components/style/stylesheets/keyframes_rule.rs | 612 +++ servo/components/style/stylesheets/loader.rs | 29 + servo/components/style/stylesheets/media_rule.rs | 71 + servo/components/style/stylesheets/mod.rs | 510 +++ .../components/style/stylesheets/namespace_rule.rs | 39 + servo/components/style/stylesheets/origin.rs | 247 ++ servo/components/style/stylesheets/page_rule.rs | 71 + servo/components/style/stylesheets/rule_list.rs | 193 + servo/components/style/stylesheets/rule_parser.rs | 613 +++ .../components/style/stylesheets/rules_iterator.rs | 319 ++ servo/components/style/stylesheets/style_rule.rs | 79 + servo/components/style/stylesheets/stylesheet.rs | 624 +++ .../components/style/stylesheets/supports_rule.rs | 422 ++ .../components/style/stylesheets/viewport_rule.rs | 799 ++++ servo/components/style/stylist.rs | 2400 ++++++++++++ servo/components/style/thread_state.rs | 97 + servo/components/style/traversal.rs | 852 ++++ servo/components/style/traversal_flags.rs | 67 + servo/components/style/use_counters/mod.rs | 109 + servo/components/style/values/animated/color.rs | 264 ++ servo/components/style/values/animated/effects.rs | 27 + servo/components/style/values/animated/font.rs | 158 + servo/components/style/values/animated/grid.rs | 166 + servo/components/style/values/animated/mod.rs | 488 +++ servo/components/style/values/animated/svg.rs | 45 + .../components/style/values/animated/transform.rs | 1456 +++++++ servo/components/style/values/computed/align.rs | 91 + servo/components/style/values/computed/angle.rs | 101 + .../components/style/values/computed/background.rs | 13 + .../style/values/computed/basic_shape.rs | 42 + servo/components/style/values/computed/border.rs | 78 + servo/components/style/values/computed/box.rs | 246 ++ servo/components/style/values/computed/color.rs | 106 + servo/components/style/values/computed/column.rs | 11 + servo/components/style/values/computed/counters.rs | 22 + servo/components/style/values/computed/easing.rs | 14 + servo/components/style/values/computed/effects.rs | 44 + servo/components/style/values/computed/flex.rs | 19 + servo/components/style/values/computed/font.rs | 987 +++++ servo/components/style/values/computed/image.rs | 196 + servo/components/style/values/computed/length.rs | 518 +++ .../style/values/computed/length_percentage.rs | 879 +++++ servo/components/style/values/computed/list.rs | 18 + servo/components/style/values/computed/mod.rs | 816 ++++ servo/components/style/values/computed/motion.rs | 57 + servo/components/style/values/computed/outline.rs | 7 + .../components/style/values/computed/percentage.rs | 105 + servo/components/style/values/computed/position.rs | 105 + servo/components/style/values/computed/rect.rs | 11 + .../components/style/values/computed/resolution.rs | 56 + servo/components/style/values/computed/svg.rs | 70 + servo/components/style/values/computed/text.rs | 218 ++ servo/components/style/values/computed/time.rs | 44 + .../components/style/values/computed/transform.rs | 555 +++ servo/components/style/values/computed/ui.rs | 22 + servo/components/style/values/computed/url.rs | 18 + servo/components/style/values/distance.rs | 131 + .../components/style/values/generics/background.rs | 54 + .../style/values/generics/basic_shape.rs | 535 +++ servo/components/style/values/generics/border.rs | 257 ++ servo/components/style/values/generics/box.rs | 132 + servo/components/style/values/generics/calc.rs | 580 +++ servo/components/style/values/generics/color.rs | 112 + servo/components/style/values/generics/column.rs | 45 + servo/components/style/values/generics/counters.rs | 221 ++ servo/components/style/values/generics/easing.rs | 135 + servo/components/style/values/generics/effects.rs | 121 + servo/components/style/values/generics/flex.rs | 33 + servo/components/style/values/generics/font.rs | 204 + servo/components/style/values/generics/grid.rs | 819 ++++ servo/components/style/values/generics/image.rs | 602 +++ servo/components/style/values/generics/length.rs | 289 ++ servo/components/style/values/generics/mod.rs | 294 ++ servo/components/style/values/generics/motion.rs | 117 + servo/components/style/values/generics/position.rs | 264 ++ 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 | 157 + .../components/style/values/generics/transform.rs | 830 ++++ servo/components/style/values/generics/ui.rs | 138 + servo/components/style/values/generics/url.rs | 47 + servo/components/style/values/mod.rs | 425 ++ servo/components/style/values/resolved/color.rs | 45 + servo/components/style/values/resolved/counters.rs | 47 + servo/components/style/values/resolved/mod.rs | 261 ++ servo/components/style/values/specified/align.rs | 817 ++++ servo/components/style/values/specified/angle.rs | 240 ++ .../style/values/specified/background.rs | 143 + .../style/values/specified/basic_shape.rs | 332 ++ servo/components/style/values/specified/border.rs | 311 ++ servo/components/style/values/specified/box.rs | 1996 ++++++++++ servo/components/style/values/specified/calc.rs | 644 +++ servo/components/style/values/specified/color.rs | 665 ++++ servo/components/style/values/specified/column.rs | 11 + .../components/style/values/specified/counters.rs | 211 + servo/components/style/values/specified/easing.rs | 106 + servo/components/style/values/specified/effects.rs | 362 ++ servo/components/style/values/specified/flex.rs | 25 + servo/components/style/values/specified/font.rs | 2385 +++++++++++ servo/components/style/values/specified/gecko.rs | 75 + servo/components/style/values/specified/grid.rs | 349 ++ servo/components/style/values/specified/image.rs | 1170 ++++++ servo/components/style/values/specified/length.rs | 1285 ++++++ servo/components/style/values/specified/list.rs | 216 + servo/components/style/values/specified/mod.rs | 933 +++++ servo/components/style/values/specified/motion.rs | 220 ++ servo/components/style/values/specified/outline.rs | 71 + .../style/values/specified/percentage.rs | 174 + .../components/style/values/specified/position.rs | 956 +++++ servo/components/style/values/specified/rect.rs | 11 + .../style/values/specified/resolution.rs | 77 + .../style/values/specified/source_size_list.rs | 145 + servo/components/style/values/specified/svg.rs | 336 ++ .../components/style/values/specified/svg_path.rs | 896 +++++ servo/components/style/values/specified/text.rs | 1195 ++++++ servo/components/style/values/specified/time.rs | 176 + .../components/style/values/specified/transform.rs | 481 +++ servo/components/style/values/specified/ui.rs | 238 ++ servo/components/style/values/specified/url.rs | 18 + 276 files changed, 96195 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/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/element_state.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/boxed_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/profiler.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/hash.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/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_condition.rs create mode 100644 servo/components/style/media_queries/media_feature.rs create mode 100644 servo/components/style/media_queries/media_feature_expression.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/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/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/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/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/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/import_rule.rs create mode 100644 servo/components/style/stylesheets/keyframes_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/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/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/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/percentage.rs create mode 100644 servo/components/style/values/computed/position.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/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/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/position.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/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/percentage.rs create mode 100644 servo/components/style/values/specified/position.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/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..95ebd60c1a --- /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", "fallible/known_system_malloc", "bindgen", "regex", "toml"] +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 = [] +gecko_profiler = [] + +[dependencies] +app_units = "0.7" +arrayvec = "0.5" +atomic_refcell = "0.1" +bitflags = "1.0" +byteorder = "1.0" +cssparser = "0.28" +derive_more = "0.99" +new_debug_unreachable = "1.0" +encoding_rs = {version = "0.8", optional = true} +euclid = "0.22" +fallible = { path = "../fallible" } +fxhash = "0.2" +hashbrown = "0.9" +hashglobe = { path = "../hashglobe" } +html5ever = {version = "0.24", optional = true} +indexmap = {version = "1.0", features = ["std"]} +itertools = "0.8" +itoa = "0.4" +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.5" +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_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} +thin-slice = "0.1.0" +to_shmem = {path = "../to_shmem"} +to_shmem_derive = {path = "../to_shmem_derive"} +time = "0.1" +uluru = "0.4" +unicode-bidi = "0.3" +unicode-segmentation = "1.0" +void = "1.0.2" + +[build-dependencies] +lazy_static = "1" +log = "0.4" +bindgen = {version = "0.56", optional = true, default-features = false} +regex = {version = "1.0", optional = true, default-features = false, features = ["perf", "std"]} +walkdir = "2.1.4" +toml = {version = "0.4.5", optional = true, default-features = false} 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..ac6d4efa16 --- /dev/null +++ b/servo/components/style/animation.rs @@ -0,0 +1,1407 @@ +/* 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::bezier::Bezier; +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::values::animated::{Animate, Procedure}; +use crate::values::computed::{Time, TimingFunction}; +use crate::values::generics::box_::AnimationIterationCount; +use crate::values::generics::easing::{StepPosition, TimingFunction as GenericTimingFunction}; +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); + match self.timing_function { + GenericTimingFunction::CubicBezier { x1, y1, x2, y2 } => { + Bezier::new(x1, y1, x2, y2).solve(progress, epsilon) + }, + GenericTimingFunction::Steps(steps, pos) => { + let mut current_step = (progress * (steps as f64)).floor() as i32; + + if pos == StepPosition::Start || + pos == StepPosition::JumpStart || + pos == StepPosition::JumpBoth + { + current_step = current_step + 1; + } + + // FIXME: We should update current_step according to the "before flag". + // 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. + // (i.e. Treat before_flag is unset,) + // https://drafts.csswg.org/css-easing/#step-timing-function-algo + + if progress >= 0.0 && current_step < 0 { + current_step = 0; + } + + let jumps = match pos { + StepPosition::JumpBoth => steps + 1, + StepPosition::JumpNone => steps - 1, + StepPosition::JumpStart | + StepPosition::JumpEnd | + StepPosition::Start | + StepPosition::End => steps, + }; + + if progress <= 1.0 && current_step > jumps { + current_step = jumps; + } + + (current_step as f64) / (jumps as f64) + }, + GenericTimingFunction::Keyword(keyword) => { + let (x1, x2, y1, y2) = keyword.to_bezier(); + Bezier::new(x1, x2, y1, y2).solve(progress, 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, + 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(), + }; + 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 timing_function = step.timing_function.unwrap_or(default_timing_function); + let properties_changed_in_step = step.declarations.longhands().clone(); + let step_style = step.resolve_style(element, context, base_style, resolver); + + 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 index = new_style + .get_box() + .animation_name_iter() + .position(|animation_name| Some(&self.name) == animation_name.as_atom()); + let index = match index { + Some(index) => index, + None => return true, + }; + + new_style.get_box().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 let KeyframesIterationState::Finite(ref mut current, max) = self.iteration_state { + // If we are already on the final iteration, just exit now. This prevents + // us from updating the direction, which might be needed for the correct + // handling of animation-fill-mode and also firing animationiteration events + // at the end of animations. + *current = (*current + 1.).min(max); + if *current == max { + return false; + } + } + + // Update the next iteration direction if applicable. + // TODO(mrobinson): The duration might now be wrong for floating point iteration counts. + self.started_at += self.duration + self.delay; + match self.direction { + AnimationDirection::Alternate | AnimationDirection::AlternateReverse => { + self.current_direction = match self.current_direction { + AnimationDirection::Normal => AnimationDirection::Reverse, + AnimationDirection::Reverse => AnimationDirection::Normal, + _ => unreachable!(), + }; + }, + _ => {}, + } + + true + } + + fn iteration_over(&self, time: f64) -> bool { + time > (self.started_at + self.duration) + } + + /// 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 { + match self.state { + AnimationState::Running => {}, + AnimationState::Finished => return true, + AnimationState::Pending | AnimationState::Canceled | AnimationState::Paused(_) => { + return false + }, + } + + if !self.iteration_over(time) { + return false; + } + + // If we have a limited number of iterations and we cannot advance to another + // iteration, then we have ended. + return match self.iteration_state { + KeyframesIterationState::Finite(current, max) => max == current, + KeyframesIterationState::Infinite(..) => false, + }; + } + + /// 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) { + let duration = self.duration; + let started_at = self.started_at; + + let now = match self.state { + AnimationState::Running | AnimationState::Pending | AnimationState::Finished => now, + AnimationState::Paused(progress) => started_at + duration * progress, + AnimationState::Canceled => return, + }; + + debug_assert!(!self.computed_steps.is_empty()); + + let mut total_progress = (now - started_at) / duration; + if total_progress < 0. && + self.fill_mode != AnimationFillMode::Backwards && + self.fill_mode != AnimationFillMode::Both + { + return; + } + + if total_progress > 1. && + self.fill_mode != AnimationFillMode::Forwards && + self.fill_mode != AnimationFillMode::Both + { + return; + } + total_progress = total_progress.min(1.0).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(); + debug_assert!(num_steps > 0); + 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, + }; + + 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 relative_timespan = + (next_keyframe.start_percentage - prev_keyframe.start_percentage).abs(); + let relative_duration = relative_timespan as f64 * duration; + let last_keyframe_ended_at = match self.current_direction { + AnimationDirection::Normal => { + self.started_at + (duration * prev_keyframe.start_percentage as f64) + }, + AnimationDirection::Reverse => { + self.started_at + (duration * (1. - prev_keyframe.start_percentage as f64)) + }, + _ => unreachable!(), + }; + + let relative_progress = (now - last_keyframe_ended_at) / relative_duration; + 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, + duration: relative_duration as f64, + }; + + if let Ok(value) = animation.calculate_value(relative_progress) { + 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 box_style = new_style.get_box(); + let timing_function = box_style.transition_timing_function_mod(index); + let duration = box_style.transition_duration_mod(index); + let delay = box_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 box_style = new_style.get_box(); + for (i, name) in box_style.animation_name_iter().enumerate() { + let name = match name.as_atom() { + Some(atom) => atom, + None => continue, + }; + + debug!("maybe_start_animations: name={}", name); + let duration = box_style.animation_duration_mod(i).seconds(); + 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; + } + + let delay = box_style.animation_delay_mod(i).seconds(); + let animation_start = context.current_time_for_animations + delay as f64; + let iteration_state = match box_style.animation_iteration_count_mod(i) { + AnimationIterationCount::Infinite => KeyframesIterationState::Infinite(0.0), + AnimationIterationCount::Number(n) => KeyframesIterationState::Finite(0.0, n.into()), + }; + + let animation_direction = box_style.animation_direction_mod(i); + + let initial_direction = match animation_direction { + AnimationDirection::Normal | AnimationDirection::Alternate => { + AnimationDirection::Normal + }, + AnimationDirection::Reverse | AnimationDirection::AlternateReverse => { + AnimationDirection::Reverse + }, + }; + + let state = match box_style.animation_play_state_mod(i) { + AnimationPlayState::Paused => AnimationState::Paused(0.), + AnimationPlayState::Running => AnimationState::Pending, + }; + + let computed_steps = ComputedKeyframe::generate_for_keyframes( + element, + &keyframe_animation, + context, + new_style, + new_style.get_box().animation_timing_function_mod(i), + resolver, + ); + + let new_animation = Animation { + name: name.clone(), + properties_changed: keyframe_animation.properties_changed, + computed_steps, + started_at: animation_start, + duration: duration as f64, + fill_mode: box_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, + }; + + 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..6353fb4c5e --- /dev/null +++ b/servo/components/style/applicable_declarations.rs @@ -0,0 +1,120 @@ +/* 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 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. +/// +/// [1] https://cs.chromium.org/chromium/src/third_party/WebKit/Source/core/css/ +/// RuleSet.h?l=128&rcl=90140ab80b84d0f889abc253410f44ed54ae04f3 +const SOURCE_ORDER_SHIFT: usize = 0; +const SOURCE_ORDER_BITS: usize = 24; +const SOURCE_ORDER_MAX: u32 = (1 << SOURCE_ORDER_BITS) - 1; +const SOURCE_ORDER_MASK: u32 = SOURCE_ORDER_MAX << SOURCE_ORDER_SHIFT; + +/// We pack the cascade level in a single byte, see CascadeLevel::to_byte_lossy +/// for the different trade-offs there. +const CASCADE_LEVEL_SHIFT: usize = SOURCE_ORDER_BITS; + +/// Stores the source order of a block, the cascade level it belongs to, and the +/// counter needed to handle Shadow DOM cascade order properly. +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq)] +struct ApplicableDeclarationBits(u32); + +impl ApplicableDeclarationBits { + fn new(source_order: u32, cascade_level: CascadeLevel) -> Self { + Self( + (source_order & SOURCE_ORDER_MASK) | + ((cascade_level.to_byte_lossy() as u32) << CASCADE_LEVEL_SHIFT), + ) + } + + fn source_order(&self) -> u32 { + self.0 & SOURCE_ORDER_MASK + } + + fn level(&self) -> CascadeLevel { + CascadeLevel::from_byte((self.0 >> CASCADE_LEVEL_SHIFT) as u8) + } +} + +/// 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. + bits: ApplicableDeclarationBits, + /// The specificity of the selector this block is represented by. + pub specificity: u32, +} + +impl ApplicableDeclarationBlock { + /// Constructs an applicable declaration block from a given property + /// declaration block and importance. + #[inline] + pub fn from_declarations( + declarations: Arc>, + level: CascadeLevel, + ) -> Self { + ApplicableDeclarationBlock { + source: StyleSource::from_declarations(declarations), + bits: ApplicableDeclarationBits::new(0, level), + specificity: 0, + } + } + + /// Constructs an applicable declaration block from the given components + #[inline] + pub fn new(source: StyleSource, order: u32, level: CascadeLevel, specificity: u32) -> Self { + ApplicableDeclarationBlock { + source, + bits: ApplicableDeclarationBits::new(order, level), + specificity, + } + } + + /// Returns the source order of the block. + #[inline] + pub fn source_order(&self) -> u32 { + self.bits.source_order() + } + + /// Returns the cascade level of the block. + #[inline] + pub fn level(&self) -> CascadeLevel { + self.bits.level() + } + + /// 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, CascadeLevel) { + let level = self.level(); + (self.source, level) + } +} diff --git a/servo/components/style/attr.rs b/servo/components/style/attr.rs new file mode 100644 index 0000000000..92ab7700fb --- /dev/null +++ b/servo/components/style/attr.rs @@ -0,0 +1,598 @@ +/* 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::{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<&String>) -> 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..58d9bda423 --- /dev/null +++ b/servo/components/style/author_styles.rs @@ -0,0 +1,78 @@ +/* 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::context::QuirksMode; +use crate::dom::TElement; +#[cfg(feature = "gecko")] +use crate::gecko_bindings::sugar::ownership::{HasBoxFFI, HasFFI, HasSimpleFFI}; +use crate::invalidation::media_queries::ToMediaListKey; +use crate::media_queries::Device; +use crate::shared_lock::SharedRwLockReadGuard; +use crate::stylesheet_set::AuthorStylesheetSet; +use crate::stylesheets::StylesheetInDocument; +use crate::stylist::CascadeData; + +/// A set of author stylesheets and their computed representation, such as the +/// ones used for ShadowRoot. +#[derive(MallocSizeOf)] +pub struct AuthorStyles +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. + pub data: CascadeData, +} + +impl AuthorStyles +where + S: StylesheetInDocument + PartialEq + 'static, +{ + /// Create an empty AuthorStyles. + #[inline] + pub fn new() -> Self { + Self { + stylesheets: AuthorStylesheetSet::new(), + data: CascadeData::new(), + } + } + + /// 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, + device: &Device, + quirks_mode: QuirksMode, + guard: &SharedRwLockReadGuard, + ) where + E: TElement, + S: ToMediaListKey, + { + let flusher = self + .stylesheets + .flush::(/* host = */ None, /* snapshot_map = */ None); + + // Ignore OOM. + let _ = self + .data + .rebuild(device, quirks_mode, flusher.sheets, guard); + } +} + +#[cfg(feature = "gecko")] +unsafe impl HasFFI for AuthorStyles { + type FFIType = crate::gecko_bindings::structs::RawServoAuthorStyles; +} +#[cfg(feature = "gecko")] +unsafe impl HasSimpleFFI for AuthorStyles {} +#[cfg(feature = "gecko")] +unsafe impl HasBoxFFI for AuthorStyles {} diff --git a/servo/components/style/bezier.rs b/servo/components/style/bezier.rs new file mode 100644 index 0000000000..bc3dc883b2 --- /dev/null +++ b/servo/components/style/bezier.rs @@ -0,0 +1,126 @@ +/* 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 { + /// Create 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%. + #[inline] + pub 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] + pub 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..c17b31d1be --- /dev/null +++ b/servo/components/style/bloom.rs @@ -0,0 +1,385 @@ +/* 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, + } + } +} + +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())); +} + +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..1e93660936 --- /dev/null +++ b/servo/components/style/build_gecko.rs @@ -0,0 +1,411 @@ +/* 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 = PathBuf::from(env::var_os("MOZ_SRC").unwrap()) + .join("layout/style/ServoBindings.toml"); + read_config(&path) + }; + static ref BUILD_CONFIG: Table = { + // Load build-specific config overrides. + let path = PathBuf::from(env::var_os("MOZ_TOPOBJDIR").unwrap()) + .join("layout/style/bindgen.toml"); + read_config(&path) + }; + static ref INCLUDE_RE: Regex = Regex::new(r#"#include\s*"(.+?)""#).unwrap(); + static ref DISTDIR_PATH: PathBuf = { + let path = PathBuf::from(env::var_os("MOZ_DIST").unwrap()); + if !path.is_absolute() || !path.is_dir() { + panic!("MOZ_DIST must be an absolute directory, was: {}", path.display()); + } + path + }; + 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 { + use bindgen::RustTarget; + + // Disable rust unions, because we replace some types inside of + // them. + let mut builder = Builder::default() + .rust_target(RustTarget::Stable_1_25) + .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"); + } + + let build_config = BUILD_CONFIG["build"] + .as_table() + .expect("Malformed config file"); + let extra_bindgen_flags = build_config["args"].as_array().unwrap().as_slice(); + for item in extra_bindgen_flags.iter() { + builder = builder.clang_arg(item.as_str().expect("Expect string in list")); + } + + 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.blacklist_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!(format!("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("whitelist-functions", |b, item| b.whitelist_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("whitelist-vars", |b, item| b.whitelist_var(item)) + .handle_str_items("whitelist-types", |b, item| b.whitelist_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.blacklist_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.blacklist_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/context.rs b/servo/components/style/context.rs new file mode 100644 index 0000000000..608e58ea03 --- /dev/null +++ b/servo/components/style/context.rs @@ -0,0 +1,803 @@ +/* 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::data::{EagerPseudoStyles, ElementData}; +use crate::dom::{SendElement, TElement}; +use crate::font_metrics::FontMetricsProvider; +#[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; +use fxhash::FxHashMap; +use selectors::matching::ElementSelectorFlags; +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; +use uluru::{Entry, LRUCache}; + +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), + } + } +} + +impl StyleSystemOptions { + #[cfg(feature = "servo")] + /// On Gecko's nightly build? + pub fn is_nightly(&self) -> bool { + false + } + + #[cfg(feature = "gecko")] + /// On Gecko's nightly build? + #[inline] + pub fn is_nightly(&self) -> bool { + structs::GECKO_IS_NIGHTLY + } +} + +/// 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, +} + +impl CascadeInputs { + /// Construct inputs from previous cascade results, if any. + pub fn new_from_style(style: &ComputedValues) -> Self { + CascadeInputs { + rules: style.rules.clone(), + visited_rules: style.visited_style().and_then(|v| v.rules.clone()), + } + } +} + +/// 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; + } +} + +#[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, + } + } +} + +type CacheItem = (SendElement, ElementSelectorFlags); + +/// Map from Elements to ElementSelectorFlags. Used to defer applying selector +/// flags until after the traversal. +pub struct SelectorFlagsMap { + /// The hashmap storing the flags to apply. + map: FxHashMap, ElementSelectorFlags>, + /// An LRU cache to avoid hashmap lookups, which can be slow if the map + /// gets big. + cache: LRUCache<[Entry>; 4 + 1]>, +} + +#[cfg(debug_assertions)] +impl Drop for SelectorFlagsMap { + fn drop(&mut self) { + debug_assert!(self.map.is_empty()); + } +} + +impl SelectorFlagsMap { + /// Creates a new empty SelectorFlagsMap. + pub fn new() -> Self { + SelectorFlagsMap { + map: FxHashMap::default(), + cache: LRUCache::default(), + } + } + + /// Inserts some flags into the map for a given element. + pub fn insert_flags(&mut self, element: E, flags: ElementSelectorFlags) { + let el = unsafe { SendElement::new(element) }; + // Check the cache. If the flags have already been noted, we're done. + if let Some(item) = self.cache.find(|x| x.0 == el) { + if !item.1.contains(flags) { + item.1.insert(flags); + self.map.get_mut(&el).unwrap().insert(flags); + } + return; + } + + let f = self.map.entry(el).or_insert(ElementSelectorFlags::empty()); + *f |= flags; + + self.cache + .insert((unsafe { SendElement::new(element) }, *f)) + } + + /// Applies the flags. Must be called on the main thread. + fn apply_flags(&mut self) { + debug_assert_eq!(thread_state::get(), ThreadState::LAYOUT); + self.cache.evict_all(); + for (el, flags) in self.map.drain() { + unsafe { + el.set_selector_flags(flags); + } + } + } +} + +/// 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, + /// ElementSelectorFlags that need to be applied after the traversal is + /// complete. This map is used in cases where the matching algorithm needs + /// to set flags on elements it doesn't have exclusive access to (i.e. other + /// than the current element). + pub selector_flags: SelectorFlagsMap, + /// Statistics about the traversal. + pub statistics: PerThreadTraversalStatistics, + /// The struct used to compute and cache font metrics from style + /// for evaluation of the font-relative em/ch units and font-size + pub font_metrics_provider: E::FontMetricsProvider, + /// 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` from a shared one. + #[cfg(feature = "servo")] + pub fn new(shared: &SharedStyleContext) -> Self { + ThreadLocalStyleContext { + sharing_cache: StyleSharingCache::new(), + rule_cache: RuleCache::new(), + bloom_filter: StyleBloom::new(), + tasks: SequentialTaskList(Vec::new()), + selector_flags: SelectorFlagsMap::new(), + statistics: PerThreadTraversalStatistics::default(), + font_metrics_provider: E::FontMetricsProvider::create_from(shared), + stack_limit_checker: StackLimitChecker::new( + (STYLE_THREAD_STACK_SIZE_KB - STACK_SAFETY_MARGIN_KB) * 1024, + ), + nth_index_cache: NthIndexCache::default(), + } + } + + #[cfg(feature = "gecko")] + /// Creates a new `ThreadLocalStyleContext` from a shared one. + pub fn new(shared: &SharedStyleContext) -> Self { + ThreadLocalStyleContext { + sharing_cache: StyleSharingCache::new(), + rule_cache: RuleCache::new(), + bloom_filter: StyleBloom::new(), + tasks: SequentialTaskList(Vec::new()), + selector_flags: SelectorFlagsMap::new(), + statistics: PerThreadTraversalStatistics::default(), + font_metrics_provider: E::FontMetricsProvider::create_from(shared), + stack_limit_checker: StackLimitChecker::new( + (STYLE_THREAD_STACK_SIZE_KB - STACK_SAFETY_MARGIN_KB) * 1024, + ), + nth_index_cache: NthIndexCache::default(), + } + } +} + +impl Drop for ThreadLocalStyleContext { + fn drop(&mut self) { + debug_assert_eq!(thread_state::get(), ThreadState::LAYOUT); + + // Apply any slow selector flags that need to be set on parents. + self.selector_flags.apply_flags(); + } +} + +/// 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..c7854b1ef1 --- /dev/null +++ b/servo/components/style/counter_style/mod.rs @@ -0,0 +1,672 @@ +/* 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, DeclarationListParser, DeclarationParser}; +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") +} + +/// 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 parser = CounterStyleRuleParser { + context: context, + rule: &mut rule, + }; + let mut iter = DeclarationListParser::new(input, 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 PreludeNoBlock = (); + type PreludeBlock = (); + type AtRule = (); + type Error = StyleParseErrorKind<'i>; +} + +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_str("}") + } + } + } +} + +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::pixel(device.safe_area_insets().top) +} + +fn get_safearea_inset_bottom(device: &Device) -> VariableValue { + VariableValue::pixel(device.safe_area_insets().bottom) +} + +fn get_safearea_inset_left(device: &Device) -> VariableValue { + VariableValue::pixel(device.safe_area_insets().left) +} + +fn get_safearea_inset_right(device: &Device) -> VariableValue { + VariableValue::pixel(device.safe_area_insets().right) +} + +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), +]; + +impl CssEnvironment { + #[inline] + fn get(&self, name: &Atom, device: &Device) -> Option { + let var = 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("--") { + 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 (1MB) is somewhat arbitrary, but silly enough that no + /// sane page would 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 = 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 css pixel value + pub fn pixel(number: f32) -> Self { + // FIXME (https://github.com/servo/rust-cssparser/issues/266): + // No way to get TokenSerializationType::Dimension without creating + // Token object. + let token = Token::Dimension { + has_sign: false, + value: number, + int_value: None, + unit: CowRcStr::from("px"), + }; + 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| { + // Need at least one token + let start = input.state(); + input.next_including_whitespace()?; + input.reset(&start); + + 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>> { + 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| { + // At least one non-comment token. + input.next_including_whitespace()?; + // Skip until the end. + while let Ok(_) = input.next_including_whitespace_and_comments() {} + 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>, + reverted: PerOrigin>, + may_have_cycles: bool, + custom_properties: Option, + inherited: Option<&'a Arc>, + 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, origin: Origin) { + let CustomDeclaration { + ref name, + ref value, + } = *declaration; + + if self.reverted.borrow_for_origin(&origin).contains(&name) { + 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(..) => { + // Don't touch the map, this has the same effect as + // making it compute to the inherited one. + return; + }, + } + } else { + (*unparsed_value).clone() + }; + map.insert(name.clone(), value); + }, + CustomDeclarationValue::CSSWideKeyword(keyword) => match keyword { + CSSWideKeyword::Revert => { + self.seen.remove(name); + for origin in origin.following_including() { + self.reverted.borrow_mut_for_origin(&origin).insert(name); + } + }, + 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 + } + + /// 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 { + let inherited = self.inherited.as_ref().map(|m| &***m); + substitute_all(&mut map, inherited, self.device); + } + 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, + inherited: Option<&CustomPropertiesMap>, + 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, + /// The inherited variables. We may need to restore some if we fail + /// substitution. + inherited: Option<&'a 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) { + 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.clone(), 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 unset (inherited) value. + let inherited = context.inherited.and_then(|m| m.get(&name)).cloned(); + match inherited { + Some(computed_value) => { + context.map.insert(name, computed_value); + }, + None => { + context.map.remove(&name); + }, + }; + }, + } + + // All resolved, so return the signal value. + None + } + + // We have to clone the names so that we can mutably borrow the map + // in the context we create for traversal. + let names: Vec<_> = custom_properties_map.keys().cloned().collect(); + for name in names.into_iter() { + let mut context = Context { + count: 0, + index_map: PrecomputedHashMap::default(), + stack: SmallVec::new(), + var_info: SmallVec::new(), + map: custom_properties_map, + inherited, + 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()?; + let after_comma = input.state(); + let first_token_type = input + .next_including_whitespace_and_comments() + // parse_var_function() ensures that .unwrap() will not fail. + .unwrap() + .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..76d64956f1 --- /dev/null +++ b/servo/components/style/data.rs @@ -0,0 +1,451 @@ +/* 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::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, +} + +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() + } + + #[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, +} + +/// 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) -> RestyleKind { + if shared_context.traversal_flags.for_animation_only() { + return self.restyle_kind_for_animation(shared_context); + } + + if !self.has_styles() { + return RestyleKind::MatchAndCascade; + } + + if self.hint.match_self() { + return RestyleKind::MatchAndCascade; + } + + if self.hint.has_replacements() { + debug_assert!( + !self.hint.has_animation_hint(), + "Animation only restyle hint should have already processed" + ); + return RestyleKind::CascadeWithReplacements(self.hint & RestyleHint::replacements()); + } + + debug_assert!( + self.hint.has_recascade_self(), + "We definitely need to do something: {:?}!", + self.hint + ); + return RestyleKind::CascadeOnly; + } + + /// Returns the kind of restyling for animation-only restyle. + fn restyle_kind_for_animation(&self, shared_context: &SharedStyleContext) -> RestyleKind { + debug_assert!(shared_context.traversal_flags.for_animation_only()); + debug_assert!( + self.has_styles(), + "Unstyled element shouldn't be traversed during \ + animation-only traversal" + ); + + // return either CascadeWithReplacements or CascadeOnly in case of + // animation-only restyle. I.e. animation-only restyle never does + // selector matching. + if self.hint.has_animation_hint() { + return RestyleKind::CascadeWithReplacements(self.hint & RestyleHint::for_animations()); + } + + return RestyleKind::CascadeOnly; + } + + /// 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 { + !self.flags.intersects( + ElementDataFlags::TRAVERSED_WITHOUT_STYLING | + ElementDataFlags::PRIMARY_STYLE_REUSED_VIA_RULE_NODE, + ) + } + + /// 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..86c5a38289 --- /dev/null +++ b/servo/components/style/dom.rs @@ -0,0 +1,986 @@ +/* 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::element_state::ElementState; +use crate::font_metrics::FontMetricsProvider; +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::traversal_flags::TraversalFlags; +use crate::values::AtomIdent; +use crate::{LocalName, Namespace, WeakAtom}; +use atomic_refcell::{AtomicRef, AtomicRefMut}; +use selectors::matching::{ElementSelectorFlags, 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(Some(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 first 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. + 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. + fn dom_descendants(&self) -> DomDescendants { + DomDescendants { + previous: Some(*self), + scope: *self, + } + } + + /// Returns the next children in pre-order, optionally scoped to a subtree + /// root. + #[inline] + fn next_in_preorder(&self, scoped_to: Option) -> Option { + if let Some(c) = self.first_child() { + return Some(c); + } + + let mut current = Some(*self); + loop { + if current == scoped_to { + return None; + } + + debug_assert!(current.is_some(), "not a descendant of the scope?"); + if let Some(s) = current?.next_sibling() { + return Some(s); + } + + 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()) + } + + /// 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; + + /// Type of the font metrics provider + /// + /// XXXManishearth It would be better to make this a type parameter on + /// ThreadLocalStyleContext and StyleContext + type FontMetricsProvider: FontMetricsProvider + Send; + + /// 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 author rules. + /// + /// We use this for Native Anonymous Content in Gecko. + fn matches_user_and_author_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 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 for |traversal_flags|. + fn has_current_styles_for_traversal( + &self, + data: &ElementData, + traversal_flags: TraversalFlags, + ) -> bool { + if traversal_flags.for_animation_only() { + // In animation-only restyle we never touch snapshots and don't care + // about them. But we can't assert '!self.handled_snapshot()' + // here since there are some cases that a second animation-only + // restyle which is a result of normal restyle (e.g. setting + // animation-name in normal restyle and creating a new CSS + // animation in a SequentialTask) is processed after the normal + // traversal in that we had elements that handled snapshot. + if !data.has_styles() { + return false; + } + + if !data.hint.has_animation_hint_or_recascade() { + return true; + } + + // FIXME: This should ideally always return false, 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. + if data.styles.is_display_none() && data.hint.match_self() { + return true; + } + + return false; + } + + if self.has_snapshot() && !self.handled_snapshot() { + return false; + } + + data.has_styles() && !data.hint.has_non_animation_invalidations() + } + + /// 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 true if this element is in a native anonymous subtree. + fn is_in_native_anonymous_subtree(&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; + + /// Sets selector flags, which indicate what kinds of selectors may have + /// matched on this element and therefore what kind of work may need to + /// be performed when DOM state changes. + /// + /// This is unsafe, like all the flag-setting methods, because it's only safe + /// to call with exclusive access to the element. When setting flags on the + /// parent during parallel traversal, we use SequentialTask to queue up the + /// set to run after the threads join. + unsafe fn set_selector_flags(&self, flags: ElementSelectorFlags); + + /// Returns true if the element has all the specified selector flags. + fn has_selector_flags(&self, flags: ElementSelectorFlags) -> 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(); + if !target.matches_user_and_author_rules() { + return false; + } + + let mut doc_rules_apply = true; + + // 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 !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 = true; + 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; +} + +/// 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..a6c50805cb --- /dev/null +++ b/servo/components/style/dom_apis.rs @@ -0,0 +1,671 @@ +/* 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::values::AtomIdent; +use selectors::attr::CaseSensitivity; +use selectors::matching::{self, MatchingContext, MatchingMode}; +use selectors::parser::{Combinator, Component, LocalName, SelectorImpl}; +use selectors::{Element, NthIndexCache, SelectorList}; +use smallvec::SmallVec; +use std::borrow::Borrow; + +/// +pub fn element_matches( + element: &E, + selector_list: &SelectorList, + quirks_mode: QuirksMode, +) -> bool +where + E: Element, +{ + let mut context = MatchingContext::new(MatchingMode::Normal, None, None, quirks_mode); + 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 = NthIndexCache::default(); + + let mut context = MatchingContext::new( + MatchingMode::Normal, + None, + Some(&mut nth_index_cache), + quirks_mode, + ); + 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 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; + if element.is_html_element_in_html_document() { + element.local_name() == lower_name.borrow() + } else { + element.local_name() == name.borrow() + } +} + +/// 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, +{ + match *component { + Component::ExplicitUniversalType => { + collect_all_elements::(root, results, |_| true) + }, + Component::ID(ref id) => { + collect_elements_with_id::(root, id, results, quirks_mode, |_| 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) + }) + }, + // TODO(emilio): More fast paths? + _ => return Err(()), + } + + Ok(()) +} + +enum SimpleFilter<'a, Impl: SelectorImpl> { + 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::ID(ref id) => { + 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(()); + }, + 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)); + } + }, + _ => {}, + } + } + + 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 quirks_mode = root.owner_doc().quirks_mode(); + + let mut nth_index_cache = NthIndexCache::default(); + let mut matching_context = MatchingContext::new( + MatchingMode::Normal, + None, + Some(&mut nth_index_cache), + quirks_mode, + ); + + 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..eab7c3a6a3 --- /dev/null +++ b/servo/components/style/driver.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/. */ + +//! 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::{DispatchMode, WORK_UNIT_MAX}; +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; +} + +/// 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(traversal.shared_context()); + 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 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 nodes_remaining_at_current_depth == 0 { + 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 pool.is_some() && discovered.len() > WORK_UNIT_MAX { + let pool = pool.unwrap(); + let tls = ScopedTLS::>::new(pool); + let root_opaque = root.as_node().opaque(); + let drain = discovered.drain(..); + pool.install(|| { + // 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. + rayon::scope_fifo(|scope| { + profiler_label!(Style); + parallel::traverse_nodes( + drain, + 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/element_state.rs b/servo/components/style/element_state.rs new file mode 100644 index 0000000000..e05ff8778e --- /dev/null +++ b/servo/components/style/element_state.rs @@ -0,0 +1,152 @@ +/* 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/. */ + +//! States elements can be in. + +bitflags! { + /// Event-based element states. + /// + /// NB: Is important for this to remain in sync with Gecko's + /// dom/events/EventStates.h. + /// + /// Please keep in that order in order for this to be easily auditable. + /// + /// TODO(emilio): We really really want to use the NS_EVENT_STATE bindings + /// for this. + #[derive(MallocSizeOf)] + pub struct ElementState: u64 { + /// The mouse is down on this element. + /// + /// FIXME(#7333): set/unset this when appropriate + const IN_ACTIVE_STATE = 1 << 0; + /// This element has focus. + /// + const IN_FOCUS_STATE = 1 << 1; + /// The mouse is hovering over this element. + /// + const IN_HOVER_STATE = 1 << 2; + /// Content is enabled (and can be disabled). + /// + const IN_ENABLED_STATE = 1 << 3; + /// Content is disabled. + /// + const IN_DISABLED_STATE = 1 << 4; + /// Content is checked. + /// + const IN_CHECKED_STATE = 1 << 5; + /// + const IN_INDETERMINATE_STATE = 1 << 6; + /// + const IN_PLACEHOLDER_SHOWN_STATE = 1 << 7; + /// + const IN_TARGET_STATE = 1 << 8; + /// + const IN_FULLSCREEN_STATE = 1 << 9; + /// + const IN_VALID_STATE = 1 << 10; + /// + const IN_INVALID_STATE = 1 << 11; + /// Non-standard: https://developer.mozilla.org/en-US/docs/Web/CSS/:-moz-ui-valid + const IN_MOZ_UI_VALID_STATE = 1 << 12; + /// Non-standard: https://developer.mozilla.org/en-US/docs/Web/CSS/:-moz-ui-invalid + const IN_MOZ_UI_INVALID_STATE = 1 << 13; + /// Non-standard: https://developer.mozilla.org/en-US/docs/Web/CSS/:-moz-broken + const IN_BROKEN_STATE = 1 << 14; + /// Non-standard: https://developer.mozilla.org/en-US/docs/Web/CSS/:-moz-loading + const IN_LOADING_STATE = 1 << 17; + /// + const IN_REQUIRED_STATE = 1 << 21; + /// + const IN_OPTIONAL_STATE = 1 << 22; + /// + const IN_READ_WRITE_STATE = 1 << 22; + /// + const IN_DEFINED_STATE = 1 << 23; + /// + const IN_VISITED_STATE = 1 << 24; + /// + const IN_UNVISITED_STATE = 1 << 25; + /// + const IN_VISITED_OR_UNVISITED_STATE = ElementState::IN_VISITED_STATE.bits | + ElementState::IN_UNVISITED_STATE.bits; + /// Non-standard: https://developer.mozilla.org/en-US/docs/Web/CSS/:-moz-drag-over + const IN_DRAGOVER_STATE = 1 << 26; + /// + const IN_INRANGE_STATE = 1 << 27; + /// + const IN_OUTOFRANGE_STATE = 1 << 28; + /// + const IN_READONLY_STATE = 1 << 29; + /// + const IN_READWRITE_STATE = 1 << 30; + /// + const IN_DEFAULT_STATE = 1 << 31; + /// Non-standard: https://developer.mozilla.org/en-US/docs/Web/CSS/:-moz-submit-invalid + const IN_MOZ_SUBMITINVALID_STATE = 1 << 32; + /// Non-standard & undocumented. + const IN_OPTIMUM_STATE = 1 << 33; + /// Non-standard & undocumented. + const IN_SUB_OPTIMUM_STATE = 1 << 34; + /// Non-standard & undocumented. + const IN_SUB_SUB_OPTIMUM_STATE = 1 << 35; + /// Non-standard & undocumented. + const IN_DEVTOOLS_HIGHLIGHTED_STATE = 1 << 36; + /// Non-standard & undocumented. + const IN_STYLEEDITOR_TRANSITIONING_STATE = 1 << 37; + /// Non-standard & undocumented. + const IN_INCREMENT_SCRIPT_LEVEL_STATE = 1 << 38; + /// Non-standard: https://developer.mozilla.org/en-US/docs/Web/CSS/:-moz-focusring + /// + /// But also https://drafts.csswg.org/selectors-4/#the-focus-visible-pseudo + const IN_FOCUSRING_STATE = 1 << 39; + /// + const IN_FOCUS_WITHIN_STATE = 1 << 43; + /// :dir matching; the states are used for dynamic change detection. + /// State that elements that match :dir(ltr) are in. + const IN_LTR_STATE = 1 << 44; + /// State that elements that match :dir(rtl) are in. + const IN_RTL_STATE = 1 << 45; + /// State that HTML elements that have a "dir" attr are in. + const IN_HAS_DIR_ATTR_STATE = 1 << 46; + /// State that HTML elements with dir="ltr" (or something + /// case-insensitively equal to "ltr") are in. + const IN_HAS_DIR_ATTR_LTR_STATE = 1 << 47; + /// State that HTML elements with dir="rtl" (or something + /// case-insensitively equal to "rtl") are in. + const IN_HAS_DIR_ATTR_RTL_STATE = 1 << 48; + /// State that HTML elements without a valid-valued "dir" attr or + /// any HTML elements (including ) with dir="auto" (or something + /// case-insensitively equal to "auto") are in. + const IN_HAS_DIR_ATTR_LIKE_AUTO_STATE = 1 << 49; + /// Non-standard & undocumented. + const IN_AUTOFILL_STATE = 1 << 50; + /// Non-standard & undocumented. + const IN_AUTOFILL_PREVIEW_STATE = 1 << 51; + /// State that dialog element is modal, for centered alignment + /// + /// https://html.spec.whatwg.org/multipage/#centered-alignment + const IN_MODAL_DIALOG_STATE = 1 << 53; + + /// https://html.spec.whatwg.org/multipage/interaction.html#inert-subtrees + const IN_MOZINERT_STATE = 1 << 54; + /// State for the topmost dialog element in top layer + const IN_TOPMOST_MODAL_DIALOG_STATE = 1 << 55; + /// Non-standard & undocumented. + const IN_HANDLER_NOPLUGINS = 1 << 56; + } +} + +bitflags! { + /// Event-based document states. + /// + /// NB: Is important for this to remain in sync with Gecko's + /// dom/base/Document.h. + #[derive(MallocSizeOf)] + pub struct DocumentState: u64 { + /// RTL locale: specific to the XUL localedir attribute + const NS_DOCUMENT_STATE_RTL_LOCALE = 1 << 0; + /// Window activation status + const NS_DOCUMENT_STATE_WINDOW_INACTIVE = 1 << 1; + } +} 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..6e63b0ad68 --- /dev/null +++ b/servo/components/style/error_reporting.rs @@ -0,0 +1,258 @@ +/* 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 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 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>), +} + +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::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::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), + } + } +} + +/// 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..24204efc13 --- /dev/null +++ b/servo/components/style/font_face.rs @@ -0,0 +1,594 @@ +/* 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; +use crate::values::generics::font::FontStyle as GenericFontStyle; +#[cfg(feature = "gecko")] +use crate::values::specified::font::SpecifiedFontFeatureSettings; +use crate::values::specified::font::SpecifiedFontStyle; +#[cfg(feature = "gecko")] +use crate::values::specified::font::SpecifiedFontVariationSettings; +use crate::values::specified::font::{AbsoluteFontWeight, FontStretch}; +use crate::values::specified::url::SpecifiedUrl; +use crate::values::specified::Angle; +#[cfg(feature = "gecko")] +use cssparser::UnicodeRange; +use cssparser::{AtRuleParser, DeclarationListParser, DeclarationParser, Parser}; +use cssparser::{CowRcStr, SourceLocation}; +use selectors::parser::SelectorParseErrorKind; +use std::fmt::{self, Write}; +use style_traits::values::SequenceWriter; +use style_traits::{Comma, CssWriter, OneOrMoreSeparated, 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), +} + +impl OneOrMoreSeparated for Source { + type S = Comma; +} + +/// 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")] +#[repr(u8)] +#[allow(missing_docs)] +pub enum FontFaceSourceListComponent { + Url(*const crate::gecko::url::CssUrl), + Local(*mut crate::gecko_bindings::structs::nsAtom), + FormatHint { + length: usize, + utf8_bytes: *const u8, + }, +} + +/// 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 hints specified with the `format()` function. + pub format_hints: Vec, +} + +impl ToCss for UrlSource { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: fmt::Write, + { + self.url.to_css(dest)?; + if !self.format_hints.is_empty() { + dest.write_str(" format(")?; + { + let mut writer = SequenceWriter::new(dest, ", "); + for hint in self.format_hints.iter() { + writer.item(hint)?; + } + } + 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_str(" ")?; + 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().0, self.1.compute().0); + 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 FontStretch, pub FontStretch); +impl_range!(FontStretchRange, FontStretch); + +/// The computed representation of the above, so that +/// Gecko can read them easily. +#[repr(C)] +#[allow(missing_docs)] +pub struct ComputedFontStretchRange(f32, f32); + +impl FontStretchRange { + /// Returns a computed font-stretch range. + pub fn compute(&self) -> ComputedFontStretchRange { + fn compute_stretch(s: &FontStretch) -> f32 { + match *s { + FontStretch::Keyword(ref kw) => kw.compute().0, + FontStretch::Stretch(ref p) => p.get(), + FontStretch::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 parser = FontFaceRuleParser { + context: context, + rule: &mut rule, + }; + let mut iter = DeclarationListParser::new(input, 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(Vec); + +#[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 { + let hints = &url_source.format_hints; + // 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. + hints.is_empty() || + hints.iter().any(|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 PreludeNoBlock = (); + type PreludeBlock = (); + type AtRule = (); + type Error = StyleParseErrorKind<'i>; +} + +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_hints = if input + .try_parse(|input| input.expect_function_matching("format")) + .is_ok() + { + input.parse_nested_block(|input| { + input.parse_comma_separated(|input| Ok(input.expect_string()?.as_ref().to_owned())) + })? + } else { + vec![] + }; + + Ok(Source::Url(UrlSource { + url: url, + format_hints: format_hints, + })) + } +} + +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") + }; + ($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, ": "))?; + ToCss::to_css(value, &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_str("}") + } +} + +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: Vec, + ] + 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: SpecifiedFontFeatureSettings, + + /// The variation settings of this font face. + "font-variation-settings" variation_settings / mFontVariationSettings: SpecifiedFontVariationSettings, + + /// The language override of this font face. + "font-language-override" language_override / mFontLanguageOverride: font_language_override::SpecifiedValue, + ] +} + +#[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: Vec, + ] + optional descriptors = [ + ] +} diff --git a/servo/components/style/font_metrics.rs b/servo/components/style/font_metrics.rs new file mode 100644 index 0000000000..b521fdf76c --- /dev/null +++ b/servo/components/style/font_metrics.rs @@ -0,0 +1,92 @@ +/* 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::context::SharedStyleContext; +use crate::values::computed::Length; +use crate::Atom; + +/// Represents the font metrics that style needs from a font to compute the +/// value of certain CSS units like `ex`. +#[derive(Clone, Debug, Default, 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, +} + +/// 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. + MatchContext, + /// Force getting horizontal metrics. + Horizontal, +} + +/// A trait used to represent something capable of providing us font metrics. +pub trait FontMetricsProvider { + /// Obtain the metrics for given font family. + fn query( + &self, + _context: &crate::values::computed::Context, + _base_size: crate::values::specified::length::FontBaseSize, + _orientation: FontMetricsOrientation, + ) -> FontMetrics { + Default::default() + } + + /// Get default size of a given language and generic family. + fn get_size( + &self, + font_name: &Atom, + font_family: crate::values::computed::font::GenericFontFamily, + ) -> Length; + + /// Construct from a shared style context + fn create_from(context: &SharedStyleContext) -> Self + where + Self: Sized; +} + +// TODO: Servo's font metrics provider will probably not live in this crate, so this will +// have to be replaced with something else (perhaps a trait method on TElement) +// when we get there +#[derive(Debug)] +#[cfg(feature = "servo")] +/// Dummy metrics provider for Servo. Knows nothing about fonts and does not provide +/// any metrics. +pub struct ServoMetricsProvider; + +#[cfg(feature = "servo")] +impl FontMetricsProvider for ServoMetricsProvider { + fn create_from(_: &SharedStyleContext) -> Self { + ServoMetricsProvider + } + + fn get_size(&self, _: &Atom, _: crate::values::computed::font::GenericFontFamily) -> Length { + unreachable!("Dummy provider should never be used to compute font size") + } +} + +// Servo's font metrics provider will probably not live in this crate, so this will +// have to be replaced with something else (perhaps a trait method on TElement) +// when we get there + +#[cfg(feature = "gecko")] +/// Construct a font metrics provider for the current product +pub fn get_metrics_provider_for_product() -> crate::gecko::wrapper::GeckoFontMetricsProvider { + crate::gecko::wrapper::GeckoFontMetricsProvider::new() +} + +#[cfg(feature = "servo")] +/// Construct a font metrics provider for the current product +pub fn get_metrics_provider_for_product() -> ServoMetricsProvider { + ServoMetricsProvider +} diff --git a/servo/components/style/gecko/arc_types.rs b/servo/components/style/gecko/arc_types.rs new file mode 100644 index 0000000000..7aed10b656 --- /dev/null +++ b/servo/components/style/gecko/arc_types.rs @@ -0,0 +1,138 @@ +/* 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 corresponds to ServoArcTypeList.h +//! file in Gecko. + +#![allow(non_snake_case, missing_docs)] + +use crate::gecko::url::CssUrlData; +use crate::gecko_bindings::structs::RawServoAnimationValue; +use crate::gecko_bindings::structs::RawServoCounterStyleRule; +use crate::gecko_bindings::structs::RawServoCssUrlData; +use crate::gecko_bindings::structs::RawServoDeclarationBlock; +use crate::gecko_bindings::structs::RawServoFontFaceRule; +use crate::gecko_bindings::structs::RawServoFontFeatureValuesRule; +use crate::gecko_bindings::structs::RawServoImportRule; +use crate::gecko_bindings::structs::RawServoKeyframe; +use crate::gecko_bindings::structs::RawServoKeyframesRule; +use crate::gecko_bindings::structs::RawServoMediaList; +use crate::gecko_bindings::structs::RawServoMediaRule; +use crate::gecko_bindings::structs::RawServoMozDocumentRule; +use crate::gecko_bindings::structs::RawServoNamespaceRule; +use crate::gecko_bindings::structs::RawServoPageRule; +use crate::gecko_bindings::structs::RawServoStyleRule; +use crate::gecko_bindings::structs::RawServoStyleSheetContents; +use crate::gecko_bindings::structs::RawServoSupportsRule; +use crate::gecko_bindings::structs::ServoCssRules; +use crate::gecko_bindings::sugar::ownership::{HasArcFFI, HasFFI, Strong}; +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::{CounterStyleRule, CssRules, FontFaceRule, FontFeatureValuesRule}; +use crate::stylesheets::{DocumentRule, ImportRule, KeyframesRule, MediaRule}; +use crate::stylesheets::{NamespaceRule, PageRule}; +use crate::stylesheets::{StyleRule, StylesheetContents, SupportsRule}; +use servo_arc::{Arc, ArcBorrow}; +use std::{mem, ptr}; + +macro_rules! impl_arc_ffi { + ($servo_type:ty => $gecko_type:ty[$addref:ident, $release:ident]) => { + unsafe impl HasFFI for $servo_type { + type FFIType = $gecko_type; + } + unsafe impl HasArcFFI for $servo_type {} + + #[no_mangle] + pub unsafe extern "C" fn $addref(obj: &$gecko_type) { + <$servo_type>::addref(obj); + } + + #[no_mangle] + pub unsafe extern "C" fn $release(obj: &$gecko_type) { + <$servo_type>::release(obj); + } + }; +} + +impl_arc_ffi!(Locked => ServoCssRules + [Servo_CssRules_AddRef, Servo_CssRules_Release]); + +impl_arc_ffi!(StylesheetContents => RawServoStyleSheetContents + [Servo_StyleSheetContents_AddRef, Servo_StyleSheetContents_Release]); + +impl_arc_ffi!(Locked => RawServoDeclarationBlock + [Servo_DeclarationBlock_AddRef, Servo_DeclarationBlock_Release]); + +impl_arc_ffi!(Locked => RawServoStyleRule + [Servo_StyleRule_AddRef, Servo_StyleRule_Release]); + +impl_arc_ffi!(Locked => RawServoImportRule + [Servo_ImportRule_AddRef, Servo_ImportRule_Release]); + +impl_arc_ffi!(AnimationValue => RawServoAnimationValue + [Servo_AnimationValue_AddRef, Servo_AnimationValue_Release]); + +impl_arc_ffi!(Locked => RawServoKeyframe + [Servo_Keyframe_AddRef, Servo_Keyframe_Release]); + +impl_arc_ffi!(Locked => RawServoKeyframesRule + [Servo_KeyframesRule_AddRef, Servo_KeyframesRule_Release]); + +impl_arc_ffi!(Locked => RawServoMediaList + [Servo_MediaList_AddRef, Servo_MediaList_Release]); + +impl_arc_ffi!(Locked => RawServoMediaRule + [Servo_MediaRule_AddRef, Servo_MediaRule_Release]); + +impl_arc_ffi!(Locked => RawServoNamespaceRule + [Servo_NamespaceRule_AddRef, Servo_NamespaceRule_Release]); + +impl_arc_ffi!(Locked => RawServoPageRule + [Servo_PageRule_AddRef, Servo_PageRule_Release]); + +impl_arc_ffi!(Locked => RawServoSupportsRule + [Servo_SupportsRule_AddRef, Servo_SupportsRule_Release]); + +impl_arc_ffi!(Locked => RawServoMozDocumentRule + [Servo_DocumentRule_AddRef, Servo_DocumentRule_Release]); + +impl_arc_ffi!(Locked => RawServoFontFeatureValuesRule + [Servo_FontFeatureValuesRule_AddRef, Servo_FontFeatureValuesRule_Release]); + +impl_arc_ffi!(Locked => RawServoFontFaceRule + [Servo_FontFaceRule_AddRef, Servo_FontFaceRule_Release]); + +impl_arc_ffi!(Locked => RawServoCounterStyleRule + [Servo_CounterStyleRule_AddRef, Servo_CounterStyleRule_Release]); + +impl_arc_ffi!(CssUrlData => RawServoCssUrlData + [Servo_CssUrlData_AddRef, Servo_CssUrlData_Release]); + +// ComputedStyle is not an opaque type on any side of FFI. +// This means that doing the HasArcFFI type trick is actually unsound, +// since it gives us a way to construct an Arc from +// an &ComputedStyle, which in general is not allowed. So we +// implement the restricted set of arc type functionality we need. + +#[no_mangle] +pub unsafe extern "C" fn Servo_ComputedStyle_AddRef(obj: &ComputedValues) { + mem::forget(ArcBorrow::from_ref(obj).clone_arc()); +} + +#[no_mangle] +pub unsafe extern "C" fn Servo_ComputedStyle_Release(obj: &ComputedValues) { + ArcBorrow::from_ref(obj).with_arc(|a: &Arc| { + let _: Arc = ptr::read(a); + }); +} + +impl From> for Strong { + fn from(arc: Arc) -> Self { + unsafe { mem::transmute(Arc::into_raw_offset(arc)) } + } +} diff --git a/servo/components/style/gecko/boxed_types.rs b/servo/components/style/gecko/boxed_types.rs new file mode 100644 index 0000000000..47fc4fe57d --- /dev/null +++ b/servo/components/style/gecko/boxed_types.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/. */ + +//! FFI implementations for types listed in ServoBoxedTypeList.h. + +use crate::gecko_bindings::sugar::ownership::{HasBoxFFI, HasFFI, HasSimpleFFI}; +use crate::properties::animated_properties::AnimationValueMap; +use to_shmem::SharedMemoryBuilder; + +// TODO(heycam): The FFI impls for most of the types in ServoBoxedTypeList.h are spread across +// various files at the moment, but should probably all move here, and use macros to define +// them more succinctly, like we do in arc_types.rs. + +#[cfg(feature = "gecko")] +unsafe impl HasFFI for AnimationValueMap { + type FFIType = crate::gecko_bindings::bindings::RawServoAnimationValueMap; +} +#[cfg(feature = "gecko")] +unsafe impl HasSimpleFFI for AnimationValueMap {} +#[cfg(feature = "gecko")] +unsafe impl HasBoxFFI for AnimationValueMap {} + +#[cfg(feature = "gecko")] +unsafe impl HasFFI for SharedMemoryBuilder { + type FFIType = crate::gecko_bindings::bindings::RawServoSharedMemoryBuilder; +} +#[cfg(feature = "gecko")] +unsafe impl HasSimpleFFI for SharedMemoryBuilder {} +#[cfg(feature = "gecko")] +unsafe impl HasBoxFFI for SharedMemoryBuilder {} 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..0ce43bca46 --- /dev/null +++ b/servo/components/style/gecko/data.rs @@ -0,0 +1,202 @@ +/* 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::context::QuirksMode; +use crate::dom::TElement; +use crate::gecko_bindings::bindings; +use crate::gecko_bindings::structs::{self, RawServoStyleSet, ServoStyleSetSizes}; +use crate::gecko_bindings::structs::{StyleSheet as DomStyleSheet, StyleSheetInfo}; +use crate::gecko_bindings::sugar::ownership::{HasArcFFI, HasBoxFFI, HasFFI, HasSimpleFFI}; +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::{Locked, SharedRwLockReadGuard, StylesheetGuards}; +use crate::stylesheets::{CssRule, Origin, 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); + +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 { + debug_assert!(!s.is_null()); + GeckoStyleSheet(s) + } + + /// 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) } + } + + /// Gets the StylesheetContents for this stylesheet. + pub fn contents(&self) -> &StylesheetContents { + debug_assert!(!self.inner().mContents.mRawPtr.is_null()); + unsafe { + let contents = + (&**StylesheetContents::as_arc(&&*self.inner().mContents.mRawPtr)) as *const _; + &*contents + } + } +} + +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 origin(&self, _guard: &SharedRwLockReadGuard) -> Origin { + self.contents().origin + } + + fn quirks_mode(&self, _guard: &SharedRwLockReadGuard) -> QuirksMode { + self.contents().quirks_mode + } + + fn media<'a>(&'a self, guard: &'a SharedRwLockReadGuard) -> Option<&'a MediaList> { + use crate::gecko_bindings::structs::mozilla::dom::MediaList as DomMediaList; + use std::mem; + + unsafe { + let dom_media_list = self.raw().mMedia.mRawPtr as *const DomMediaList; + if dom_media_list.is_null() { + return None; + } + let raw_list = &*(*dom_media_list).mRawList.mRawPtr; + let list = Locked::::as_arc(mem::transmute(&raw_list)); + Some(list.read_with(guard)) + } + } + + // All the stylesheets Servo knows about are enabled, because that state is + // handled externally by Gecko. + fn enabled(&self) -> bool { + true + } + + #[inline] + fn rules<'a, 'b: 'a>(&'a self, guard: &'b SharedRwLockReadGuard) -> &'a [CssRule] { + self.contents().rules(guard) + } +} + +/// The container for data that a Servo-backed Gecko document needs to style +/// itself. +pub struct PerDocumentStyleDataImpl { + /// Rule processor. + pub stylist: Stylist, +} + +/// 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()), + })) + } + + /// 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() + } + + /// Returns whether visited styles are enabled. + #[inline] + pub fn visited_styles_enabled(&self) -> bool { + unsafe { bindings::Gecko_VisitedStylesEnabled(self.stylist.device().document()) } + } + + /// Measure heap usage. + pub fn add_size_of(&self, ops: &mut MallocSizeOfOps, sizes: &mut ServoStyleSetSizes) { + self.stylist.add_size_of(ops, sizes); + } +} + +unsafe impl HasFFI for PerDocumentStyleData { + type FFIType = RawServoStyleSet; +} +unsafe impl HasSimpleFFI for PerDocumentStyleData {} +unsafe impl HasBoxFFI for PerDocumentStyleData {} diff --git a/servo/components/style/gecko/media_features.rs b/servo/components/style/gecko/media_features.rs new file mode 100644 index 0000000000..9c17c6a799 --- /dev/null +++ b/servo/components/style/gecko/media_features.rs @@ -0,0 +1,856 @@ +/* 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::media_queries::media_feature::{AllowsRanges, ParsingRequirements}; +use crate::media_queries::media_feature::{Evaluator, MediaFeatureDescription}; +use crate::media_queries::media_feature_expression::RangeOrOperator; +use crate::media_queries::{Device, MediaType}; +use crate::values::computed::position::Ratio; +use crate::values::computed::CSSPixelLength; +use crate::values::computed::Resolution; +use crate::Atom; +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( + device: &Device, + value: Option, + range_or_operator: Option, +) -> bool { + RangeOrOperator::evaluate( + range_or_operator, + value.map(Au::from), + device.au_viewport_size().width, + ) +} + +/// https://drafts.csswg.org/mediaqueries-4/#device-width +fn eval_device_width( + device: &Device, + value: Option, + range_or_operator: Option, +) -> bool { + RangeOrOperator::evaluate( + range_or_operator, + value.map(Au::from), + device_size(device).width, + ) +} + +/// https://drafts.csswg.org/mediaqueries-4/#height +fn eval_height( + device: &Device, + value: Option, + range_or_operator: Option, +) -> bool { + RangeOrOperator::evaluate( + range_or_operator, + value.map(Au::from), + device.au_viewport_size().height, + ) +} + +/// https://drafts.csswg.org/mediaqueries-4/#device-height +fn eval_device_height( + device: &Device, + value: Option, + range_or_operator: Option, +) -> bool { + RangeOrOperator::evaluate( + range_or_operator, + value.map(Au::from), + device_size(device).height, + ) +} + +fn eval_aspect_ratio_for( + device: &Device, + query_value: Option, + range_or_operator: Option, + get_size: F, +) -> bool +where + F: FnOnce(&Device) -> Size2D, +{ + // A ratio of 0/0 behaves as the ratio 1/0, so we need to call used_value() + // to convert it if necessary. + // FIXME: we may need to update here once + // https://github.com/w3c/csswg-drafts/issues/4954 got resolved. + let query_value = match query_value { + Some(v) => v.used_value(), + None => return true, + }; + + let size = get_size(device); + let value = Ratio::new(size.width.0 as f32, size.height.0 as f32); + RangeOrOperator::evaluate_with_query_value(range_or_operator, query_value, value) +} + +/// https://drafts.csswg.org/mediaqueries-4/#aspect-ratio +fn eval_aspect_ratio( + device: &Device, + query_value: Option, + range_or_operator: Option, +) -> bool { + eval_aspect_ratio_for( + device, + query_value, + range_or_operator, + Device::au_viewport_size, + ) +} + +/// https://drafts.csswg.org/mediaqueries-4/#device-aspect-ratio +fn eval_device_aspect_ratio( + device: &Device, + query_value: Option, + range_or_operator: Option, +) -> bool { + eval_aspect_ratio_for(device, query_value, range_or_operator, device_size) +} + +/// https://compat.spec.whatwg.org/#css-media-queries-webkit-device-pixel-ratio +fn eval_device_pixel_ratio( + device: &Device, + query_value: Option, + range_or_operator: Option, +) -> bool { + eval_resolution( + device, + query_value.map(Resolution::from_dppx), + range_or_operator, + ) +} + +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] +#[repr(u8)] +enum Orientation { + Landscape, + Portrait, +} + +fn eval_orientation_for(device: &Device, value: Option, get_size: F) -> bool +where + F: FnOnce(&Device) -> Size2D, +{ + let query_orientation = match value { + Some(v) => v, + None => return true, + }; + + let size = get_size(device); + + // Per spec, square viewports should be 'portrait' + let is_landscape = size.width > size.height; + match query_orientation { + Orientation::Landscape => is_landscape, + Orientation::Portrait => !is_landscape, + } +} + +/// https://drafts.csswg.org/mediaqueries-4/#orientation +fn eval_orientation(device: &Device, value: Option) -> bool { + eval_orientation_for(device, value, Device::au_viewport_size) +} + +/// FIXME: There's no spec for `-moz-device-orientation`. +fn eval_device_orientation(device: &Device, value: Option) -> bool { + eval_orientation_for(device, value, device_size) +} + +/// 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(device: &Device, query_value: Option) -> bool { + match query_value { + Some(v) => v == unsafe { bindings::Gecko_MediaFeatures_GetDisplayMode(device.document()) }, + None => true, + } +} + +/// https://drafts.csswg.org/mediaqueries-4/#grid +fn eval_grid(_: &Device, query_value: Option, _: Option) -> bool { + // Gecko doesn't support grid devices (e.g., ttys), so the 'grid' feature + // is always 0. + let supports_grid = false; + query_value.map_or(supports_grid, |v| v == supports_grid) +} + +/// https://compat.spec.whatwg.org/#css-media-queries-webkit-transform-3d +fn eval_transform_3d(_: &Device, query_value: Option, _: Option) -> bool { + let supports_transforms = true; + query_value.map_or(supports_transforms, |v| v == supports_transforms) +} + +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] +#[repr(u8)] +enum Scan { + Progressive, + Interlace, +} + +/// https://drafts.csswg.org/mediaqueries-4/#scan +fn eval_scan(_: &Device, _: 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( + device: &Device, + query_value: Option, + range_or_operator: Option, +) -> bool { + let color_bits_per_channel = + unsafe { bindings::Gecko_MediaFeatures_GetColorDepth(device.document()) }; + RangeOrOperator::evaluate(range_or_operator, query_value, color_bits_per_channel) +} + +/// https://drafts.csswg.org/mediaqueries-4/#color-index +fn eval_color_index( + _: &Device, + query_value: Option, + range_or_operator: Option, +) -> bool { + // We should return zero if the device does not use a color lookup table. + let index = 0; + RangeOrOperator::evaluate(range_or_operator, query_value, index) +} + +/// https://drafts.csswg.org/mediaqueries-4/#monochrome +fn eval_monochrome( + device: &Device, + query_value: Option, + range_or_operator: Option, +) -> bool { + // For color devices we should return 0. + let depth = + unsafe { bindings::Gecko_MediaFeatures_GetMonochromeBitsPerPixel(device.document()) }; + RangeOrOperator::evaluate(range_or_operator, query_value, depth) +} + +/// https://drafts.csswg.org/mediaqueries-4/#resolution +fn eval_resolution( + device: &Device, + query_value: Option, + range_or_operator: Option, +) -> bool { + let resolution_dppx = unsafe { bindings::Gecko_MediaFeatures_GetResolution(device.document()) }; + RangeOrOperator::evaluate( + range_or_operator, + query_value.map(|r| r.dppx()), + resolution_dppx, + ) +} + +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] +#[repr(u8)] +enum PrefersReducedMotion { + NoPreference, + Reduce, +} + +fn color_scheme_no_preference_enabled(_: &crate::parser::ParserContext) -> bool { + static_prefs::pref!("layout.css.prefers-color-scheme-no-preference.enabled") +} + +/// 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, + #[parse(condition = "color_scheme_no_preference_enabled")] + NoPreference, +} + +/// https://drafts.csswg.org/mediaqueries-5/#prefers-reduced-motion +fn eval_prefers_reduced_motion(device: &Device, query_value: Option) -> bool { + let prefers_reduced = + unsafe { bindings::Gecko_MediaFeatures_PrefersReducedMotion(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, + } +} + +/// Possible values for prefers-contrast media query. +/// https://drafts.csswg.org/mediaqueries-5/#prefers-contrast +#[derive(Clone, Copy, Debug, FromPrimitive, PartialEq, Parse, ToCss)] +#[repr(u8)] +#[allow(missing_docs)] +enum PrefersContrast { + More, + Less, + NoPreference, + Forced, +} + +/// Represents the parts of prefers-contrast that explicitly deal with +/// contrast. Used in combination with information about rather or not +/// forced colors are active this allows for evaluation of the +/// prefers-contrast media query. +#[derive(Clone, Copy, Debug, FromPrimitive, PartialEq)] +#[repr(u8)] +pub enum ContrastPref { + /// More contrast is prefered. Corresponds to an accessibility theme + /// being enabled or firefox forcing high contrast colors. + More, + /// Low contrast is prefered. + Less, + /// The default value if neither high or low contrast is enabled. + NoPreference, +} + +/// https://drafts.csswg.org/mediaqueries-5/#prefers-contrast +fn eval_prefers_contrast(device: &Device, query_value: Option) -> bool { + let forced_colors = !device.use_document_colors(); + let contrast_pref = + unsafe { bindings::Gecko_MediaFeatures_PrefersContrast(device.document(), forced_colors) }; + if let Some(query_value) = query_value { + match query_value { + PrefersContrast::Forced => forced_colors, + PrefersContrast::More => contrast_pref == ContrastPref::More, + PrefersContrast::Less => contrast_pref == ContrastPref::Less, + PrefersContrast::NoPreference => contrast_pref == ContrastPref::NoPreference, + } + } else { + // Only prefers-contrast: no-preference evaluates to false. + forced_colors || (contrast_pref != ContrastPref::NoPreference) + } +} + +/// Possible values for the forced-colors media query. +/// https://drafts.csswg.org/mediaqueries-5/#forced-colors +#[derive(Clone, Copy, Debug, FromPrimitive, PartialEq, Parse, 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(device: &Device, query_value: Option) -> bool { + let forced = !device.use_document_colors(); + match query_value { + Some(query_value) => forced == (query_value == ForcedColors::Active), + None => forced, + } +} + +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] +#[repr(u8)] +enum OverflowBlock { + None, + Scroll, + OptionalPaged, + Paged, +} + +/// https://drafts.csswg.org/mediaqueries-4/#mf-overflow-block +fn eval_overflow_block(device: &Device, 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 = device.media_type() != MediaType::print(); + let query_value = match query_value { + Some(v) => v, + None => return true, + }; + + match query_value { + OverflowBlock::None | OverflowBlock::OptionalPaged => 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(device: &Device, query_value: Option) -> bool { + // See the note in eval_overflow_block. + let scrolling = 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, + } +} + +/// https://drafts.csswg.org/mediaqueries-5/#prefers-color-scheme +fn eval_prefers_color_scheme(device: &Device, query_value: Option) -> bool { + let prefers_color_scheme = + unsafe { bindings::Gecko_MediaFeatures_PrefersColorScheme(device.document()) }; + match query_value { + Some(v) => prefers_color_scheme == v, + None => prefers_color_scheme != PrefersColorScheme::NoPreference, + } +} + +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(device: &Device) -> PointerCapabilities { + PointerCapabilities::from_bits_truncate(unsafe { + bindings::Gecko_MediaFeatures_PrimaryPointerCapabilities(device.document()) + }) +} + +fn all_pointer_capabilities(device: &Device) -> PointerCapabilities { + PointerCapabilities::from_bits_truncate(unsafe { + bindings::Gecko_MediaFeatures_AllPointerCapabilities(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(device: &Device, query_value: Option) -> bool { + eval_pointer_capabilities(query_value, primary_pointer_capabilities(device)) +} + +/// https://drafts.csswg.org/mediaqueries-4/#descdef-media-any-pointer +fn eval_any_pointer(device: &Device, query_value: Option) -> bool { + eval_pointer_capabilities(query_value, all_pointer_capabilities(device)) +} + +#[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(device: &Device, query_value: Option) -> bool { + eval_hover_capabilities(query_value, primary_pointer_capabilities(device)) +} + +/// https://drafts.csswg.org/mediaqueries-4/#descdef-media-any-hover +fn eval_any_hover(device: &Device, query_value: Option) -> bool { + eval_hover_capabilities(query_value, all_pointer_capabilities(device)) +} + +fn eval_moz_is_glyph( + device: &Device, + query_value: Option, + _: Option, +) -> bool { + let is_glyph = device.document().mIsSVGGlyphsDocument(); + query_value.map_or(is_glyph, |v| v == is_glyph) +} + +fn eval_moz_print_preview( + device: &Device, + query_value: Option, + _: Option, +) -> bool { + let is_print_preview = device.is_print_preview(); + if is_print_preview { + debug_assert_eq!(device.media_type(), MediaType::print()); + } + query_value.map_or(is_print_preview, |v| v == is_print_preview) +} + +fn eval_moz_non_native_content_theme( + device: &Device, + query_value: Option, + _: Option, +) -> bool { + let non_native_theme = + unsafe { bindings::Gecko_MediaFeatures_ShouldAvoidNativeTheme(device.document()) }; + query_value.map_or(non_native_theme, |v| v == non_native_theme) +} + +fn eval_moz_is_resource_document( + device: &Device, + query_value: Option, + _: Option, +) -> bool { + let is_resource_doc = + unsafe { bindings::Gecko_MediaFeatures_IsResourceDocument(device.document()) }; + query_value.map_or(is_resource_doc, |v| v == is_resource_doc) +} + +fn eval_system_metric( + device: &Device, + query_value: Option, + metric: Atom, + accessible_from_content: bool, +) -> bool { + let supports_metric = unsafe { + bindings::Gecko_MediaFeatures_HasSystemMetric( + device.document(), + metric.as_ptr(), + accessible_from_content, + ) + }; + query_value.map_or(supports_metric, |v| v == supports_metric) +} + +fn eval_moz_os_version( + device: &Device, + query_value: Option, + _: Option, +) -> bool { + let query_value = match query_value { + Some(v) => v, + None => return false, + }; + + let os_version = + unsafe { bindings::Gecko_MediaFeatures_GetOperatingSystemVersion(device.document()) }; + + query_value.as_ptr() == os_version +} + +macro_rules! system_metric_feature { + ($feature_name:expr) => {{ + fn __eval(device: &Device, query_value: Option, _: Option) -> bool { + eval_system_metric( + device, + query_value, + $feature_name, + /* accessible_from_content = */ false, + ) + } + + feature!( + $feature_name, + AllowsRanges::No, + Evaluator::BoolInteger(__eval), + ParsingRequirements::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: [MediaFeatureDescription; 56] = [ + feature!( + atom!("width"), + AllowsRanges::Yes, + Evaluator::Length(eval_width), + ParsingRequirements::empty(), + ), + feature!( + atom!("height"), + AllowsRanges::Yes, + Evaluator::Length(eval_height), + ParsingRequirements::empty(), + ), + feature!( + atom!("aspect-ratio"), + AllowsRanges::Yes, + Evaluator::NumberRatio(eval_aspect_ratio), + ParsingRequirements::empty(), + ), + feature!( + atom!("orientation"), + AllowsRanges::No, + keyword_evaluator!(eval_orientation, Orientation), + ParsingRequirements::empty(), + ), + feature!( + atom!("device-width"), + AllowsRanges::Yes, + Evaluator::Length(eval_device_width), + ParsingRequirements::empty(), + ), + feature!( + atom!("device-height"), + AllowsRanges::Yes, + Evaluator::Length(eval_device_height), + ParsingRequirements::empty(), + ), + feature!( + atom!("device-aspect-ratio"), + AllowsRanges::Yes, + Evaluator::NumberRatio(eval_device_aspect_ratio), + ParsingRequirements::empty(), + ), + feature!( + atom!("-moz-device-orientation"), + AllowsRanges::No, + keyword_evaluator!(eval_device_orientation, Orientation), + ParsingRequirements::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), + ParsingRequirements::WEBKIT_PREFIX, + ), + // -webkit-transform-3d. + feature!( + atom!("transform-3d"), + AllowsRanges::No, + Evaluator::BoolInteger(eval_transform_3d), + ParsingRequirements::WEBKIT_PREFIX, + ), + feature!( + atom!("-moz-device-pixel-ratio"), + AllowsRanges::Yes, + Evaluator::Float(eval_device_pixel_ratio), + ParsingRequirements::empty(), + ), + feature!( + atom!("resolution"), + AllowsRanges::Yes, + Evaluator::Resolution(eval_resolution), + ParsingRequirements::empty(), + ), + feature!( + atom!("display-mode"), + AllowsRanges::No, + keyword_evaluator!(eval_display_mode, DisplayMode), + ParsingRequirements::empty(), + ), + feature!( + atom!("grid"), + AllowsRanges::No, + Evaluator::BoolInteger(eval_grid), + ParsingRequirements::empty(), + ), + feature!( + atom!("scan"), + AllowsRanges::No, + keyword_evaluator!(eval_scan, Scan), + ParsingRequirements::empty(), + ), + feature!( + atom!("color"), + AllowsRanges::Yes, + Evaluator::Integer(eval_color), + ParsingRequirements::empty(), + ), + feature!( + atom!("color-index"), + AllowsRanges::Yes, + Evaluator::Integer(eval_color_index), + ParsingRequirements::empty(), + ), + feature!( + atom!("monochrome"), + AllowsRanges::Yes, + Evaluator::Integer(eval_monochrome), + ParsingRequirements::empty(), + ), + feature!( + atom!("prefers-reduced-motion"), + AllowsRanges::No, + keyword_evaluator!(eval_prefers_reduced_motion, PrefersReducedMotion), + ParsingRequirements::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. + ParsingRequirements::empty(), + ), + feature!( + atom!("forced-colors"), + AllowsRanges::No, + keyword_evaluator!(eval_forced_colors, ForcedColors), + ParsingRequirements::empty(), + ), + feature!( + atom!("overflow-block"), + AllowsRanges::No, + keyword_evaluator!(eval_overflow_block, OverflowBlock), + ParsingRequirements::empty(), + ), + feature!( + atom!("overflow-inline"), + AllowsRanges::No, + keyword_evaluator!(eval_overflow_inline, OverflowInline), + ParsingRequirements::empty(), + ), + feature!( + atom!("prefers-color-scheme"), + AllowsRanges::No, + keyword_evaluator!(eval_prefers_color_scheme, PrefersColorScheme), + ParsingRequirements::empty(), + ), + feature!( + atom!("pointer"), + AllowsRanges::No, + keyword_evaluator!(eval_pointer, Pointer), + ParsingRequirements::empty(), + ), + feature!( + atom!("any-pointer"), + AllowsRanges::No, + keyword_evaluator!(eval_any_pointer, Pointer), + ParsingRequirements::empty(), + ), + feature!( + atom!("hover"), + AllowsRanges::No, + keyword_evaluator!(eval_hover, Hover), + ParsingRequirements::empty(), + ), + feature!( + atom!("any-hover"), + AllowsRanges::No, + keyword_evaluator!(eval_any_hover, Hover), + ParsingRequirements::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), + ParsingRequirements::CHROME_AND_UA_ONLY, + ), + feature!( + atom!("-moz-is-resource-document"), + AllowsRanges::No, + Evaluator::BoolInteger(eval_moz_is_resource_document), + ParsingRequirements::CHROME_AND_UA_ONLY, + ), + feature!( + atom!("-moz-os-version"), + AllowsRanges::No, + Evaluator::Ident(eval_moz_os_version), + ParsingRequirements::CHROME_AND_UA_ONLY, + ), + feature!( + atom!("-moz-print-preview"), + AllowsRanges::No, + Evaluator::BoolInteger(eval_moz_print_preview), + ParsingRequirements::CHROME_AND_UA_ONLY, + ), + feature!( + atom!("-moz-non-native-content-theme"), + AllowsRanges::No, + Evaluator::BoolInteger(eval_moz_non_native_content_theme), + ParsingRequirements::CHROME_AND_UA_ONLY, + ), + system_metric_feature!(atom!("-moz-scrollbar-start-backward")), + system_metric_feature!(atom!("-moz-scrollbar-start-forward")), + system_metric_feature!(atom!("-moz-scrollbar-end-backward")), + system_metric_feature!(atom!("-moz-scrollbar-end-forward")), + system_metric_feature!(atom!("-moz-scrollbar-thumb-proportional")), + system_metric_feature!(atom!("-moz-overlay-scrollbars")), + system_metric_feature!(atom!("-moz-windows-default-theme")), + system_metric_feature!(atom!("-moz-mac-graphite-theme")), + system_metric_feature!(atom!("-moz-mac-big-sur-theme")), + system_metric_feature!(atom!("-moz-windows-accent-color-in-titlebar")), + system_metric_feature!(atom!("-moz-windows-compositor")), + system_metric_feature!(atom!("-moz-windows-classic")), + system_metric_feature!(atom!("-moz-windows-glass")), + system_metric_feature!(atom!("-moz-menubar-drag")), + system_metric_feature!(atom!("-moz-swipe-animation-enabled")), + system_metric_feature!(atom!("-moz-gtk-csd-available")), + system_metric_feature!(atom!("-moz-gtk-csd-hide-titlebar-by-default")), + system_metric_feature!(atom!("-moz-gtk-csd-transparent-background")), + system_metric_feature!(atom!("-moz-gtk-csd-minimize-button")), + system_metric_feature!(atom!("-moz-gtk-csd-maximize-button")), + system_metric_feature!(atom!("-moz-gtk-csd-close-button")), + system_metric_feature!(atom!("-moz-gtk-csd-reversed-placement")), + system_metric_feature!(atom!("-moz-system-dark-theme")), +]; diff --git a/servo/components/style/gecko/media_queries.rs b/servo/components/style/gecko/media_queries.rs new file mode 100644 index 0000000000..a116121a25 --- /dev/null +++ b/servo/components/style/gecko/media_queries.rs @@ -0,0 +1,363 @@ +/* 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::context::QuirksMode; +use crate::custom_properties::CssEnvironment; +use crate::gecko::values::{convert_nscolor_to_rgba, convert_rgba_to_nscolor}; +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::Length; +use crate::values::specified::font::FONT_MEDIUM_PX; +use crate::values::{CustomIdent, KeyframesName}; +use app_units::{Au, AU_PER_PX}; +use cssparser::RGBA; +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 the viewport size + /// by using vw/vh/vmin/vmax units. + used_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()), + body_text_color: AtomicUsize::new(prefs.mDefaultColor as usize), + used_root_font_size: AtomicBool::new(false), + used_viewport_size: AtomicBool::new(false), + environment: CssEnvironment, + } + } + + /// Get the relevant environment to resolve `env()` functions. + #[inline] + pub fn environment(&self) -> &CssEnvironment { + &self.environment + } + + /// 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: RGBA) { + self.body_text_color + .store(convert_rgba_to_nscolor(&color) as usize, Ordering::Relaxed) + } + + /// Returns the body text color. + pub fn body_text_color(&self) -> RGBA { + convert_nscolor_to_rgba(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_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) -> 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); + } + + let size = &pc.mSizeForViewportUnits; + Size2D::new(Au(size.width), Au(size.height)) + } + + /// 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 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 + } + + /// Returns the default background color. + pub fn default_background_color(&self) -> RGBA { + convert_nscolor_to_rgba(self.pref_sheet_prefs().mDefaultBackgroundColor) + } + + /// Returns the default foreground color. + pub fn default_color(&self) -> RGBA { + convert_nscolor_to_rgba(self.pref_sheet_prefs().mDefaultColor) + } + + /// Returns the current effective text zoom. + #[inline] + fn effective_text_zoom(&self) -> f32 { + let pc = match self.pres_context() { + Some(pc) => pc, + None => return 1., + }; + pc.mEffectiveTextZoom + } + + /// 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.effective_text_zoom()) + } + + /// Un-apply text zoom. + #[inline] + pub fn unzoom_text(&self, size: Length) -> Length { + size.scale_by(1. / self.effective_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) + } +} diff --git a/servo/components/style/gecko/mod.rs b/servo/components/style/gecko/mod.rs new file mode 100644 index 0000000000..3ff2cfcf14 --- /dev/null +++ b/servo/components/style/gecko/mod.rs @@ -0,0 +1,26 @@ +/* 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 boxed_types; +pub mod conversions; +pub mod data; +pub mod media_features; +pub mod media_queries; +#[cfg(feature = "gecko_profiler")] +pub mod profiler; +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..7bffe282a7 --- /dev/null +++ b/servo/components/style/gecko/non_ts_pseudo_class_list.rs @@ -0,0 +1,104 @@ +/* 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, IN_UNVISITED_STATE, _), + ("any-link", AnyLink, IN_VISITED_OR_UNVISITED_STATE, _), + ("visited", Visited, IN_VISITED_STATE, _), + ("active", Active, IN_ACTIVE_STATE, _), + ("autofill", Autofill, IN_AUTOFILL_STATE, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS_AND_CHROME), + ("checked", Checked, IN_CHECKED_STATE, _), + ("defined", Defined, IN_DEFINED_STATE, _), + ("disabled", Disabled, IN_DISABLED_STATE, _), + ("enabled", Enabled, IN_ENABLED_STATE, _), + ("focus", Focus, IN_FOCUS_STATE, _), + ("focus-within", FocusWithin, IN_FOCUS_WITHIN_STATE, _), + ("focus-visible", FocusVisible, IN_FOCUSRING_STATE, _), + ("hover", Hover, IN_HOVER_STATE, _), + ("-moz-drag-over", MozDragOver, IN_DRAGOVER_STATE, _), + ("target", Target, IN_TARGET_STATE, _), + ("indeterminate", Indeterminate, IN_INDETERMINATE_STATE, _), + ("-moz-inert", MozInert, IN_MOZINERT_STATE, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), + ("-moz-devtools-highlighted", MozDevtoolsHighlighted, IN_DEVTOOLS_HIGHLIGHTED_STATE, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), + ("-moz-styleeditor-transitioning", MozStyleeditorTransitioning, IN_STYLEEDITOR_TRANSITIONING_STATE, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), + ("fullscreen", Fullscreen, IN_FULLSCREEN_STATE, _), + ("-moz-modal-dialog", MozModalDialog, IN_MODAL_DIALOG_STATE, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), + ("-moz-topmost-modal-dialog", MozTopmostModalDialog, IN_TOPMOST_MODAL_DIALOG_STATE, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), + // TODO(emilio): This is inconsistently named (the capital R). + ("-moz-focusring", MozFocusRing, IN_FOCUSRING_STATE, _), + ("-moz-broken", MozBroken, IN_BROKEN_STATE, _), + ("-moz-loading", MozLoading, IN_LOADING_STATE, _), + ("-moz-has-dir-attr", MozHasDirAttr, IN_HAS_DIR_ATTR_STATE, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), + ("-moz-dir-attr-ltr", MozDirAttrLTR, IN_HAS_DIR_ATTR_LTR_STATE, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), + ("-moz-dir-attr-rtl", MozDirAttrRTL, IN_HAS_DIR_ATTR_RTL_STATE, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), + ("-moz-dir-attr-like-auto", MozDirAttrLikeAuto, IN_HAS_DIR_ATTR_LIKE_AUTO_STATE, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), + + ("-moz-autofill-preview", MozAutofillPreview, IN_AUTOFILL_PREVIEW_STATE, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS_AND_CHROME), + + ("-moz-handler-noplugins", MozHandlerNoPlugins, IN_HANDLER_NOPLUGINS, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS_AND_CHROME), + + ("-moz-math-increment-script-level", MozMathIncrementScriptLevel, IN_INCREMENT_SCRIPT_LEVEL_STATE, _), + + ("required", Required, IN_REQUIRED_STATE, _), + ("optional", Optional, IN_OPTIONAL_STATE, _), + ("valid", Valid, IN_VALID_STATE, _), + ("invalid", Invalid, IN_INVALID_STATE, _), + ("in-range", InRange, IN_INRANGE_STATE, _), + ("out-of-range", OutOfRange, IN_OUTOFRANGE_STATE, _), + ("default", Default, IN_DEFAULT_STATE, _), + ("placeholder-shown", PlaceholderShown, IN_PLACEHOLDER_SHOWN_STATE, _), + ("read-only", ReadOnly, IN_READONLY_STATE, _), + ("read-write", ReadWrite, IN_READWRITE_STATE, _), + ("-moz-submit-invalid", MozSubmitInvalid, IN_MOZ_SUBMITINVALID_STATE, _), + ("-moz-ui-valid", MozUIValid, IN_MOZ_UI_VALID_STATE, _), + ("-moz-ui-invalid", MozUIInvalid, IN_MOZ_UI_INVALID_STATE, _), + ("-moz-meter-optimum", MozMeterOptimum, IN_OPTIMUM_STATE, _), + ("-moz-meter-sub-optimum", MozMeterSubOptimum, IN_SUB_OPTIMUM_STATE, _), + ("-moz-meter-sub-sub-optimum", MozMeterSubSubOptimum, IN_SUB_SUB_OPTIMUM_STATE, _), + + ("-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, _, _), + ("-moz-lwtheme-brighttext", MozLWThemeBrightText, _, _), + ("-moz-lwtheme-darktext", MozLWThemeDarkText, _, _), + ("-moz-window-inactive", MozWindowInactive, _, _), + ] + } + } +} diff --git a/servo/components/style/gecko/profiler.rs b/servo/components/style/gecko/profiler.rs new file mode 100644 index 0000000000..db269b497d --- /dev/null +++ b/servo/components/style/gecko/profiler.rs @@ -0,0 +1,75 @@ +/* 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 profiler support. +//! +//! Use the `profiler_label!` macro from macros.rs. + +use crate::gecko_bindings::structs; + +/// A label describing a category of work that style threads can perform. +pub enum ProfilerLabel { + /// Style computation. + Style, + /// Style sheet parsing. + Parse, +} + +/// RAII object that constructs and destroys a C++ AutoProfilerLabel object +/// pointed to be the specified reference. +#[cfg(feature = "gecko_profiler")] +pub struct AutoProfilerLabel<'a>(&'a mut structs::AutoProfilerLabel); + +#[cfg(feature = "gecko_profiler")] +impl<'a> AutoProfilerLabel<'a> { + /// Creates a new AutoProfilerLabel with the specified label type. + /// + /// unsafe since the caller must ensure that `label` is allocated on the + /// stack. + #[inline] + pub unsafe fn new( + label: &mut std::mem::MaybeUninit, + label_type: ProfilerLabel, + ) -> AutoProfilerLabel { + let category_pair = match label_type { + ProfilerLabel::Style => structs::JS::ProfilingCategoryPair_LAYOUT_StyleComputation, + ProfilerLabel::Parse => structs::JS::ProfilingCategoryPair_LAYOUT_CSSParsing, + }; + structs::Gecko_Construct_AutoProfilerLabel(label.as_mut_ptr(), category_pair); + AutoProfilerLabel(&mut *label.as_mut_ptr()) + } +} + +#[cfg(feature = "gecko_profiler")] +impl<'a> Drop for AutoProfilerLabel<'a> { + #[inline] + fn drop(&mut self) { + unsafe { + structs::Gecko_Destroy_AutoProfilerLabel(self.0); + } + } +} + +/// Whether the Gecko profiler is currently active. +/// +/// This implementation must be kept in sync with +/// `mozilla::profiler::detail::RacyFeatures::IsActive`. +#[cfg(feature = "gecko_profiler")] +#[inline] +pub fn profiler_is_active() -> bool { + use self::structs::profiler::detail; + use std::mem; + use std::sync::atomic::{AtomicU32, Ordering}; + + let active_and_features: &AtomicU32 = + unsafe { mem::transmute(&detail::RacyFeatures_sActiveAndFeatures) }; + (active_and_features.load(Ordering::Relaxed) & detail::RacyFeatures_Active) != 0 +} + +/// Always false when the Gecko profiler is disabled. +#[cfg(not(feature = "gecko_profiler"))] +#[inline] +pub fn profiler_is_active() -> bool { + false +} diff --git a/servo/components/style/gecko/pseudo_element.rs b/servo/components/style/gecko/pseudo_element.rs new file mode 100644 index 0000000000..5a9b955b1c --- /dev/null +++ b/servo/components/style/gecko/pseudo_element.rs @@ -0,0 +1,219 @@ +/* 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 cssparser::ToCss; +use std::fmt; +use thin_slice::ThinBoxedSlice; + +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 the current pseudo element is animatable. + #[inline] + pub fn is_animatable(&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() + } + + /// 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 { + 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..41dff1a946 --- /dev/null +++ b/servo/components/style/gecko/pseudo_element_definition.mako.rs @@ -0,0 +1,264 @@ +/* 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. +#[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()}(ThinBoxedSlice), + % 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 not pseudo.is_tree_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 pseudo.is_tree_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 not pseudo.is_tree_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, + % 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) -> 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, Box::new([])) + } + const WEBKIT_PREFIX: &str = "-webkit-"; + if 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: Box<[Atom]>) -> 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.into()")}); + } + % 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 PSEUDOS: + ${pseudo_element_variant(pseudo)} => dest.write_str("${pseudo.value}")?, + % endfor + 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..f05ddb8d64 --- /dev/null +++ b/servo/components/style/gecko/regen_atoms.py @@ -0,0 +1,215 @@ +#!/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 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..b619f58af6 --- /dev/null +++ b/servo/components/style/gecko/selector_parser.rs @@ -0,0 +1,453 @@ +/* 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::element_state::{DocumentState, ElementState}; +use crate::gecko_bindings::structs::RawServoSelectorList; +use crate::gecko_bindings::sugar::ownership::{HasBoxFFI, HasFFI, HasSimpleFFI}; +use crate::invalidation::element::document_state::InvalidationMatchingData; +use crate::selector_parser::{Direction, 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 selectors::parser::{ParseErrorRecovery, SelectorParseErrorKind}; +use selectors::SelectorList; +use std::fmt; +use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss as ToCss_}; + +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() + 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. +pub type Lang = AtomIdent; + +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 s) => { + dest.write_str(":lang(")?; + s.to_css(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), + "-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(_) | + 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 let NonTSPseudoClass::FocusVisible = *self { + return static_prefs::pref!("layout.css.focus-visible.enabled"); + } + if let NonTSPseudoClass::Autofill = *self { + return static_prefs::pref!("layout.css.autofill.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(..) | + 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(..) => DocumentState::NS_DOCUMENT_STATE_RTL_LOCALE, + NonTSPseudoClass::MozWindowInactive => DocumentState::NS_DOCUMENT_STATE_WINDOW_INACTIVE, + _ => 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 doesn't use state_flag + // because its semantics don't quite match. Nevertheless, it + // doesn't need cache revalidation, because we already compare + // states for elements and candidates. + 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-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::MozLocaleDir(_) | + NonTSPseudoClass::MozWindowInactive | + // Similar for the document themes. + NonTSPseudoClass::MozLWTheme | + NonTSPseudoClass::MozLWThemeBrightText | + NonTSPseudoClass::MozLWThemeDarkText + ) + } +} + +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; + +impl ::selectors::SelectorImpl for SelectorImpl { + type ExtraMatchingData = InvalidationMatchingData; + 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; +} + +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>; + + #[inline] + fn parse_slotted(&self) -> bool { + true + } + + #[inline] + fn parse_host(&self) -> bool { + true + } + + #[inline] + fn parse_is_and_where(&self) -> bool { + true + } + + #[inline] + fn is_and_where_error_recovery(&self) -> ParseErrorRecovery { + if static_prefs::pref!("layout.css.is-and-where-better-error-recovery.enabled") { + ParseErrorRecovery::IgnoreInvalidSelector + } else { + ParseErrorRecovery::DiscardList + } + } + + #[inline] + fn parse_part(&self) -> bool { + true + } + + #[inline] + fn is_is_alias(&self, function: &str) -> bool { + function.eq_ignore_ascii_case("-moz-any") + } + + 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 name = parser.expect_ident_or_string()?; + NonTSPseudoClass::Lang(Lang::from(name.as_ref())) + }, + "-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> { + if let Some(pseudo) = PseudoElement::from_slice(&name) { + 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 = Vec::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"), + } + } + let args = args.into_boxed_slice(); + if let Some(pseudo) = PseudoElement::tree_pseudo_element(&name, args) { + 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()) + } + } +} + +unsafe impl HasFFI for SelectorList { + type FFIType = RawServoSelectorList; +} +unsafe impl HasSimpleFFI for SelectorList {} +unsafe impl HasBoxFFI for SelectorList {} diff --git a/servo/components/style/gecko/snapshot.rs b/servo/components/style/gecko/snapshot.rs new file mode 100644 index 0000000000..8ff794a4d8 --- /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::element_state::ElementState; +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 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_truncate(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..8d99f84a95 --- /dev/null +++ b/servo/components/style/gecko/snapshot_helpers.rs @@ -0,0 +1,167 @@ +/* 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::gecko_bindings::bindings; +use crate::gecko_bindings::structs::{self, nsAtom}; +use crate::string_cache::WeakAtom; +use crate::values::AtomIdent; +use crate::Atom; +use crate::CaseSensitivityExt; +use selectors::attr::CaseSensitivity; + +/// 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 array: *mut u8 = *(*container) + .__bindgen_anon_1 + .mValue + .as_ref() + .__bindgen_anon_1 + .mAtomArray + .as_ref(); + let array = array 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) + } + }, + } + } +} 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..31e38e444a --- /dev/null +++ b/servo/components/style/gecko/url.rs @@ -0,0 +1,381 @@ +/* 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. +#[css(function = "url")] +#[derive(Clone, Debug, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +#[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..a755556e85 --- /dev/null +++ b/servo/components/style/gecko/values.rs @@ -0,0 +1,85 @@ +/* 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::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; +use app_units::Au; +use cssparser::RGBA; +use std::cmp::max; + +/// Convert a given RGBA value to `nscolor`. +pub fn convert_rgba_to_nscolor(rgba: &RGBA) -> u32 { + ((rgba.alpha as u32) << 24) | + ((rgba.blue as u32) << 16) | + ((rgba.green as u32) << 8) | + (rgba.red as u32) +} + +/// Convert a given `nscolor` to a Servo RGBA value. +pub fn convert_nscolor_to_rgba(color: u32) -> RGBA { + RGBA::new( + (color & 0xff) as u8, + (color >> 8 & 0xff) as u8, + (color >> 16 & 0xff) as u8, + (color >> 24 & 0xff) as u8, + ) +} + +/// Round `width` down to the nearest device pixel, but any non-zero value that +/// would round down to zero is clamped to 1 device pixel. Used for storing +/// computed values of border-*-width and outline-width. +#[inline] +pub fn round_border_to_device_pixels(width: Au, au_per_device_px: Au) -> Au { + if width == Au(0) { + Au(0) + } else { + max( + au_per_device_px, + Au(width.0 / au_per_device_px.0 * au_per_device_px.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..ce873ed45f --- /dev/null +++ b/servo/components/style/gecko/wrapper.rs @@ -0,0 +1,2242 @@ +/* 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::author_styles::AuthorStyles; +use crate::context::{PostAnimationTasks, QuirksMode, SharedStyleContext, UpdateAnimationsTasks}; +use crate::data::ElementData; +use crate::dom::{LayoutIterator, NodeInfo, OpaqueNode, TDocument, TElement, TNode, TShadowRoot}; +use crate::element_state::{DocumentState, ElementState}; +use crate::font_metrics::{FontMetrics, FontMetricsOrientation, FontMetricsProvider}; +use crate::gecko::data::GeckoStyleSheet; +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_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::bindings::{Gecko_ElementState, Gecko_GetDocumentLWTheme}; +use crate::gecko_bindings::bindings::{Gecko_SetNodeFlags, Gecko_UnsetNodeFlags}; +use crate::gecko_bindings::structs; +use crate::gecko_bindings::structs::nsChangeHint; +use crate::gecko_bindings::structs::Document_DocumentTheme as DocumentTheme; +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::gecko_bindings::sugar::ownership::{HasArcFFI, HasSimpleFFI}; +use crate::global_style_data::GLOBAL_STYLE_DATA; +use crate::hash::FxHashMap; +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, HorizontalDirection, Lang}; +use crate::shared_lock::{Locked, SharedRwLock}; +use crate::string_cache::{Atom, Namespace, WeakAtom, WeakNamespace}; +use crate::stylist::CascadeData; +use crate::values::computed::font::GenericFontFamily; +use crate::values::computed::Length; +use crate::values::specified::length::FontBaseSize; +use crate::values::{AtomIdent, AtomString}; +use crate::CaseSensitivityExt; +use crate::LocalName; +use atomic_refcell::{AtomicRef, AtomicRefCell, AtomicRefMut}; +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, RawOffsetArc}; +use std::cell::RefCell; +use std::fmt; +use std::hash::{Hash, Hasher}; +use std::mem; +use std::ptr; + +#[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()? }; + let author_styles = AuthorStyles::::from_ffi(author_styles); + 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 flags(&self) -> u32 { + (self.0)._base._base_1.mFlags + } + + #[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 as u32) != 0 + } + + /// WARNING: This logic is duplicated in Gecko's FlattenedTreeParentIsParent. + /// Make sure to mirror any modifications in both places. + #[inline] + fn flattened_tree_parent_is_parent(&self) -> bool { + use crate::gecko_bindings::structs::*; + let flags = self.flags(); + + // FIXME(emilio): The shadow tree condition seems it shouldn't be needed + // anymore, if we check for slots. + if self.is_in_shadow_tree() { + return false; + } + + let parent = unsafe { self.0.mParent.as_ref() }.map(GeckoNode); + let parent_el = parent.and_then(|p| p.as_element()); + if flags & (NODE_IS_NATIVE_ANONYMOUS_ROOT as u32) != 0 && + parent_el.map_or(false, |el| el.is_root()) + { + return false; + } + + if let Some(parent) = parent_el { + if parent.shadow_root().is_some() { + return false; + } + } + + true + } + + #[inline] + fn flattened_tree_parent(&self) -> Option { + // TODO(emilio): Measure and consider not doing this fast-path and take + // always the common 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`. + 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 { + bindings::Gecko_GetPreviousSibling(self.0) + .as_ref() + .map(GeckoNode) + } + } + + #[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 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 get_part_attr(&self) -> Option<&structs::nsAttrValue> { + if !self.has_part_attr() { + return None; + } + snapshot_helpers::find_attr(self.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.attrs(), &atom!("class")) + } + + #[inline] + fn closest_anon_subtree_root_parent(&self) -> Option { + debug_assert!(self.is_in_native_anonymous_subtree()); + let mut current = *self; + + loop { + if current.is_root_of_native_anonymous_subtree() { + return current.traversal_parent(); + } + + current = current.traversal_parent()?; + } + } + + #[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() + } + + // FIXME: We can implement this without OOL calls, but we can't easily given + // GeckoNode is a raw reference. + // + // We can use a Cell, but that's a bit of a pain. + #[inline] + fn set_flags(&self, flags: u32) { + unsafe { Gecko_SetNodeFlags(self.as_node().0, flags) } + } + + #[inline] + unsafe fn unset_flags(&self, flags: u32) { + Gecko_UnsetNodeFlags(self.as_node().0, flags) + } + + /// 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 as u32) != 0 + } + + /// Returns true if this element needs lazy frame construction. + #[inline] + pub fn needs_frame(&self) -> bool { + self.flags() & (NODE_NEEDS_FRAME as u32) != 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.mStates; + } + unsafe { Gecko_ElementState(self.0) } + } + + #[inline] + fn document_state(&self) -> DocumentState { + DocumentState::from_bits_truncate(self.as_node().owner_doc().0.mDocumentState.mStates) + } + + #[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 as u32) != 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) + } + + #[inline] + fn document_theme(&self) -> DocumentTheme { + let node = self.as_node(); + unsafe { Gecko_GetDocumentLWTheme(node.owner_doc().0) } + } + + /// 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 as u32) != 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 raw_end_value = unsafe { Gecko_ElementTransitions_EndValueAt(self.0, i).as_ref() }; + + let end_value = AnimationValue::arc_from_borrowed(&raw_end_value) + .expect("AnimationValue not found in ElementTransitions"); + + let property = end_value.id(); + debug_assert!(!property.is_logical()); + map.insert(property, end_value.clone_arc()); + } + map + } + + fn needs_transitions_update_per_property( + &self, + longhand_id: LonghandId, + combined_duration: 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 > 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 as u32; + } + if flags.contains(ElementSelectorFlags::HAS_SLOW_SELECTOR_LATER_SIBLINGS) { + gecko_flags |= NODE_HAS_SLOW_SELECTOR_LATER_SIBLINGS as u32; + } + if flags.contains(ElementSelectorFlags::HAS_EDGE_CHILD_SELECTOR) { + gecko_flags |= NODE_HAS_EDGE_CHILD_SELECTOR as u32; + } + if flags.contains(ElementSelectorFlags::HAS_EMPTY_SELECTOR) { + gecko_flags |= NODE_HAS_EMPTY_SELECTOR as u32; + } + + 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, + AnimationValueMap::as_ffi_mut(&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 + } +} + +#[derive(Debug)] +/// Gecko font metrics provider +pub struct GeckoFontMetricsProvider { + /// Cache of base font sizes for each language. Usually will have 1 element. + /// + /// This may be slow on pages using more languages, might be worth + /// optimizing by caching lang->group mapping separately and/or using a + /// hashmap on larger loads. + pub font_size_cache: RefCell>, +} + +impl GeckoFontMetricsProvider { + /// Construct + pub fn new() -> Self { + GeckoFontMetricsProvider { + font_size_cache: RefCell::new(Vec::new()), + } + } +} + +impl FontMetricsProvider for GeckoFontMetricsProvider { + fn create_from(_: &SharedStyleContext) -> GeckoFontMetricsProvider { + GeckoFontMetricsProvider::new() + } + + fn get_size(&self, font_name: &Atom, font_family: GenericFontFamily) -> Length { + let mut cache = self.font_size_cache.borrow_mut(); + if let Some(sizes) = cache.iter().find(|el| el.0 == *font_name) { + return sizes.1.size_for_generic(font_family); + } + let sizes = unsafe { bindings::Gecko_GetBaseSize(font_name.as_ptr()) }; + let size = sizes.size_for_generic(font_family); + cache.push((font_name.clone(), sizes)); + size + } + + fn query( + &self, + context: &crate::values::computed::Context, + base_size: FontBaseSize, + orientation: FontMetricsOrientation, + ) -> FontMetrics { + let pc = match context.device().pres_context() { + Some(pc) => pc, + None => return Default::default(), + }; + + let size = base_size.resolve(context); + let style = context.style(); + + let (wm, font) = match base_size { + FontBaseSize::CurrentStyle => (style.writing_mode, style.get_font()), + // This is only used for font-size computation. + FontBaseSize::InheritedStyle => { + (*style.inherited_writing_mode(), style.get_parent_font()) + }, + }; + + let vertical_metrics = match orientation { + FontMetricsOrientation::MatchContext => wm.is_vertical() && wm.is_upright(), + FontMetricsOrientation::Horizontal => false, + }; + let gecko_metrics = unsafe { + bindings::Gecko_GetFontMetrics( + pc, + vertical_metrics, + font.gecko(), + size, + // we don't use the user font set in a media query + !context.in_media_query, + ) + }; + FontMetrics { + x_height: Some(gecko_metrics.mXSize), + zero_advance_measure: if gecko_metrics.mChSize.px() >= 0. { + Some(gecko_metrics.mChSize) + } else { + None + }, + } + } +} + +/// The default font sizes for generic families for a given language group. +#[derive(Debug)] +#[repr(C)] +pub struct DefaultFontSizes { + variable: Length, + serif: Length, + sans_serif: Length, + monospace: Length, + cursive: Length, + fantasy: Length, +} + +impl DefaultFontSizes { + fn size_for_generic(&self, font_family: GenericFontFamily) -> Length { + match font_family { + GenericFontFamily::None => self.variable, + GenericFontFamily::Serif => self.serif, + GenericFontFamily::SansSerif => self.sans_serif, + GenericFontFamily::Monospace => self.monospace, + GenericFontFamily::Cursive => self.cursive, + GenericFontFamily::Fantasy => self.fantasy, + GenericFontFamily::MozEmoji => unreachable!( + "Should never get here, since this doesn't (yet) appear on font family" + ), + } + } +} + +impl<'le> TElement for GeckoElement<'le> { + type ConcreteNode = GeckoNode<'le>; + type FontMetricsProvider = GeckoFontMetricsProvider; + 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_in_native_anonymous_subtree() || + 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::nsContentUtils_sNameSpaceManager; + WeakNamespace::new((*namespace_manager).mURIArray[self.namespace_id() as usize].mRawPtr) + } + } + + /// 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; + } + + let declarations = unsafe { Gecko_GetStyleAttrDeclarationBlock(self.0).as_ref() }; + let declarations: Option<&RawOffsetArc>> = + declarations.and_then(|s| s.as_arc_opt()); + declarations.map(|s| s.borrow_arc()) + } + + 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::RawServoDeclarationBlock = declaration.mRaw.mRawPtr.as_ref()?; + + Some( + Locked::::as_arc( + &*(&raw as *const &structs::RawServoDeclarationBlock), + ) + .borrow_arc(), + ) + } + } + + 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_truncate(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.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.attrs()) + } + + 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.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 as u32) != 0 + } + + #[inline] + fn handled_snapshot(&self) -> bool { + self.flags() & (ELEMENT_HANDLED_SNAPSHOT as u32) != 0 + } + + unsafe fn set_handled_snapshot(&self) { + debug_assert!(self.has_data()); + self.set_flags(ELEMENT_HANDLED_SNAPSHOT as u32) + } + + #[inline] + fn has_dirty_descendants(&self) -> bool { + self.flags() & (ELEMENT_HAS_DIRTY_DESCENDANTS_FOR_SERVO as u32) != 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 as u32) + } + + unsafe fn unset_dirty_descendants(&self) { + self.unset_flags(ELEMENT_HAS_DIRTY_DESCENDANTS_FOR_SERVO as u32) + } + + #[inline] + fn has_animation_only_dirty_descendants(&self) -> bool { + self.flags() & (ELEMENT_HAS_ANIMATION_ONLY_DIRTY_DESCENDANTS_FOR_SERVO as u32) != 0 + } + + unsafe fn set_animation_only_dirty_descendants(&self) { + self.set_flags(ELEMENT_HAS_ANIMATION_ONLY_DIRTY_DESCENDANTS_FOR_SERVO as u32) + } + + unsafe fn unset_animation_only_dirty_descendants(&self) { + self.unset_flags(ELEMENT_HAS_ANIMATION_ONLY_DIRTY_DESCENDANTS_FOR_SERVO as u32) + } + + unsafe fn clear_descendant_bits(&self) { + self.unset_flags( + ELEMENT_HAS_DIRTY_DESCENDANTS_FOR_SERVO as u32 | + ELEMENT_HAS_ANIMATION_ONLY_DIRTY_DESCENDANTS_FOR_SERVO as u32 | + NODE_DESCENDANTS_NEED_FRAMES as u32, + ) + } + + fn is_visited_link(&self) -> bool { + self.state().intersects(ElementState::IN_VISITED_STATE) + } + + /// This logic is duplicated in Gecko's nsINode::IsInNativeAnonymousSubtree. + #[inline] + fn is_in_native_anonymous_subtree(&self) -> bool { + use crate::gecko_bindings::structs::NODE_IS_IN_NATIVE_ANONYMOUS_SUBTREE; + self.flags() & (NODE_IS_IN_NATIVE_ANONYMOUS_SUBTREE as u32) != 0 + } + + #[inline] + fn matches_user_and_author_rules(&self) -> bool { + !self.is_in_native_anonymous_subtree() + } + + #[inline] + fn implemented_pseudo_element(&self) -> Option { + if !self.is_in_native_anonymous_subtree() { + return None; + } + + if !self.has_properties() { + return None; + } + + let pseudo_type = unsafe { bindings::Gecko_GetImplementedPseudo(self.0) }; + PseudoElement::from_pseudo_type(pseudo_type) + } + + #[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 as u32 | + ELEMENT_HANDLED_SNAPSHOT as u32 | + structs::Element_kAllServoDescendantBits | + NODE_NEEDS_FRAME as u32, + ); + 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() + } + + unsafe fn set_selector_flags(&self, flags: ElementSelectorFlags) { + debug_assert!(!flags.is_empty()); + self.set_flags(selector_flags_to_node_flags(flags)); + } + + fn has_selector_flags(&self, flags: ElementSelectorFlags) -> bool { + let node_flags = selector_flags_to_node_flags(flags); + (self.flags() & node_flags) == node_flags + } + + #[inline] + fn may_have_animations(&self) -> bool { + if let Some(pseudo) = self.implemented_pseudo_element() { + if !pseudo.is_animatable() { + return false; + } + // 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_box_style = after_change_style.get_box(); + let existing_transitions = self.css_transitions_info(); + 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_box_style.transition_combined_duration_at(transition_property.index), + 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 3, which only accepts a + // single language tag, and which performs simple dash-prefix matching + // on it. + let override_lang_ptr = match override_lang { + Some(Some(ref atom)) => atom.as_ptr(), + _ => ptr::null_mut(), + }; + unsafe { + Gecko_MatchLang( + self.0, + override_lang_ptr, + override_lang.is_some(), + value.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::_x_text_zoom::SpecifiedValue as SpecifiedZoom; + use crate::properties::longhands::color::SpecifiedValue as SpecifiedColor; + use crate::properties::longhands::text_align::SpecifiedValue as SpecifiedTextAlign; + use crate::values::specified::color::Color; + lazy_static! { + static ref TH_RULE: ApplicableDeclarationBlock = { + let global_style_data = &*GLOBAL_STYLE_DATA; + let pdb = PropertyDeclarationBlock::with_one( + PropertyDeclaration::TextAlign(SpecifiedTextAlign::MozCenterOrInherit), + Importance::Normal, + ); + let arc = Arc::new_leaked(global_style_data.shared_lock.wrap(pdb)); + ApplicableDeclarationBlock::from_declarations(arc, ServoCascadeLevel::PresHints) + }; + 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) + }; + 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) + }; + static ref SVG_TEXT_DISABLE_ZOOM_RULE: ApplicableDeclarationBlock = { + let global_style_data = &*GLOBAL_STYLE_DATA; + let pdb = PropertyDeclarationBlock::with_one( + PropertyDeclaration::XTextZoom(SpecifiedZoom(false)), + Importance::Normal, + ); + let arc = Arc::new_leaked(global_style_data.shared_lock.wrap(pdb)); + ApplicableDeclarationBlock::from_declarations(arc, ServoCascadeLevel::PresHints) + }; + }; + + 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!("th").as_ptr() { + hints.push(TH_RULE.clone()); + } else 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_ZOOM_RULE.clone()); + } + } + let declarations = + unsafe { Gecko_GetHTMLPresentationAttrDeclarationBlock(self.0).as_ref() }; + let declarations: Option<&RawOffsetArc>> = + declarations.and_then(|s| s.as_arc_opt()); + if let Some(decl) = declarations { + hints.push(ApplicableDeclarationBlock::from_declarations( + decl.clone_arc(), + ServoCascadeLevel::PresHints, + )); + } + let declarations = unsafe { Gecko_GetExtraContentStyleDeclarations(self.0).as_ref() }; + let declarations: Option<&RawOffsetArc>> = + declarations.and_then(|s| s.as_arc_opt()); + if let Some(decl) = declarations { + hints.push(ApplicableDeclarationBlock::from_declarations( + decl.clone_arc(), + ServoCascadeLevel::PresHints, + )); + } + + // 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() + }, + }; + let declarations: Option<&RawOffsetArc>> = + declarations.and_then(|s| s.as_arc_opt()); + if let Some(decl) = declarations { + hints.push(ApplicableDeclarationBlock::from_declarations( + decl.clone_arc(), + ServoCascadeLevel::PresHints, + )); + } + + let active = self + .state() + .intersects(NonTSPseudoClass::Active.state_flag()); + if active { + let declarations = + unsafe { Gecko_GetActiveLinkAttrDeclarationBlock(self.0).as_ref() }; + let declarations: Option<&RawOffsetArc>> = + declarations.and_then(|s| s.as_arc_opt()); + if let Some(decl) = declarations { + hints.push(ApplicableDeclarationBlock::from_declarations( + decl.clone_arc(), + ServoCascadeLevel::PresHints, + )); + } + } + } + + // 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, + )) + } + // 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()); + } + } + } +} + +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()); + self.closest_anon_subtree_root_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 + } + + 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())); + 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, + flags_setter: &mut F, + ) -> bool + where + F: FnMut(&Self, ElementSelectorFlags), + { + 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::PlaceholderShown | + NonTSPseudoClass::Target | + NonTSPseudoClass::Valid | + NonTSPseudoClass::Invalid | + NonTSPseudoClass::MozUIValid | + NonTSPseudoClass::MozBroken | + NonTSPseudoClass::MozLoading | + NonTSPseudoClass::Required | + NonTSPseudoClass::Optional | + NonTSPseudoClass::ReadOnly | + NonTSPseudoClass::ReadWrite | + NonTSPseudoClass::FocusWithin | + NonTSPseudoClass::FocusVisible | + NonTSPseudoClass::MozDragOver | + NonTSPseudoClass::MozDevtoolsHighlighted | + NonTSPseudoClass::MozStyleeditorTransitioning | + NonTSPseudoClass::MozFocusRing | + NonTSPseudoClass::MozHandlerNoPlugins | + NonTSPseudoClass::MozMathIncrementScriptLevel | + NonTSPseudoClass::InRange | + NonTSPseudoClass::OutOfRange | + NonTSPseudoClass::Default | + NonTSPseudoClass::MozSubmitInvalid | + NonTSPseudoClass::MozUIInvalid | + NonTSPseudoClass::MozMeterOptimum | + NonTSPseudoClass::MozMeterSubOptimum | + NonTSPseudoClass::MozMeterSubSubOptimum | + NonTSPseudoClass::MozHasDirAttr | + NonTSPseudoClass::MozDirAttrLTR | + NonTSPseudoClass::MozDirAttrRTL | + NonTSPseudoClass::MozDirAttrLikeAuto | + NonTSPseudoClass::MozModalDialog | + NonTSPseudoClass::MozTopmostModalDialog | + NonTSPseudoClass::Active | + NonTSPseudoClass::Hover | + NonTSPseudoClass::MozAutofillPreview => { + 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 => { + flags_setter(self, 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 => { + flags_setter(self, 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 => { + flags_setter(self, ElementSelectorFlags::HAS_EMPTY_SELECTOR); + if self + .as_node() + .dom_children() + .any(|c| c.contains_non_whitespace_content()) + { + return false; + } + true + }, + NonTSPseudoClass::MozNativeAnonymous => self.is_in_native_anonymous_subtree(), + 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 => self.document_theme() != DocumentTheme::Doc_Theme_None, + NonTSPseudoClass::MozLWThemeBrightText => { + self.document_theme() == DocumentTheme::Doc_Theme_Bright + }, + NonTSPseudoClass::MozLWThemeDarkText => { + self.document_theme() == DocumentTheme::Doc_Theme_Dark + }, + NonTSPseudoClass::MozWindowInactive => { + let state_bit = DocumentState::NS_DOCUMENT_STATE_WINDOW_INACTIVE; + if context.extra_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), + NonTSPseudoClass::MozLocaleDir(ref dir) => { + let state_bit = DocumentState::NS_DOCUMENT_STATE_RTL_LOCALE; + if context.extra_data.document_state.intersects(state_bit) { + // NOTE(emilio): We could still return false for values + // other than "ltr" and "rtl", but we don't bother. + return !context.in_negation(); + } + + let doc_is_rtl = self.document_state().contains(state_bit); + + match dir.as_horizontal_direction() { + Some(HorizontalDirection::Ltr) => !doc_is_rtl, + Some(HorizontalDirection::Rtl) => doc_is_rtl, + None => false, + } + }, + NonTSPseudoClass::Dir(ref dir) => self.state().intersects(dir.element_state()), + } + } + + 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::IN_VISITED_OR_UNVISITED_STATE) + } + + #[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.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.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..87f444f5a9 --- /dev/null +++ b/servo/components/style/gecko_bindings/mod.rs @@ -0,0 +1,25 @@ +/* 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 +)] +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..10f7604647 --- /dev/null +++ b/servo/components/style/gecko_bindings/sugar/ns_com_ptr.rs @@ -0,0 +1,25 @@ +/* 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; + +#[cfg(feature = "gecko_debug")] +impl nsCOMPtr { + /// Get this pointer as a raw pointer. + #[inline] + pub fn raw(&self) -> *mut T { + self.mRawPtr + } +} + +#[cfg(not(feature = "gecko_debug"))] +impl nsCOMPtr { + /// Get this pointer as a raw pointer. + #[inline] + pub fn raw(&self) -> *mut T { + self._base.mRawPtr as *mut _ + } +} 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..97992b3f76 --- /dev/null +++ b/servo/components/style/gecko_bindings/sugar/ns_style_auto_array.rs @@ -0,0 +1,84 @@ +/* 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_EnsureStyleTransitionArrayLength; +use crate::gecko_bindings::structs::nsStyleAutoArray; +use crate::gecko_bindings::structs::{StyleAnimation, StyleTransition}; +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<'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..e0f0981c5d --- /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_truncate(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..249134169f --- /dev/null +++ b/servo/components/style/gecko_bindings/sugar/ownership.rs @@ -0,0 +1,348 @@ +/* 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, RawOffsetArc}; +use std::marker::PhantomData; +use std::mem::{forget, transmute}; +use std::ops::{Deref, DerefMut}; +use std::ptr; + +/// Indicates that a given Servo type has a corresponding Gecko FFI type. +pub unsafe trait HasFFI: Sized + 'static { + /// The corresponding Gecko type that this rust type represents. + /// + /// See the examples in `components/style/gecko/conversions.rs`. + type FFIType: Sized; +} + +/// Indicates that a given Servo type has the same layout as the corresponding +/// `HasFFI::FFIType` type. +pub unsafe trait HasSimpleFFI: HasFFI { + #[inline] + /// Given a Servo-side reference, converts it to an FFI-safe reference which + /// can be passed to Gecko. + /// + /// &ServoType -> &GeckoType + fn as_ffi(&self) -> &Self::FFIType { + unsafe { transmute(self) } + } + #[inline] + /// Given a Servo-side mutable reference, converts it to an FFI-safe mutable + /// reference which can be passed to Gecko. + /// + /// &mut ServoType -> &mut GeckoType + fn as_ffi_mut(&mut self) -> &mut Self::FFIType { + unsafe { transmute(self) } + } + #[inline] + /// Given an FFI-safe reference obtained from Gecko converts it to a + /// Servo-side reference. + /// + /// &GeckoType -> &ServoType + fn from_ffi(ffi: &Self::FFIType) -> &Self { + unsafe { transmute(ffi) } + } + #[inline] + /// Given an FFI-safe mutable reference obtained from Gecko converts it to a + /// Servo-side mutable reference. + /// + /// &mut GeckoType -> &mut ServoType + fn from_ffi_mut(ffi: &mut Self::FFIType) -> &mut Self { + unsafe { transmute(ffi) } + } +} + +/// Indicates that the given Servo type is passed over FFI +/// as a Box +pub unsafe trait HasBoxFFI: HasSimpleFFI { + #[inline] + /// Converts a borrowed Arc to a borrowed FFI reference. + /// + /// &Arc -> &GeckoType + fn into_ffi(self: Box) -> Owned { + unsafe { transmute(self) } + } + + /// Drops an owned FFI pointer. This conceptually takes the + /// Owned, except it's a bit of a paint to do that without + /// much benefit. + #[inline] + unsafe fn drop_ffi(ptr: *mut Self::FFIType) { + let _ = Box::from_raw(ptr as *mut Self); + } +} + +/// Helper trait for conversions between FFI Strong/Borrowed types and Arcs +/// +/// Should be implemented by types which are passed over FFI as Arcs via Strong +/// and Borrowed. +/// +/// In this case, the FFIType is the rough equivalent of ArcInner. +pub unsafe trait HasArcFFI: HasFFI { + // these methods can't be on Borrowed because it leads to an unspecified + // impl parameter + /// Artificially increments the refcount of a (possibly null) borrowed Arc + /// over FFI. + unsafe fn addref_opt(ptr: Option<&Self::FFIType>) { + forget(Self::arc_from_borrowed(&ptr).clone()) + } + + /// Given a (possibly null) borrowed FFI reference, decrements the refcount. + /// Unsafe since it doesn't consume the backing Arc. Run it only when you + /// know that a strong reference to the backing Arc is disappearing + /// (usually on the C++ side) without running the Arc destructor. + unsafe fn release_opt(ptr: Option<&Self::FFIType>) { + if let Some(arc) = Self::arc_from_borrowed(&ptr) { + let _: RawOffsetArc<_> = ptr::read(arc as *const RawOffsetArc<_>); + } + } + + /// Artificially increments the refcount of a borrowed Arc over FFI. + unsafe fn addref(ptr: &Self::FFIType) { + forget(Self::as_arc(&ptr).clone()) + } + + /// Given a non-null borrowed FFI reference, decrements the refcount. + /// Unsafe since it doesn't consume the backing Arc. Run it only when you + /// know that a strong reference to the backing Arc is disappearing + /// (usually on the C++ side) without running the Arc destructor. + unsafe fn release(ptr: &Self::FFIType) { + let _: RawOffsetArc<_> = ptr::read(Self::as_arc(&ptr) as *const RawOffsetArc<_>); + } + #[inline] + /// Converts a borrowed FFI reference to a borrowed Arc. + /// + /// &GeckoType -> &Arc + fn as_arc<'a>(ptr: &'a &Self::FFIType) -> &'a RawOffsetArc { + unsafe { transmute::<&&Self::FFIType, &RawOffsetArc>(ptr) } + } + + #[inline] + /// Converts a borrowed Arc to a borrowed FFI reference. + /// + /// &Arc -> &GeckoType + fn arc_as_borrowed<'a>(arc: &'a RawOffsetArc) -> &'a &Self::FFIType { + unsafe { transmute::<&RawOffsetArc, &&Self::FFIType>(arc) } + } + + #[inline] + /// Converts a borrowed nullable FFI reference to a borrowed Arc. + /// + /// &GeckoType -> &Arc + fn arc_from_borrowed<'a>(ptr: &'a Option<&Self::FFIType>) -> Option<&'a RawOffsetArc> { + unsafe { + if let Some(ref reference) = *ptr { + Some(transmute::<&&Self::FFIType, &RawOffsetArc<_>>(reference)) + } else { + None + } + } + } +} + +/// 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 Strong { + #[inline] + /// Returns whether this reference is null. + pub fn is_null(&self) -> bool { + self.ptr.is_null() + } + + #[inline] + /// Given a non-null strong FFI reference, converts it into a servo-side + /// Arc. + /// + /// Panics on null. + /// + /// Strong -> Arc + pub fn into_arc(self) -> RawOffsetArc + where + ServoType: HasArcFFI, + { + self.into_arc_opt().unwrap() + } + + #[inline] + /// Given a strong FFI reference, + /// converts it into a servo-side Arc + /// Returns None on null. + /// + /// Strong -> Arc + pub fn into_arc_opt(self) -> Option> + where + ServoType: HasArcFFI, + { + if self.is_null() { + None + } else { + unsafe { Some(transmute(self)) } + } + } + + #[inline] + /// Given a reference to a strong FFI reference, converts it to a reference + /// to a servo-side Arc. + /// + /// Returns None on null. + /// + /// Strong -> Arc + pub fn as_arc_opt(&self) -> Option<&RawOffsetArc> + where + ServoType: HasArcFFI, + { + if self.is_null() { + None + } else { + unsafe { Some(transmute(self)) } + } + } + + #[inline] + /// Produces a null strong FFI reference. + pub fn null() -> Self { + unsafe { transmute(ptr::null::()) } + } +} + +/// A few helpers implemented on top of Arc to make it more +/// comfortable to use and write safe code with. +pub unsafe trait FFIArcHelpers { + /// The Rust FFI type that we're implementing methods for. + type Inner: HasArcFFI; + + /// Converts an Arc into a strong FFI reference. + /// + /// Arc -> Strong + fn into_strong(self) -> Strong<::FFIType>; + + /// Produces a borrowed FFI reference by borrowing an Arc. + /// + /// &Arc -> &GeckoType + /// + /// Then the `arc_as_borrowed` method can go away. + fn as_borrowed(&self) -> &::FFIType; +} + +unsafe impl FFIArcHelpers for RawOffsetArc { + type Inner = T; + + #[inline] + fn into_strong(self) -> Strong { + unsafe { transmute(self) } + } + + #[inline] + fn as_borrowed(&self) -> &T::FFIType { + unsafe { &*(&**self as *const T as *const T::FFIType) } + } +} + +unsafe impl FFIArcHelpers for Arc { + type Inner = T; + + #[inline] + fn into_strong(self) -> Strong { + Arc::into_raw_offset(self).into_strong() + } + + #[inline] + fn as_borrowed(&self) -> &T::FFIType { + unsafe { &*(&**self as *const T as *const T::FFIType) } + } +} + +#[repr(C)] +#[derive(Debug)] +/// Gecko-FFI-safe owned pointer. +/// +/// Cannot be null, and leaks on drop, so needs to be converted into a rust-side +/// `Box` before. +pub struct Owned { + ptr: *mut GeckoType, + _marker: PhantomData, +} + +impl Owned { + /// Converts this instance to a (non-null) instance of `OwnedOrNull`. + pub fn maybe(self) -> OwnedOrNull { + unsafe { transmute(self) } + } +} + +impl Deref for Owned { + type Target = GeckoType; + fn deref(&self) -> &GeckoType { + unsafe { &*self.ptr } + } +} + +impl DerefMut for Owned { + fn deref_mut(&mut self) -> &mut GeckoType { + unsafe { &mut *self.ptr } + } +} + +#[repr(C)] +/// Gecko-FFI-safe owned pointer. +/// +/// Can be null, and just as `Owned` leaks on `Drop`. +pub struct OwnedOrNull { + ptr: *mut GeckoType, + _marker: PhantomData, +} + +impl OwnedOrNull { + /// Returns a null pointer. + #[inline] + pub fn null() -> Self { + Self { + ptr: ptr::null_mut(), + _marker: PhantomData, + } + } + + /// Returns whether this pointer is null. + #[inline] + pub fn is_null(&self) -> bool { + self.ptr.is_null() + } + + /// Gets a immutable reference to the underlying Gecko type, or `None` if + /// null. + pub fn borrow(&self) -> Option<&GeckoType> { + unsafe { transmute(self) } + } + + /// Gets a mutable reference to the underlying Gecko type, or `None` if + /// null. + pub fn borrow_mut(&self) -> Option<&mut GeckoType> { + unsafe { transmute(self) } + } +} + +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..a141aef4e8 --- /dev/null +++ b/servo/components/style/gecko_bindings/sugar/refptr.rs @@ -0,0 +1,331 @@ +/* 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::sugar::ownership::HasArcFFI; +use crate::gecko_bindings::{bindings, structs}; +use crate::Atom; +use servo_arc::Arc; +use std::marker::PhantomData; +use std::ops::{Deref, DerefMut}; +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_str("}") + } +} + +/// A RefPtr that we know is uniquely owned. +/// +/// This is basically Box, with the additional guarantee that the box can be +/// safely interpreted as a RefPtr (with refcount 1) +/// +/// This is useful when you wish to create a refptr and mutate it temporarily, +/// while it is still uniquely owned. +pub struct UniqueRefPtr(RefPtr); + +// There is no safe conversion from &T to RefPtr (like Gecko has) +// because this lets you break UniqueRefPtr's guarantee + +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: 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 UniqueRefPtr { + /// Create a unique refptr from an already addrefed pointer obtained from + /// FFI. + /// + /// The refcount must be one. + /// + /// The pointer must be valid and non null + pub unsafe fn from_addrefed(ptr: *mut T) -> Self { + UniqueRefPtr(RefPtr::from_addrefed(ptr)) + } + + /// Convert to a RefPtr so that it can be used. + pub fn get(self) -> RefPtr { + self.0 + } +} + +impl Deref for RefPtr { + type Target = T; + fn deref(&self) -> &T { + debug_assert!(!self.ptr.is_null()); + unsafe { &*self.ptr } + } +} + +impl Deref for UniqueRefPtr { + type Target = T; + fn deref(&self) -> &T { + unsafe { &*self.0.ptr } + } +} + +impl DerefMut for UniqueRefPtr { + fn deref_mut(&mut self) -> &mut T { + unsafe { &mut *self.0.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 { + /// Sets the contents to an `Arc`, releasing the old value in `self` if + /// necessary. + pub fn set_arc(&mut self, other: Arc) + where + U: HasArcFFI, + { + unsafe { + U::release_opt(self.mRawPtr.as_ref()); + } + self.set_arc_leaky(other); + } + + /// Sets the contents to an Arc + /// will leak existing contents + pub fn set_arc_leaky(&mut self, other: Arc) + where + U: HasArcFFI, + { + *self = unsafe { mem::transmute(Arc::into_raw_offset(other)) }; + } +} + +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::SharedFontList, + bindings::Gecko_AddRefSharedFontListArbitraryThread, + bindings::Gecko_ReleaseSharedFontListArbitraryThread +); +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..a6cf017877 --- /dev/null +++ b/servo/components/style/gecko_string_cache/mod.rs @@ -0,0 +1,525 @@ +/* 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 + } +} + +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..1acf04c93f --- /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(Hash, Deref)] +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..a9e79bf682 --- /dev/null +++ b/servo/components/style/global_style_data.rs @@ -0,0 +1,173 @@ +/* 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 parking_lot::{RwLock, RwLockReadGuard}; +use rayon; +use std::env; +use std::sync::atomic::{AtomicUsize, Ordering}; + +/// 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. + pub num_threads: usize, + + /// 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) +} + +// A counter so that we can wait for shutdown of all threads. See +// StyleThreadPool::shutdown. +static ALIVE_WORKER_THREADS: AtomicUsize = AtomicUsize::new(0); + +fn thread_startup(_index: usize) { + ALIVE_WORKER_THREADS.fetch_add(1, Ordering::Relaxed); + thread_state::initialize_layout_worker_thread(); + #[cfg(feature = "gecko")] + unsafe { + use std::ffi::CString; + + bindings::Gecko_SetJemallocThreadLocalArena(true); + let name = thread_name(_index); + let name = CString::new(name).unwrap(); + // Gecko_RegisterProfilerThread copies the passed name here. + bindings::Gecko_RegisterProfilerThread(name.as_ptr()); + } +} + +fn thread_shutdown(_: usize) { + #[cfg(feature = "gecko")] + unsafe { + bindings::Gecko_UnregisterProfilerThread(); + bindings::Gecko_SetJemallocThreadLocalArena(false); + } + ALIVE_WORKER_THREADS.fetch_sub(1, Ordering::Relaxed); +} + +impl StyleThreadPool { + /// Shuts down the thread pool, waiting for all work to complete. + pub fn shutdown() { + if ALIVE_WORKER_THREADS.load(Ordering::Relaxed) == 0 { + return; + } + { + // Drop the pool. + let _ = STYLE_THREAD_POOL.style_thread_pool.write().take(); + } + // Spin until all our threads are done. This will usually be pretty + // fast, as on shutdown there should be basically no threads left + // running. + // + // This still _technically_ doesn't give us the guarantee of TLS + // destructors running on the worker threads. For that we'd need help + // from rayon to properly join the threads. + // + // See https://github.com/rayon-rs/rayon/issues/688 + // + // So we instead intentionally leak TLS stuff (see BLOOM_KEY and co) for + // now until that's fixed. + while ALIVE_WORKER_THREADS.load(Ordering::Relaxed) != 0 { + std::thread::yield_now(); + } + } + + /// 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() + } +} + +lazy_static! { + /// Global thread pool + pub static ref STYLE_THREAD_POOL: StyleThreadPool = { + let stylo_threads = env::var("STYLO_THREADS") + .map(|s| s.parse::().expect("invalid STYLO_THREADS value")); + let mut num_threads = match stylo_threads { + Ok(num) => num, + #[cfg(feature = "servo")] + _ => { + use servo_config::pref; + // We always set this pref on startup, before layout or script + // have had a chance of accessing (and thus creating) the + // thread-pool. + pref!(layout.threads) as usize + } + #[cfg(feature = "gecko")] + _ => { + // 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). + use num_cpus; + use std::cmp; + cmp::min(cmp::max(num_cpus::get() * 3 / 4, 1), 6) + } + }; + + // If num_threads is one, there's no point in creating a thread pool, so + // force it to zero. + // + // We allow developers to force a one-thread pool for testing via a + // special environmental variable. + if num_threads == 1 { + let force_pool = env::var("FORCE_STYLO_THREAD_POOL") + .ok().map_or(false, |s| s.parse::().expect("invalid FORCE_STYLO_THREAD_POOL value") == 1); + if !force_pool { + num_threads = 0; + } + } + + let pool = if num_threads < 1 { + None + } else { + let workers = rayon::ThreadPoolBuilder::new() + .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() + }; + + 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/hash.rs b/servo/components/style/hash.rs new file mode 100644 index 0000000000..197c5c1283 --- /dev/null +++ b/servo/components/style/hash.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/. */ + +//! Reexports of hashglobe types in Gecko mode, and stdlib hashmap shims in Servo mode +//! +//! Can go away when the stdlib gets fallible collections +//! https://github.com/rust-lang/rfcs/pull/2116 + +use fxhash; + +#[cfg(feature = "gecko")] +pub use hashglobe::hash_map::HashMap; +#[cfg(feature = "gecko")] +pub use hashglobe::hash_set::HashSet; + +#[cfg(feature = "servo")] +pub use hashglobe::fake::{HashMap, HashSet}; + +/// Appropriate reexports of hash_map types +pub mod map { + #[cfg(feature = "gecko")] + pub use hashglobe::hash_map::{Entry, Iter}; + #[cfg(feature = "servo")] + pub use std::collections::hash_map::{Entry, Iter}; +} + +/// Hash map that uses the Fx hasher +pub type FxHashMap = HashMap; +/// Hash set that uses the Fx hasher +pub type FxHashSet = HashSet; 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..9ee97344a4 --- /dev/null +++ b/servo/components/style/invalidation/element/document_state.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/. */ + +//! An invalidation processor for style changes due to document state changes. + +use crate::dom::TElement; +use crate::element_state::DocumentState; +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 selectors::matching::{MatchingContext, MatchingMode, QuirksMode, VisitedHandlingMode}; + +/// A struct holding the members necessary to invalidate document state +/// selectors. +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, quirks_mode: QuirksMode) -> Self { + let mut matching_context = MatchingContext::new_for_visited( + MatchingMode::Normal, + None, + None, + VisitedHandlingMode::AllLinksVisitedAndUnvisited, + quirks_mode, + ); + + matching_context.extra_data = InvalidationMatchingData { + 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); + } +} 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..d79e140222 --- /dev/null +++ b/servo/components/style/invalidation/element/element_wrapper.rs @@ -0,0 +1,408 @@ +/* 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::element_state::ElementState; +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 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, + _setter: &mut F, + ) -> bool + where + F: FnMut(&Self, ElementSelectorFlags), + { + // Some pseudo-classes need special handling to evaluate them against + // the snapshot. + match *pseudo_class { + // :dir is implemented in terms of state flags, but which state flag + // it maps to depends on the argument to :dir. That means we can't + // just add its state flags to the NonTSPseudoClass, because if we + // added all of them there, and tested via intersects() here, we'd + // get incorrect behavior for :not(:dir()) cases. + // + // FIXME(bz): How can I set this up so once Servo adds :dir() + // support we don't forget to update this code? + #[cfg(feature = "gecko")] + NonTSPseudoClass::Dir(ref dir) => { + let selector_flag = dir.element_state(); + if selector_flag.is_empty() { + // :dir() with some random argument; does not match. + return false; + } + let state = match self.snapshot().and_then(|s| s.state()) { + Some(snapshot_state) => snapshot_state, + None => self.element.state(), + }; + return state.contains(selector_flag); + }, + + // 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, &mut |_, _| {}); + } + 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, &mut |_, _| {}), + } + } + + 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::IN_VISITED_OR_UNVISITED_STATE), + 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)) + } + + #[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..29e0962a89 --- /dev/null +++ b/servo/components/style/invalidation/element/invalidation_map.rs @@ -0,0 +1,541 @@ +/* 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::element_state::{DocumentState, ElementState}; +use crate::selector_map::{ + MaybeCaseInsensitiveHashMap, PrecomputedHashMap, SelectorMap, SelectorMapEntry, +}; +use crate::selector_parser::SelectorImpl; +use crate::{Atom, LocalName, Namespace}; +use fallible::FallibleVec; +use hashglobe::FailedAllocationError; +use selectors::attr::NamespaceConstraint; +use selectors::parser::{Combinator, Component}; +use selectors::parser::{Selector, SelectorIter}; +use selectors::visitor::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(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(); + } + + /// Adds a selector to this `InvalidationMap`. Returns Err(..) to + /// signify OOM. + pub fn note_selector( + &mut self, + selector: &Selector, + quirks_mode: QuirksMode, + ) -> Result<(), FailedAllocationError> { + 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_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); + 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; + let entry = match map.try_entry(name) { + Ok(entry) => entry, + Err(err) => { + *self.alloc_error = Some(err); + return false; + }, + }; + + match entry.or_insert_with(SmallVec::new).try_push(dependency) { + Ok(..) => true, + Err(err) => { + *self.alloc_error = Some(err); + return false; + }, + } + } + + 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: &[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); + return false; + }, + }; + match entry.or_insert_with(SmallVec::new).try_push(dependency) { + Ok(..) => true, + Err(err) => { + *self.alloc_error = Some(err); + return false; + }, + } + }, + Component::NonTSPseudoClass(ref pc) => { + self.compound_state.element_state |= match *pc { + #[cfg(feature = "gecko")] + NonTSPseudoClass::Dir(ref dir) => dir.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..c148b63e3e --- /dev/null +++ b/servo/components/style/invalidation/element/invalidator.rs @@ -0,0 +1,1054 @@ +/* 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; + +/// 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 an eager pseudo-element + /// selector like ::before or ::after triggers invalidation of the element + /// that would originate it. + fn invalidates_on_eager_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 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_str(")") + } +} + +/// 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_self(sibling); + } + + 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) { + // This will usually be the very next component, except for + // the fact that we store compound selectors the other way + // around, so there could also be state pseudo-classes. + let pseudo = next_invalidation + .dependency + .selector + .iter_raw_parse_order_from(next_invalidation.offset) + .flat_map(|c| { + if let Component::PseudoElement(ref pseudo) = *c { + return Some(pseudo); + } + + // TODO: Would be nice to make this a diagnostic_assert! of + // sorts. + debug_assert!( + c.maybe_allowed_after_pseudo_element(), + "Someone seriously messed up selector parsing: \ + {:?} at offset {:?}: {:?}", + next_invalidation.dependency, + next_invalidation.offset, + c, + ); + + None + }) + .next() + .unwrap(); + + // FIXME(emilio): This is not ideal, and could not be + // accurate if we ever have stateful element-backed eager + // pseudos. + // + // Ideally, we'd just remove element-backed eager pseudos + // altogether, given they work fine without it. Only gotcha + // is that we wouldn't style them in parallel, which may or + // may not be an issue. + // + // Also, this could be more fine grained now (perhaps a + // RESTYLE_PSEUDOS hint?). + // + // Note that we'll also restyle the pseudo-element because + // it would match this invalidation. + if self.processor.invalidates_on_eager_pseudo_element() { + if pseudo.is_eager() { + invalidated_self = true; + } + // If we start or stop matching some marker rules, and + // don't have a marker, then we need to restyle the + // element to potentially create one. + // + // Same caveats as for other eager pseudos apply, this + // could be more fine-grained. + if pseudo.is_marker() && self.element.marker_pseudo_element().is_none() { + invalidated_self = true; + } + + // FIXME: ::selection doesn't generate elements, so the + // regular invalidation doesn't work for it. We store + // the cached selection style holding off the originating + // element, so we need to restyle it in order to invalidate + // it. This is still not quite correct, since nothing + // triggers a repaint necessarily, but matches old Gecko + // behavior, and the ::selection implementation needs to + // change significantly anyway to implement + // https://github.com/w3c/csswg-drafts/issues/2474. + if pseudo.is_selection() { + 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..c0bf2424d5 --- /dev/null +++ b/servo/components/style/invalidation/element/restyle_hints.rs @@ -0,0 +1,187 @@ +/* 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)] + pub struct RestyleHint: u8 { + /// Do a selector match of the element. + const RESTYLE_SELF = 1 << 0; + + /// Do a selector match of the element's descendants. + const RESTYLE_DESCENDANTS = 1 << 1; + + /// Recascade the current element. + const RECASCADE_SELF = 1 << 2; + + /// Recascade all descendant elements. + const RECASCADE_DESCENDANTS = 1 << 3; + + /// 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 << 4; + + /// 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 << 5; + + /// 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 << 6; + + /// 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 << 7; + } +} + +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(RestyleHint::RESTYLE_SELF | RestyleHint::RESTYLE_DESCENDANTS) + } + + /// Returns whether we need to restyle this element. + pub fn has_non_animation_invalidations(&self) -> bool { + self.intersects( + RestyleHint::RESTYLE_SELF | + RestyleHint::RECASCADE_SELF | + (Self::replacements() & !Self::for_animations()), + ) + } + + /// 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 `CascadeHint` 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(); + } + if self.contains(RestyleHint::RECASCADE_DESCENDANTS) { + return Self::recascade_subtree(); + } + Self::empty() + } + + /// 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 the currently element must be + /// recascaded. + pub fn has_recascade_self(&self) -> bool { + self.contains(RestyleHint::RECASCADE_SELF) + } + + /// 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() | RestyleHint::RECASCADE_SELF) + } + + /// 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 selector matching must be re-run + /// for the element. + #[inline] + pub fn match_self(&self) -> bool { + self.intersects(RestyleHint::RESTYLE_SELF) + } + + /// 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(RestyleHint::RECASCADE_SELF); + } +} + +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..128feae76b --- /dev/null +++ b/servo/components/style/invalidation/element/state_and_attributes.rs @@ -0,0 +1,524 @@ +/* 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; +use crate::element_state::ElementState; +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 selectors::attr::CaseSensitivity; +use selectors::matching::matches_selector; +use selectors::matching::{MatchingContext, MatchingMode, 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, + Some(nth_index_cache), + VisitedHandlingMode::AllLinksVisitedAndUnvisited, + shared_context.quirks_mode(), + ); + + 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, + mut context: &mut MatchingContext<'_, E::Impl>, +) -> bool +where + E: TElement, + W: selectors::Element, +{ + let matches_now = matches_selector( + &dependency.selector, + dependency.selector_offset, + None, + element, + &mut context, + &mut |_, _| {}, + ); + + let matched_then = matches_selector( + &dependency.selector, + dependency.selector_offset, + None, + wrapper, + &mut context, + &mut |_, _| {}, + ); + + 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 invalidated_descendants(element: E, child: E) +where + E: TElement, +{ + if !child.has_data() { + return; + } + + // 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 == element { + break; + } + } +} + +/// Sets the appropriate restyle hint after invalidating the style of a given +/// element. +pub fn invalidated_self(element: E) +where + E: TElement, +{ + if let Some(mut data) = element.mutate_data() { + data.hint.insert(RestyleHint::RESTYLE_SELF); + } +} + +impl<'a, 'b: 'a, E: 'a> InvalidationProcessor<'a, E> + for StateAndAttrInvalidationProcessor<'a, 'b, E> +where + E: TElement, +{ + /// We need to invalidate style on an eager pseudo-element, in order to + /// process changes that could otherwise end up in ::before or ::after + /// content being generated. + fn invalidates_on_eager_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; + } + + // If we the visited state changed, we force a restyle here. Matching + // doesn't depend on the actual visited state at all, so we can't look + // at matching results to decide what to do for this case. + // + // TODO(emilio): This piece of code should be removed when + // layout.css.always-repaint-on-unvisited is true, since we cannot get + // into this situation in that case. + if state_changes.contains(ElementState::IN_VISITED_OR_UNVISITED_STATE) { + trace!(" > visitedness change, force subtree restyle"); + // We can't just return here because there may also be attribute + // changes as well that imply additional hints for siblings. + self.data.hint.insert(RestyleHint::restyle_subtree()); + } + + 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); + } +} + +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); + } + } + }); + + let state_changes = self.state_changes; + if !state_changes.is_empty() { + self.collect_state_dependencies(&map.state_affecting_selectors, state_changes) + } + } + + fn collect_state_dependencies( + &mut self, + map: &'selectors SelectorMap, + state_changes: ElementState, + ) { + map.lookup_with_additional( + self.lookup_element, + self.matching_context.quirks_mode(), + self.removed_id, + self.classes_removed, + |dependency| { + if !dependency.state.intersects(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..75149a0289 --- /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, Stylesheet, 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 Stylesheet {} +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(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..a59d9474b0 --- /dev/null +++ b/servo/components/style/invalidation/mod.rs @@ -0,0 +1,9 @@ +/* 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; diff --git a/servo/components/style/invalidation/stylesheets.rs b/servo/components/style/invalidation/stylesheets.rs new file mode 100644 index 0000000000..9854387cca --- /dev/null +++ b/servo/components/style/invalidation/stylesheets.rs @@ -0,0 +1,635 @@ +/* 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::Atom; +use crate::LocalName as SelectorLocalName; +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(Copy, Clone, 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; + } + + /// 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; + } + } + + 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; + let entry = match self.local_names.try_entry(name) { + Ok(e) => e, + Err(..) => return false, + }; + *entry.or_insert(InvalidationKind::None) |= kind; + if insert_lower { + let entry = match self.local_names.try_entry(lower_name) { + Ok(e) => e, + Err(..) => return false, + }; + *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(..) | + Viewport(..) | + FontFeatureValues(..) | + FontFace(..) | + Keyframes(..) | + 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(..) => { + 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(..) => { + // 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(..) | Viewport(..) | FontFeatureValues(..) => { + 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/lib.rs b/servo/components/style/lib.rs new file mode 100644 index 0000000000..944f4a1e68 --- /dev/null +++ b/servo/components/style/lib.rs @@ -0,0 +1,261 @@ +/* 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; +#[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 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; +#[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; +pub mod element_state; +#[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 hash; +pub mod invalidation; +#[allow(missing_docs)] // TODO. +pub mod logical_geometry; +pub mod matching; +#[macro_use] +pub mod media_queries; +pub mod parallel; +pub mod parser; +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 html5ever::LocalName; +#[cfg(feature = "servo")] +pub use html5ever::Namespace; +#[cfg(feature = "servo")] +pub use html5ever::Prefix; +#[cfg(feature = "servo")] +pub use servo_atoms::Atom; + +pub use style_traits::arc_slice::ArcSlice; +pub use style_traits::owned_slice::OwnedSlice; +pub use style_traits::owned_str::OwnedStr; + +/// The CSS properties supported by the style system. +/// Generated from the properties.mako.rs template by build.rs +#[macro_use] +#[allow(unsafe_code)] +#[deny(missing_docs)] +pub mod properties { + include!(concat!(env!("OUT_DIR"), "/properties.rs")); +} + +#[cfg(feature = "gecko")] +#[allow(unsafe_code)] +pub mod gecko; + +// uses a macro from properties +#[cfg(feature = "servo")] +#[allow(unsafe_code)] +pub mod servo; + +#[cfg(feature = "gecko")] +#[allow(unsafe_code, missing_docs)] +pub mod gecko_properties { + include!(concat!(env!("OUT_DIR"), "/gecko_properties.rs")); +} + +macro_rules! reexport_computed_values { + ( $( { $name: ident, $boxed: expr } )+ ) => { + /// 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 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() + } +} diff --git a/servo/components/style/logical_geometry.rs b/servo/components/style/logical_geometry.rs new file mode 100644 index 0000000000..d4d058adc6 --- /dev/null +++ b/servo/components/style/logical_geometry.rs @@ -0,0 +1,1467 @@ +/* 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!( + #[cfg_attr(feature = "servo", derive(MallocSizeOf, 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 + } + + #[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: 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: block_start, + inline_end: inline_end, + block_end: block_end, + inline_start: 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..e60be8efc5 --- /dev/null +++ b/servo/components/style/macros.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/. */ + +//! 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()) + )) + } + }} +} + +macro_rules! define_keyword_type { + ($name:ident, $css:expr) => { + #[allow(missing_docs)] + #[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToAnimatedValue, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, + )] + pub struct $name; + + impl fmt::Debug for $name { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str($css) + } + } + + impl $crate::parser::Parse for $name { + fn parse<'i, 't>( + _context: &$crate::parser::ParserContext, + input: &mut ::cssparser::Parser<'i, 't>, + ) -> Result<$name, ::style_traits::ParseError<'i>> { + input + .expect_ident_matching($css) + .map(|_| $name) + .map_err(|e| e.into()) + } + } + }; +} + +/// Place a Gecko profiler label on the stack. +/// +/// The `label_type` argument must be the name of a variant of `ProfilerLabel`. +#[cfg(feature = "gecko_profiler")] +#[macro_export] +macro_rules! profiler_label { + ($label_type:ident) => { + let mut _profiler_label = + ::std::mem::MaybeUninit::<$crate::gecko_bindings::structs::AutoProfilerLabel>::uninit(); + let _profiler_label = if $crate::gecko::profiler::profiler_is_active() { + unsafe { + Some($crate::gecko::profiler::AutoProfilerLabel::new( + &mut _profiler_label, + $crate::gecko::profiler::ProfilerLabel::$label_type, + )) + } + } else { + None + }; + }; +} + +/// No-op when the Gecko profiler is not available. +#[cfg(not(feature = "gecko_profiler"))] +#[macro_export] +macro_rules! profiler_label { + ($label_type:ident) => {}; +} + +#[cfg(feature = "gecko")] +macro_rules! local_name { + ($s:tt) => { + $crate::values::AtomIdent(atom!($s)) + }; +} diff --git a/servo/components/style/matching.rs b/servo/components/style/matching.rs new file mode 100644 index 0000000000..f81f9c3a23 --- /dev/null +++ b/servo/components/style/matching.rs @@ -0,0 +1,1083 @@ +/* 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, SelectorFlagsMap}; +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::stylist::RuleInclusion; +use crate::traversal_flags::TraversalFlags; +use selectors::matching::ElementSelectorFlags; +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 cascade +/// to children. +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub enum ChildCascadeRequirement { + /// 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, +} + +impl ChildCascadeRequirement { + /// Whether we can unconditionally skip the cascade. + pub fn can_skip_cascade(&self) -> bool { + matches!(*self, ChildCascadeRequirement::CanSkipCascade) + } +} + +/// 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, + 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, + 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(), + style_attribute, + primary_rules, + ); + result |= Self::replace_single_rule_node( + context.shared, + CascadeLevel::same_tree_author_important(), + 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, + self.smil_override(), + primary_rules, + ); + } + + if replacements.contains(RestyleHint::RESTYLE_CSS_TRANSITIONS) { + Self::replace_single_rule_node( + context.shared, + CascadeLevel::Transitions, + 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, + 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(), + }; + + // 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_box_style = new_style.get_box(); + let new_style_specifies_animations = new_box_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_box_style = old_style.get_box(); + + 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_box_style.animations_equals(new_box_style) { + return true; + } + + let old_display = old_box_style.clone_display(); + let new_display = new_box_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, + }; + + let new_box_style = new_style.get_box(); + if !self.has_css_transitions(context.shared, pseudo_element) && + !new_box_style.specifies_transitions() + { + return false; + } + + if new_box_style.clone_display().is_none() || 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 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, + 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 == *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>, + ) -> ChildCascadeRequirement { + 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 ChildCascadeRequirement::MustCascadeChildren; + } + + match difference.change { + StyleChange::Unchanged => return ChildCascadeRequirement::CanSkipCascade, + StyleChange::Changed { reset_only } => { + // If inherited properties changed, the best we can do is + // cascade the children. + if !reset_only { + return ChildCascadeRequirement::MustCascadeChildren; + } + }, + } + + let old_display = old_values.get_box().clone_display(); + let new_display = new_values.get_box().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 ChildCascadeRequirement::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 ChildCascadeRequirement::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 ChildCascadeRequirement::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 ChildCascadeRequirement::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 ChildCascadeRequirement::MustCascadeChildren; + } + + if was_legacy_justify_items && old_justify_items.computed != new_justify_items.computed + { + return ChildCascadeRequirement::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 ChildCascadeRequirement::MustCascadeChildren; + } + } + + // We could prove that, if our children don't inherit reset + // properties, we can stop the cascade. + ChildCascadeRequirement::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, + ) -> ChildCascadeRequirement { + 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 cascade_requirement = ChildCascadeRequirement::CanSkipCascade; + if new_primary_style + .flags + .contains(ComputedValueFlags::IS_ROOT_ELEMENT_STYLE) + { + let device = context.shared.stylist.device(); + let new_font_size = new_primary_style.get_font().clone_font_size(); + + if old_styles + .primary + .as_ref() + .map_or(true, |s| s.get_font().clone_font_size() != new_font_size) + { + debug_assert!(self.owner_doc_matches_for_testing(device)); + device.set_root_font_size(new_font_size.size().into()); + // If the root font-size changed since last time, and something + // in the document did use rem units, ensure we recascade the + // entire tree. + if device.used_root_font_size() { + cascade_requirement = ChildCascadeRequirement::MustCascadeDescendants; + } + } + } + + 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 ChildCascadeRequirement::MustCascadeChildren; + } + + // Also, don't do anything if there was no style. + let old_primary_style = match old_styles.primary { + Some(s) => s, + None => return ChildCascadeRequirement::MustCascadeChildren, + }; + + cascade_requirement = cmp::max( + cascade_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 cascade_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 cascade_requirement; + } + }, + } + } + + cascade_requirement + } + + /// Applies selector flags to an element, deferring mutations of the parent + /// until after the traversal. + /// + /// TODO(emilio): This is somewhat inefficient, because it doesn't take + /// advantage of us knowing that the traversal is sequential. + fn apply_selector_flags( + &self, + map: &mut SelectorFlagsMap, + element: &Self, + flags: ElementSelectorFlags, + ) { + // Handle flags that apply to the element. + let self_flags = flags.for_self(); + if !self_flags.is_empty() { + if element == self { + // If this is the element we're styling, we have exclusive + // access to the element, and thus it's fine inserting them, + // even from the worker. + unsafe { + element.set_selector_flags(self_flags); + } + } else { + // Otherwise, this element is an ancestor of the current element + // we're styling, and thus multiple children could write to it + // if we did from here. + // + // Instead, we can read them, and post them if necessary as a + // sequential task in order for them to be processed later. + if !element.has_selector_flags(self_flags) { + map.insert_flags(*element, self_flags); + } + } + } + + // Handle flags that apply to the parent. + let parent_flags = flags.for_parent(); + if !parent_flags.is_empty() { + if let Some(p) = element.parent_element() { + if !p.has_selector_flags(parent_flags) { + map.insert_flags(p, parent_flags); + } + } + } + } + + /// 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_condition.rs b/servo/components/style/media_queries/media_condition.rs new file mode 100644 index 0000000000..f735704556 --- /dev/null +++ b/servo/components/style/media_queries/media_condition.rs @@ -0,0 +1,185 @@ +/* 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 condition: +//! +//! https://drafts.csswg.org/mediaqueries-4/#typedef-media-condition + +use super::{Device, MediaFeatureExpression}; +use crate::context::QuirksMode; +use crate::parser::ParserContext; +use cssparser::{Parser, Token}; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; + +/// A binary `and` or `or` operator. +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, ToCss, ToShmem)] +#[allow(missing_docs)] +pub enum Operator { + And, + Or, +} + +/// Whether to allow an `or` condition or not during parsing. +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToCss)] +enum AllowOr { + Yes, + No, +} + +/// Represents a media condition. +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] +pub enum MediaCondition { + /// A simple media feature expression, implicitly parenthesized. + Feature(MediaFeatureExpression), + /// A negation of a condition. + Not(Box), + /// A set of joint operations. + Operation(Box<[MediaCondition]>, Operator), + /// A condition wrapped in parenthesis. + InParens(Box), +} + +impl ToCss for MediaCondition { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: fmt::Write, + { + match *self { + // NOTE(emilio): MediaFeatureExpression already includes the + // parenthesis. + MediaCondition::Feature(ref f) => f.to_css(dest), + MediaCondition::Not(ref c) => { + dest.write_str("not ")?; + c.to_css(dest) + }, + MediaCondition::InParens(ref c) => { + dest.write_char('(')?; + c.to_css(dest)?; + dest.write_char(')') + }, + MediaCondition::Operation(ref list, op) => { + let mut iter = list.iter(); + iter.next().unwrap().to_css(dest)?; + for item in iter { + dest.write_char(' ')?; + op.to_css(dest)?; + dest.write_char(' ')?; + item.to_css(dest)?; + } + Ok(()) + }, + } + } +} + +impl MediaCondition { + /// Parse a single media condition. + pub fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + Self::parse_internal(context, input, AllowOr::Yes) + } + + /// Parse a single media condition, disallowing `or` expressions. + /// + /// To be used from the legacy media query syntax. + pub fn parse_disallow_or<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + Self::parse_internal(context, input, AllowOr::No) + } + + fn parse_internal<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_or: AllowOr, + ) -> Result> { + let location = input.current_source_location(); + + // FIXME(emilio): This can be cleaner with nll. + let is_negation = match *input.next()? { + Token::ParenthesisBlock => false, + Token::Ident(ref ident) if ident.eq_ignore_ascii_case("not") => true, + ref t => return Err(location.new_unexpected_token_error(t.clone())), + }; + + if is_negation { + let inner_condition = Self::parse_in_parens(context, input)?; + return Ok(MediaCondition::Not(Box::new(inner_condition))); + } + + // ParenthesisBlock. + let first_condition = Self::parse_paren_block(context, input)?; + let operator = match input.try_parse(Operator::parse) { + Ok(op) => op, + Err(..) => return Ok(first_condition), + }; + + if allow_or == AllowOr::No && operator == Operator::Or { + return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + let mut conditions = vec![]; + conditions.push(first_condition); + conditions.push(Self::parse_in_parens(context, input)?); + + let delim = match operator { + Operator::And => "and", + Operator::Or => "or", + }; + + loop { + if input.try_parse(|i| i.expect_ident_matching(delim)).is_err() { + return Ok(MediaCondition::Operation( + conditions.into_boxed_slice(), + operator, + )); + } + + conditions.push(Self::parse_in_parens(context, input)?); + } + } + + /// Parse a media condition in parentheses. + pub fn parse_in_parens<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + input.expect_parenthesis_block()?; + Self::parse_paren_block(context, input) + } + + fn parse_paren_block<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + input.parse_nested_block(|input| { + // Base case. + if let Ok(inner) = input.try_parse(|i| Self::parse(context, i)) { + return Ok(MediaCondition::InParens(Box::new(inner))); + } + let expr = MediaFeatureExpression::parse_in_parenthesis_block(context, input)?; + Ok(MediaCondition::Feature(expr)) + }) + } + + /// Whether this condition matches the device and quirks mode. + pub fn matches(&self, device: &Device, quirks_mode: QuirksMode) -> bool { + match *self { + MediaCondition::Feature(ref f) => f.matches(device, quirks_mode), + MediaCondition::InParens(ref c) => c.matches(device, quirks_mode), + MediaCondition::Not(ref c) => !c.matches(device, quirks_mode), + MediaCondition::Operation(ref conditions, op) => { + let mut iter = conditions.iter(); + match op { + Operator::And => iter.all(|c| c.matches(device, quirks_mode)), + Operator::Or => iter.any(|c| c.matches(device, quirks_mode)), + } + }, + } + } +} diff --git a/servo/components/style/media_queries/media_feature.rs b/servo/components/style/media_queries/media_feature.rs new file mode 100644 index 0000000000..cad695413c --- /dev/null +++ b/servo/components/style/media_queries/media_feature.rs @@ -0,0 +1,180 @@ +/* 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 features. + +use super::media_feature_expression::RangeOrOperator; +use super::Device; +use crate::parser::ParserContext; +use crate::values::computed::position::Ratio; +use crate::values::computed::{CSSPixelLength, Resolution}; +use crate::Atom; +use cssparser::Parser; +use std::fmt; +use style_traits::ParseError; + +/// A generic discriminant for an enum value. +pub type KeywordDiscriminant = u8; + +type MediaFeatureEvaluator = fn( + device: &Device, + // null == no value was given in the query. + value: Option, + range_or_operator: Option, +) -> bool; + +/// Serializes a given discriminant. +/// +/// FIXME(emilio): we could prevent this allocation if the ToCss code would +/// generate a method for keywords to get the static string or something. +pub type KeywordSerializer = fn(KeywordDiscriminant) -> String; + +/// Parses a given identifier. +pub type KeywordParser = for<'a, 'i, 't> fn( + context: &'a ParserContext, + input: &'a mut Parser<'i, 't>, +) -> Result>; + +/// An evaluator for a given media feature. +/// +/// This determines the kind of values that get parsed, too. +#[allow(missing_docs)] +pub enum Evaluator { + Length(MediaFeatureEvaluator), + Integer(MediaFeatureEvaluator), + Float(MediaFeatureEvaluator), + BoolInteger(MediaFeatureEvaluator), + /// A non-negative number ratio, such as the one from device-pixel-ratio. + NumberRatio(MediaFeatureEvaluator), + /// A resolution. + Resolution(MediaFeatureEvaluator), + /// A keyword value. + Enumerated { + /// The parser to get a discriminant given a string. + parser: KeywordParser, + /// The serializer to get a string from a discriminant. + /// + /// This is guaranteed to be called with a keyword that `parser` has + /// produced. + serializer: KeywordSerializer, + /// The evaluator itself. This is guaranteed to be called with a + /// keyword that `parser` has produced. + evaluator: MediaFeatureEvaluator, + }, + Ident(MediaFeatureEvaluator), +} + +/// A simple helper macro to create a keyword evaluator. +/// +/// This assumes that keyword feature expressions don't accept ranges, and +/// asserts if that's not true. As of today there's nothing like that (does that +/// even make sense?). +macro_rules! keyword_evaluator { + ($actual_evaluator:ident, $keyword_type:ty) => {{ + fn __parse<'i, 't>( + context: &$crate::parser::ParserContext, + input: &mut $crate::cssparser::Parser<'i, 't>, + ) -> Result< + $crate::media_queries::media_feature::KeywordDiscriminant, + ::style_traits::ParseError<'i>, + > { + let kw = <$keyword_type as $crate::parser::Parse>::parse(context, input)?; + Ok(kw as $crate::media_queries::media_feature::KeywordDiscriminant) + } + + fn __serialize(kw: $crate::media_queries::media_feature::KeywordDiscriminant) -> String { + // This unwrap is ok because the only discriminants that get + // back to us is the ones that `parse` produces. + let value: $keyword_type = ::num_traits::cast::FromPrimitive::from_u8(kw).unwrap(); + <$keyword_type as ::style_traits::ToCss>::to_css_string(&value) + } + + fn __evaluate( + device: &$crate::media_queries::Device, + value: Option<$crate::media_queries::media_feature::KeywordDiscriminant>, + range_or_operator: Option< + $crate::media_queries::media_feature_expression::RangeOrOperator, + >, + ) -> bool { + debug_assert!( + range_or_operator.is_none(), + "Since when do keywords accept ranges?" + ); + // This unwrap is ok because the only discriminants that get + // back to us is the ones that `parse` produces. + let value: Option<$keyword_type> = + value.map(|kw| ::num_traits::cast::FromPrimitive::from_u8(kw).unwrap()); + $actual_evaluator(device, value) + } + + $crate::media_queries::media_feature::Evaluator::Enumerated { + parser: __parse, + serializer: __serialize, + evaluator: __evaluate, + } + }}; +} + +bitflags! { + /// Different requirements or toggles that change how a expression is + /// parsed. + pub struct ParsingRequirements: u8 { + /// The feature should only be parsed in chrome and ua sheets. + const CHROME_AND_UA_ONLY = 1 << 0; + /// The feature requires a -webkit- prefix. + const WEBKIT_PREFIX = 1 << 1; + } +} + +/// Whether a media feature allows ranges or not. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[allow(missing_docs)] +pub enum AllowsRanges { + Yes, + No, +} + +/// A description of a media feature. +pub struct MediaFeatureDescription { + /// The media feature name, in ascii lowercase. + pub name: Atom, + /// Whether min- / max- prefixes are allowed or not. + pub allows_ranges: AllowsRanges, + /// The evaluator, which we also use to determine which kind of value to + /// parse. + pub evaluator: Evaluator, + /// Different requirements that need to hold for the feature to be + /// successfully parsed. + pub requirements: ParsingRequirements, +} + +impl MediaFeatureDescription { + /// Whether this media feature allows ranges. + #[inline] + pub fn allows_ranges(&self) -> bool { + self.allows_ranges == AllowsRanges::Yes + } +} + +/// A simple helper to construct a `MediaFeatureDescription`. +macro_rules! feature { + ($name:expr, $allows_ranges:expr, $evaluator:expr, $reqs:expr,) => { + $crate::media_queries::media_feature::MediaFeatureDescription { + name: $name, + allows_ranges: $allows_ranges, + evaluator: $evaluator, + requirements: $reqs, + } + }; +} + +impl fmt::Debug for MediaFeatureDescription { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("MediaFeatureExpression") + .field("name", &self.name) + .field("allows_ranges", &self.allows_ranges) + .field("requirements", &self.requirements) + .finish() + } +} diff --git a/servo/components/style/media_queries/media_feature_expression.rs b/servo/components/style/media_queries/media_feature_expression.rs new file mode 100644 index 0000000000..08b13136ef --- /dev/null +++ b/servo/components/style/media_queries/media_feature_expression.rs @@ -0,0 +1,522 @@ +/* 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 for media feature expressions, like `(foo: bar)` or +//! `(width >= 400px)`. + +use super::media_feature::{Evaluator, MediaFeatureDescription}; +use super::media_feature::{KeywordDiscriminant, ParsingRequirements}; +use super::Device; +use crate::context::QuirksMode; +#[cfg(feature = "gecko")] +use crate::gecko::media_features::MEDIA_FEATURES; +use crate::parser::{Parse, ParserContext}; +#[cfg(feature = "servo")] +use crate::servo::media_queries::MEDIA_FEATURES; +use crate::str::{starts_with_ignore_ascii_case, string_as_ascii_lowercase}; +use crate::values::computed::position::Ratio; +use crate::values::computed::{self, ToComputedValue}; +use crate::values::specified::{Integer, Length, Number, Resolution}; +use crate::values::{serialize_atom_identifier, CSSFloat}; +use crate::{Atom, Zero}; +use cssparser::{Parser, Token}; +use std::cmp::{Ordering, PartialOrd}; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; + +/// The kind of matching that should be performed on a media feature value. +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)] +pub enum Range { + /// At least the specified value. + Min, + /// At most the specified value. + Max, +} + +/// The operator that was specified in this media feature. +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)] +pub enum Operator { + /// = + Equal, + /// > + GreaterThan, + /// >= + GreaterThanEqual, + /// < + LessThan, + /// <= + LessThanEqual, +} + +impl ToCss for Operator { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: fmt::Write, + { + dest.write_str(match *self { + Operator::Equal => "=", + Operator::LessThan => "<", + Operator::LessThanEqual => "<=", + Operator::GreaterThan => ">", + Operator::GreaterThanEqual => ">=", + }) + } +} + +/// Either a `Range` or an `Operator`. +/// +/// Ranged media features are not allowed with operations (that'd make no +/// sense). +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)] +pub enum RangeOrOperator { + /// A `Range`. + Range(Range), + /// An `Operator`. + Operator(Operator), +} + +impl RangeOrOperator { + /// Evaluate a given range given an optional query value and a value from + /// the browser. + pub fn evaluate(range_or_op: Option, query_value: Option, value: T) -> bool + where + T: PartialOrd + Zero, + { + match query_value { + Some(v) => Self::evaluate_with_query_value(range_or_op, v, value), + None => !value.is_zero(), + } + } + + /// Evaluate a given range given a non-optional query value and a value from + /// the browser. + pub fn evaluate_with_query_value(range_or_op: Option, query_value: T, value: T) -> bool + where + T: PartialOrd, + { + let cmp = match value.partial_cmp(&query_value) { + Some(c) => c, + None => return false, + }; + + let range_or_op = match range_or_op { + Some(r) => r, + None => return cmp == Ordering::Equal, + }; + + match range_or_op { + RangeOrOperator::Range(range) => { + cmp == Ordering::Equal || + match range { + Range::Min => cmp == Ordering::Greater, + Range::Max => cmp == Ordering::Less, + } + }, + RangeOrOperator::Operator(op) => match op { + Operator::Equal => cmp == Ordering::Equal, + Operator::GreaterThan => cmp == Ordering::Greater, + Operator::GreaterThanEqual => cmp == Ordering::Equal || cmp == Ordering::Greater, + Operator::LessThan => cmp == Ordering::Less, + Operator::LessThanEqual => cmp == Ordering::Equal || cmp == Ordering::Less, + }, + } + } +} + +/// A feature expression contains a reference to the media feature, the value +/// the media query contained, and the range to evaluate. +#[derive(Clone, Debug, MallocSizeOf, ToShmem)] +pub struct MediaFeatureExpression { + feature_index: usize, + value: Option, + range_or_operator: Option, +} + +impl PartialEq for MediaFeatureExpression { + fn eq(&self, other: &Self) -> bool { + self.feature_index == other.feature_index && + self.value == other.value && + self.range_or_operator == other.range_or_operator + } +} + +impl ToCss for MediaFeatureExpression { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: fmt::Write, + { + dest.write_str("(")?; + + let feature = self.feature(); + + if feature + .requirements + .contains(ParsingRequirements::WEBKIT_PREFIX) + { + dest.write_str("-webkit-")?; + } + + if let Some(RangeOrOperator::Range(range)) = self.range_or_operator { + match range { + Range::Min => dest.write_str("min-")?, + Range::Max => dest.write_str("max-")?, + } + } + + // NB: CssStringWriter not needed, feature names are under control. + write!(dest, "{}", feature.name)?; + + if let Some(RangeOrOperator::Operator(op)) = self.range_or_operator { + dest.write_char(' ')?; + op.to_css(dest)?; + dest.write_char(' ')?; + } else if self.value.is_some() { + dest.write_str(": ")?; + } + + if let Some(ref val) = self.value { + val.to_css(dest, self)?; + } + + dest.write_str(")") + } +} + +/// Consumes an operation or a colon, or returns an error. +fn consume_operation_or_colon(input: &mut Parser) -> Result, ()> { + let first_delim = { + let next_token = match input.next() { + Ok(t) => t, + Err(..) => return Err(()), + }; + + match *next_token { + Token::Colon => return Ok(None), + Token::Delim(oper) => oper, + _ => return Err(()), + } + }; + Ok(Some(match first_delim { + '=' => Operator::Equal, + '>' => { + if input.try_parse(|i| i.expect_delim('=')).is_ok() { + Operator::GreaterThanEqual + } else { + Operator::GreaterThan + } + }, + '<' => { + if input.try_parse(|i| i.expect_delim('=')).is_ok() { + Operator::LessThanEqual + } else { + Operator::LessThan + } + }, + _ => return Err(()), + })) +} + +#[allow(unused_variables)] +fn disabled_by_pref(feature: &Atom, context: &ParserContext) -> bool { + #[cfg(feature = "gecko")] + { + if *feature == atom!("forced-colors") { + return !static_prefs::pref!("layout.css.forced-colors.enabled"); + } + // prefers-contrast is always enabled in the ua and chrome. On + // the web it is hidden behind a preference. + if *feature == atom!("prefers-contrast") { + return !context.in_ua_or_chrome_sheet() && + !static_prefs::pref!("layout.css.prefers-contrast.enabled"); + } + } + false +} + +impl MediaFeatureExpression { + fn new( + feature_index: usize, + value: Option, + range_or_operator: Option, + ) -> Self { + debug_assert!(feature_index < MEDIA_FEATURES.len()); + Self { + feature_index, + value, + range_or_operator, + } + } + + fn feature(&self) -> &'static MediaFeatureDescription { + &MEDIA_FEATURES[self.feature_index] + } + + /// Parse a media expression of the form: + /// + /// ``` + /// (media-feature: media-value) + /// ``` + pub fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + input.expect_parenthesis_block()?; + input.parse_nested_block(|input| Self::parse_in_parenthesis_block(context, input)) + } + + /// Parse a media feature expression where we've already consumed the + /// parenthesis. + pub fn parse_in_parenthesis_block<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + let mut requirements = ParsingRequirements::empty(); + let location = input.current_source_location(); + let ident = input.expect_ident()?; + + if context.in_ua_or_chrome_sheet() { + requirements.insert(ParsingRequirements::CHROME_AND_UA_ONLY); + } + + let mut feature_name = &**ident; + + if starts_with_ignore_ascii_case(feature_name, "-webkit-") { + feature_name = &feature_name[8..]; + requirements.insert(ParsingRequirements::WEBKIT_PREFIX); + } + + let range = if starts_with_ignore_ascii_case(feature_name, "min-") { + feature_name = &feature_name[4..]; + Some(Range::Min) + } else if starts_with_ignore_ascii_case(feature_name, "max-") { + feature_name = &feature_name[4..]; + Some(Range::Max) + } else { + None + }; + + let atom = Atom::from(string_as_ascii_lowercase(feature_name)); + + let (feature_index, feature) = match MEDIA_FEATURES + .iter() + .enumerate() + .find(|(_, f)| f.name == atom) + { + Some((i, f)) => (i, f), + None => { + return Err(location.new_custom_error( + StyleParseErrorKind::MediaQueryExpectedFeatureName(ident.clone()), + )) + }, + }; + + if disabled_by_pref(&feature.name, context) || + !requirements.contains(feature.requirements) || + (range.is_some() && !feature.allows_ranges()) + { + return Err(location.new_custom_error( + StyleParseErrorKind::MediaQueryExpectedFeatureName(ident.clone()), + )); + } + + let operator = input.try_parse(consume_operation_or_colon); + let operator = match operator { + Err(..) => { + // If there's no colon, this is a media query of the + // form '()', that is, there's no value + // specified. + // + // Gecko doesn't allow ranged expressions without a + // value, so just reject them here too. + if range.is_some() { + return Err( + input.new_custom_error(StyleParseErrorKind::RangedExpressionWithNoValue) + ); + } + + return Ok(Self::new(feature_index, None, None)); + }, + Ok(operator) => operator, + }; + + let range_or_operator = match range { + Some(range) => { + if operator.is_some() { + return Err( + input.new_custom_error(StyleParseErrorKind::MediaQueryUnexpectedOperator) + ); + } + Some(RangeOrOperator::Range(range)) + }, + None => match operator { + Some(operator) => { + if !feature.allows_ranges() { + return Err(input + .new_custom_error(StyleParseErrorKind::MediaQueryUnexpectedOperator)); + } + Some(RangeOrOperator::Operator(operator)) + }, + None => None, + }, + }; + + let value = MediaExpressionValue::parse(feature, context, input).map_err(|err| { + err.location + .new_custom_error(StyleParseErrorKind::MediaQueryExpectedFeatureValue) + })?; + + Ok(Self::new(feature_index, Some(value), range_or_operator)) + } + + /// Returns whether this media query evaluates to true for the given device. + pub fn matches(&self, device: &Device, quirks_mode: QuirksMode) -> bool { + let value = self.value.as_ref(); + + macro_rules! expect { + ($variant:ident) => { + value.map(|value| match *value { + MediaExpressionValue::$variant(ref v) => v, + _ => unreachable!("Unexpected MediaExpressionValue"), + }) + }; + } + + match self.feature().evaluator { + Evaluator::Length(eval) => { + let computed = expect!(Length).map(|specified| { + computed::Context::for_media_query_evaluation(device, quirks_mode, |context| { + specified.to_computed_value(context) + }) + }); + eval(device, computed, self.range_or_operator) + }, + Evaluator::Integer(eval) => { + eval(device, expect!(Integer).cloned(), self.range_or_operator) + }, + Evaluator::Float(eval) => eval(device, expect!(Float).cloned(), self.range_or_operator), + Evaluator::NumberRatio(eval) => eval( + device, + expect!(NumberRatio).cloned(), + self.range_or_operator, + ), + Evaluator::Resolution(eval) => { + let computed = expect!(Resolution).map(|specified| { + computed::Context::for_media_query_evaluation(device, quirks_mode, |context| { + specified.to_computed_value(context) + }) + }); + eval(device, computed, self.range_or_operator) + }, + Evaluator::Enumerated { evaluator, .. } => { + evaluator(device, expect!(Enumerated).cloned(), self.range_or_operator) + }, + Evaluator::Ident(eval) => eval(device, expect!(Ident).cloned(), self.range_or_operator), + Evaluator::BoolInteger(eval) => eval( + device, + expect!(BoolInteger).cloned(), + self.range_or_operator, + ), + } + } +} + +/// A value found or expected in a media expression. +/// +/// FIXME(emilio): How should calc() serialize in the Number / Integer / +/// BoolInteger / NumberRatio case, as computed or as specified value? +/// +/// If the first, this would need to store the relevant values. +/// +/// See: https://github.com/w3c/csswg-drafts/issues/1968 +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] +pub enum MediaExpressionValue { + /// A length. + Length(Length), + /// A (non-negative) integer. + Integer(u32), + /// A floating point value. + Float(CSSFloat), + /// A boolean value, specified as an integer (i.e., either 0 or 1). + BoolInteger(bool), + /// A single non-negative number or two non-negative numbers separated by '/', + /// with optional whitespace on either side of the '/'. + NumberRatio(Ratio), + /// A resolution. + Resolution(Resolution), + /// An enumerated value, defined by the variant keyword table in the + /// feature's `mData` member. + Enumerated(KeywordDiscriminant), + /// An identifier. + Ident(Atom), +} + +impl MediaExpressionValue { + fn to_css(&self, dest: &mut CssWriter, for_expr: &MediaFeatureExpression) -> fmt::Result + where + W: fmt::Write, + { + match *self { + MediaExpressionValue::Length(ref l) => l.to_css(dest), + MediaExpressionValue::Integer(v) => v.to_css(dest), + MediaExpressionValue::Float(v) => v.to_css(dest), + MediaExpressionValue::BoolInteger(v) => dest.write_str(if v { "1" } else { "0" }), + MediaExpressionValue::NumberRatio(ratio) => ratio.to_css(dest), + MediaExpressionValue::Resolution(ref r) => r.to_css(dest), + MediaExpressionValue::Ident(ref ident) => serialize_atom_identifier(ident, dest), + MediaExpressionValue::Enumerated(value) => match for_expr.feature().evaluator { + Evaluator::Enumerated { serializer, .. } => dest.write_str(&*serializer(value)), + _ => unreachable!(), + }, + } + } + + fn parse<'i, 't>( + for_feature: &MediaFeatureDescription, + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + Ok(match for_feature.evaluator { + Evaluator::Length(..) => { + let length = Length::parse_non_negative(context, input)?; + MediaExpressionValue::Length(length) + }, + Evaluator::Integer(..) => { + let integer = Integer::parse_non_negative(context, input)?; + MediaExpressionValue::Integer(integer.value() as u32) + }, + Evaluator::BoolInteger(..) => { + let integer = Integer::parse_non_negative(context, input)?; + let value = integer.value(); + if value > 1 { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + MediaExpressionValue::BoolInteger(value == 1) + }, + Evaluator::Float(..) => { + let number = Number::parse(context, input)?; + MediaExpressionValue::Float(number.get()) + }, + Evaluator::NumberRatio(..) => { + use crate::values::generics::position::Ratio as GenericRatio; + use crate::values::generics::NonNegative; + use crate::values::specified::position::Ratio; + + let ratio = Ratio::parse(context, input)?; + MediaExpressionValue::NumberRatio(GenericRatio( + NonNegative(ratio.0.get()), + NonNegative(ratio.1.get()), + )) + }, + Evaluator::Resolution(..) => { + MediaExpressionValue::Resolution(Resolution::parse(context, input)?) + }, + Evaluator::Enumerated { parser, .. } => { + MediaExpressionValue::Enumerated(parser(context, input)?) + }, + Evaluator::Ident(..) => { + MediaExpressionValue::Ident(Atom::from(input.expect_ident()?.as_ref())) + }, + }) + } +} 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..5d150f7db9 --- /dev/null +++ b/servo/components/style/media_queries/media_list.rs @@ -0,0 +1,138 @@ +/* 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 cssparser::{Delimiter, Parser}; +use cssparser::{ParserInput, Token}; + +/// A type that encapsulates a media query list. +#[css(comma, derive_debug)] +#[derive(Clone, MallocSizeOf, ToCss, ToShmem)] +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 + self.media_queries.is_empty() || + self.media_queries.iter().any(|mq| { + let media_match = mq.media_type.matches(device.media_type()); + + // Check if the media condition match. + let query_match = media_match && + mq.condition + .as_ref() + .map_or(true, |c| c.matches(device, quirks_mode)); + + // Apply the logical NOT qualifier to the result + match mq.qualifier { + Some(Qualifier::Not) => !query_match, + _ => query_match, + } + }) + } + + /// Whether this `MediaList` contains no media queries. + pub fn is_empty(&self) -> bool { + self.media_queries.is_empty() + } + + /// 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..83e34c5037 --- /dev/null +++ b/servo/components/style/media_queries/media_query.rs @@ -0,0 +1,180 @@ +/* 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 super::media_condition::MediaCondition; +use crate::parser::ParserContext; +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 not, or, and, and only. + // + // 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" => 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, + } + } + + /// 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(MediaCondition::parse(context, input)?) + } else if input.try_parse(|i| i.expect_ident_matching("and")).is_ok() { + Some(MediaCondition::parse_disallow_or(context, input)?) + } 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..ca38585748 --- /dev/null +++ b/servo/components/style/media_queries/mod.rs @@ -0,0 +1,24 @@ +/* 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_condition; +mod media_list; +mod media_query; +#[macro_use] +pub mod media_feature; +pub mod media_feature_expression; + +pub use self::media_condition::MediaCondition; +pub use self::media_feature_expression::MediaFeatureExpression; +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..9e578e9e9e --- /dev/null +++ b/servo/components/style/parallel.rs @@ -0,0 +1,296 @@ +/* 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 arrayvec::ArrayVec; +use itertools::Itertools; +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; + +/// The maximum number of child nodes that we will process as a single unit. +/// +/// Larger values will increase style sharing cache hits and general DOM +/// locality at the expense of decreased opportunities for parallelism. There +/// are some measurements in +/// https://bugzilla.mozilla.org/show_bug.cgi?id=1385982#c11 and comments 12 +/// and 13 that investigate some slightly different values for the work unit +/// size. If the size is significantly increased, make sure to adjust the +/// condition for kicking off a new work unit in top_down_dom, because +/// otherwise we're likely to end up doing too much work serially. For +/// example, the condition there could become some fraction of WORK_UNIT_MAX +/// instead of WORK_UNIT_MAX. +pub const WORK_UNIT_MAX: usize = 16; + +/// A set of nodes, sized to the work unit. This gets copied when sent to other +/// threads, so we keep it compact. +type WorkUnit = ArrayVec<[SendNode; WORK_UNIT_MAX]>; + +/// 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, D>( + traversal: &'scope D, + slot: &mut Option>, +) where + E: TElement + 'scope, + D: DomTraversal, +{ + *slot = Some(ThreadLocalStyleContext::new(traversal.shared_context())); +} + +/// 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, +{ + 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(traversal, 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.drain(..), + DispatchMode::NotTailCall, + recursion_ok, + root, + traversal_data_copy, + scope, + pool, + traversal, + tls, + ); + } + + 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.drain(..), + 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, I>( + nodes: I, + 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, + I: ExactSizeIterator>, +{ + 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(); + + // In the common case, our children fit within a single work unit, in which + // case we can pass the SmallVec directly and avoid extra allocation. + if nodes.len() <= WORK_UNIT_MAX { + let work: WorkUnit = nodes.collect(); + if may_dispatch_tail { + top_down_dom(&work, root, traversal_data, scope, pool, traversal, tls); + } else { + scope.spawn_fifo(move |scope| { + profiler_label!(Style); + let work = work; + top_down_dom(&work, root, traversal_data, scope, pool, traversal, tls); + }); + } + } else { + for chunk in nodes.chunks(WORK_UNIT_MAX).into_iter() { + let nodes: WorkUnit = chunk.collect(); + let traversal_data_copy = traversal_data.clone(); + scope.spawn_fifo(move |scope| { + profiler_label!(Style); + let n = nodes; + top_down_dom(&*n, 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..837b974002 --- /dev/null +++ b/servo/components/style/parser.rs @@ -0,0 +1,217 @@ +/* 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, Namespaces, Origin, UrlExtraData}; +use crate::use_counters::UseCounters; +use cssparser::{Parser, SourceLocation, UnicodeRange}; +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 type, if any. + pub rule_type: Option, + /// 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: Option<&'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, + error_reporter: Option<&'a dyn ParseErrorReporter>, + use_counters: Option<&'a UseCounters>, + ) -> Self { + Self { + stylesheet_origin, + url_data, + rule_type, + parsing_mode, + quirks_mode, + error_reporter, + namespaces: None, + use_counters, + } + } + + /// Create a parser context based on a previous context, but with a modified + /// rule type. + #[inline] + pub fn new_with_rule_type( + context: &'a ParserContext, + rule_type: CssRuleType, + namespaces: &'a Namespaces, + ) -> ParserContext<'a> { + Self { + stylesheet_origin: context.stylesheet_origin, + url_data: context.url_data, + rule_type: Some(rule_type), + parsing_mode: context.parsing_mode, + quirks_mode: context.quirks_mode, + namespaces: Some(namespaces), + error_reporter: context.error_reporter, + use_counters: context.use_counters, + } + } + + /// Whether we're in a @page rule. + #[inline] + pub fn in_page_rule(&self) -> bool { + self.rule_type + .map_or(false, |rule_type| rule_type == CssRuleType::Page) + } + + /// Get the rule type, which assumes that one is available. + pub fn rule_type(&self) -> CssRuleType { + self.rule_type + .expect("Rule type expected, but none was found.") + } + + /// 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/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..e74c9b9795 --- /dev/null +++ b/servo/components/style/properties/build.py @@ -0,0 +1,175 @@ +# 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", + "padding", + "position", + "svg", + "table", + "text", + "ui", + "xul", +] + + +def main(): + usage = ( + "Usage: %s [ servo-2013 | servo-2020 | gecko ] [ style-crate | geckolib