From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- servo/components/derive_common/Cargo.toml | 16 + servo/components/derive_common/cg.rs | 396 + servo/components/derive_common/lib.rs | 13 + servo/components/malloc_size_of/Cargo.toml | 53 + servo/components/malloc_size_of/LICENSE-APACHE | 201 + servo/components/malloc_size_of/LICENSE-MIT | 23 + servo/components/malloc_size_of/lib.rs | 1003 +++ servo/components/selectors/CHANGES.md | 1 + servo/components/selectors/Cargo.toml | 35 + servo/components/selectors/README.md | 25 + servo/components/selectors/attr.rs | 183 + servo/components/selectors/bloom.rs | 422 + servo/components/selectors/build.rs | 77 + servo/components/selectors/builder.rs | 391 + servo/components/selectors/context.rs | 418 + servo/components/selectors/lib.rs | 41 + servo/components/selectors/matching.rs | 1370 +++ servo/components/selectors/nth_index_cache.rs | 102 + servo/components/selectors/parser.rs | 4483 ++++++++++ .../selectors/relative_selector/cache.rs | 81 + .../selectors/relative_selector/filter.rs | 159 + .../components/selectors/relative_selector/mod.rs | 6 + servo/components/selectors/sink.rs | 31 + servo/components/selectors/tree.rs | 168 + servo/components/selectors/visitor.rs | 136 + servo/components/servo_arc/Cargo.toml | 19 + servo/components/servo_arc/lib.rs | 1195 +++ servo/components/style/Cargo.toml | 91 + servo/components/style/README.md | 6 + servo/components/style/animation.rs | 1415 ++++ servo/components/style/applicable_declarations.rs | 215 + servo/components/style/attr.rs | 599 ++ servo/components/style/author_styles.rs | 70 + servo/components/style/bezier.rs | 176 + servo/components/style/bloom.rs | 401 + servo/components/style/build.rs | 91 + servo/components/style/build_gecko.rs | 400 + servo/components/style/color/convert.rs | 902 ++ servo/components/style/color/mix.rs | 558 ++ servo/components/style/color/mod.rs | 613 ++ servo/components/style/color/parsing.rs | 1246 +++ servo/components/style/context.rs | 698 ++ servo/components/style/counter_style/mod.rs | 695 ++ servo/components/style/counter_style/predefined.rs | 61 + .../style/counter_style/update_predefined.py | 35 + servo/components/style/custom_properties.rs | 1959 +++++ servo/components/style/custom_properties_map.rs | 237 + servo/components/style/data.rs | 545 ++ servo/components/style/dom.rs | 951 +++ servo/components/style/dom_apis.rs | 814 ++ servo/components/style/driver.rs | 164 + servo/components/style/encoding_support.rs | 105 + servo/components/style/error_reporting.rs | 454 + servo/components/style/font_face.rs | 807 ++ servo/components/style/font_metrics.rs | 58 + servo/components/style/gecko/arc_types.rs | 171 + servo/components/style/gecko/conversions.rs | 59 + servo/components/style/gecko/data.rs | 198 + servo/components/style/gecko/media_features.rs | 1003 +++ servo/components/style/gecko/media_queries.rs | 593 ++ servo/components/style/gecko/mod.rs | 23 + .../style/gecko/non_ts_pseudo_class_list.rs | 106 + servo/components/style/gecko/pseudo_element.rs | 233 + .../style/gecko/pseudo_element_definition.mako.rs | 278 + servo/components/style/gecko/regen_atoms.py | 218 + servo/components/style/gecko/restyle_damage.rs | 121 + servo/components/style/gecko/selector_parser.rs | 519 ++ servo/components/style/gecko/snapshot.rs | 174 + servo/components/style/gecko/snapshot_helpers.rs | 316 + servo/components/style/gecko/traversal.rs | 53 + servo/components/style/gecko/url.rs | 384 + servo/components/style/gecko/values.rs | 77 + servo/components/style/gecko/wrapper.rs | 2211 +++++ servo/components/style/gecko_bindings/mod.rs | 28 + servo/components/style/gecko_bindings/sugar/mod.rs | 13 + .../style/gecko_bindings/sugar/ns_com_ptr.rs | 15 + .../style/gecko_bindings/sugar/ns_compatibility.rs | 19 + .../gecko_bindings/sugar/ns_style_auto_array.rs | 111 + .../style/gecko_bindings/sugar/ns_t_array.rs | 144 + .../style/gecko_bindings/sugar/origin_flags.rs | 31 + .../style/gecko_bindings/sugar/ownership.rs | 61 + .../style/gecko_bindings/sugar/refptr.rs | 289 + servo/components/style/gecko_string_cache/mod.rs | 497 ++ .../style/gecko_string_cache/namespace.rs | 105 + servo/components/style/global_style_data.rs | 212 + .../style/invalidation/element/document_state.rs | 154 + .../style/invalidation/element/element_wrapper.rs | 388 + .../style/invalidation/element/invalidation_map.rs | 1425 ++++ .../style/invalidation/element/invalidator.rs | 1130 +++ servo/components/style/invalidation/element/mod.rs | 13 + .../invalidation/element/relative_selector.rs | 1164 +++ .../style/invalidation/element/restyle_hints.rs | 191 + .../invalidation/element/state_and_attributes.rs | 601 ++ .../components/style/invalidation/media_queries.rs | 130 + servo/components/style/invalidation/mod.rs | 10 + servo/components/style/invalidation/stylesheets.rs | 651 ++ .../style/invalidation/viewport_units.rs | 71 + servo/components/style/lib.rs | 332 + servo/components/style/logical_geometry.rs | 1629 ++++ servo/components/style/macros.rs | 98 + servo/components/style/matching.rs | 1128 +++ servo/components/style/media_queries/media_list.rs | 150 + .../components/style/media_queries/media_query.rs | 193 + servo/components/style/media_queries/mod.rs | 18 + servo/components/style/parallel.rs | 194 + servo/components/style/parser.rs | 178 + servo/components/style/piecewise_linear.rs | 281 + .../properties/Mako-1.1.2-py2.py3-none-any.whl | Bin 0 -> 75521 bytes servo/components/style/properties/build.py | 176 + servo/components/style/properties/cascade.rs | 1381 +++ .../style/properties/computed_value_flags.rs | 194 + .../style/properties/counted_unknown_properties.py | 110 + servo/components/style/properties/data.py | 1083 +++ .../style/properties/declaration_block.rs | 1642 ++++ servo/components/style/properties/gecko.mako.rs | 1806 ++++ servo/components/style/properties/helpers.mako.rs | 909 ++ .../properties/helpers/animated_properties.mako.rs | 785 ++ .../style/properties/longhands/background.mako.rs | 126 + .../style/properties/longhands/border.mako.rs | 170 + .../style/properties/longhands/box.mako.rs | 644 ++ .../style/properties/longhands/column.mako.rs | 90 + .../style/properties/longhands/counters.mako.rs | 52 + .../style/properties/longhands/effects.mako.rs | 92 + .../style/properties/longhands/font.mako.rs | 505 ++ .../properties/longhands/inherited_box.mako.rs | 105 + .../properties/longhands/inherited_svg.mako.rs | 239 + .../properties/longhands/inherited_table.mako.rs | 53 + .../properties/longhands/inherited_text.mako.rs | 414 + .../properties/longhands/inherited_ui.mako.rs | 135 + .../style/properties/longhands/list.mako.rs | 80 + .../style/properties/longhands/margin.mako.rs | 55 + .../style/properties/longhands/outline.mako.rs | 57 + .../style/properties/longhands/padding.mako.rs | 43 + .../style/properties/longhands/page.mako.rs | 44 + .../style/properties/longhands/position.mako.rs | 485 ++ .../style/properties/longhands/svg.mako.rs | 282 + .../style/properties/longhands/table.mako.rs | 30 + .../style/properties/longhands/text.mako.rs | 88 + .../style/properties/longhands/ui.mako.rs | 422 + .../style/properties/longhands/xul.mako.rs | 85 + servo/components/style/properties/mod.rs | 1531 ++++ .../style/properties/properties.html.mako | 31 + .../components/style/properties/properties.mako.rs | 2958 +++++++ .../style/properties/shorthands/background.mako.rs | 289 + .../style/properties/shorthands/border.mako.rs | 491 ++ .../style/properties/shorthands/box.mako.rs | 253 + .../style/properties/shorthands/column.mako.rs | 115 + .../style/properties/shorthands/font.mako.rs | 542 ++ .../properties/shorthands/inherited_svg.mako.rs | 38 + .../properties/shorthands/inherited_text.mako.rs | 254 + .../style/properties/shorthands/list.mako.rs | 137 + .../style/properties/shorthands/margin.mako.rs | 60 + .../style/properties/shorthands/outline.mako.rs | 80 + .../style/properties/shorthands/padding.mako.rs | 58 + .../style/properties/shorthands/position.mako.rs | 891 ++ .../style/properties/shorthands/svg.mako.rs | 287 + .../style/properties/shorthands/text.mako.rs | 120 + .../style/properties/shorthands/ui.mako.rs | 444 + .../components/style/properties_and_values/mod.rs | 12 + .../style/properties_and_values/registry.rs | 104 + .../components/style/properties_and_values/rule.rs | 348 + .../style/properties_and_values/syntax/ascii.rs | 60 + .../properties_and_values/syntax/data_type.rs | 134 + .../style/properties_and_values/syntax/mod.rs | 392 + .../style/properties_and_values/value.rs | 626 ++ servo/components/style/queries/condition.rs | 366 + servo/components/style/queries/feature.rs | 198 + .../components/style/queries/feature_expression.rs | 764 ++ servo/components/style/queries/mod.rs | 19 + servo/components/style/queries/values.rs | 36 + servo/components/style/rule_cache.rs | 219 + servo/components/style/rule_collector.rs | 505 ++ servo/components/style/rule_tree/core.rs | 772 ++ servo/components/style/rule_tree/level.rs | 249 + servo/components/style/rule_tree/map.rs | 201 + servo/components/style/rule_tree/mod.rs | 403 + servo/components/style/rule_tree/source.rs | 75 + servo/components/style/rule_tree/unsafe_box.rs | 74 + servo/components/style/scoped_tls.rs | 81 + servo/components/style/selector_map.rs | 870 ++ servo/components/style/selector_parser.rs | 240 + servo/components/style/servo/media_queries.rs | 226 + servo/components/style/servo/mod.rs | 12 + servo/components/style/servo/restyle_damage.rs | 268 + servo/components/style/servo/selector_parser.rs | 806 ++ servo/components/style/servo/url.rs | 238 + servo/components/style/shared_lock.rs | 374 + servo/components/style/sharing/checks.rs | 166 + servo/components/style/sharing/mod.rs | 923 ++ servo/components/style/str.rs | 181 + servo/components/style/style_adjuster.rs | 1009 +++ servo/components/style/style_resolver.rs | 585 ++ servo/components/style/stylesheet_set.rs | 705 ++ .../components/style/stylesheets/container_rule.rs | 642 ++ .../style/stylesheets/counter_style_rule.rs | 7 + .../components/style/stylesheets/document_rule.rs | 299 + .../components/style/stylesheets/font_face_rule.rs | 7 + .../style/stylesheets/font_feature_values_rule.rs | 490 ++ .../style/stylesheets/font_palette_values_rule.rs | 264 + servo/components/style/stylesheets/import_rule.rs | 301 + .../components/style/stylesheets/keyframes_rule.rs | 690 ++ servo/components/style/stylesheets/layer_rule.rs | 228 + servo/components/style/stylesheets/loader.rs | 31 + servo/components/style/stylesheets/margin_rule.rs | 167 + servo/components/style/stylesheets/media_rule.rs | 71 + servo/components/style/stylesheets/mod.rs | 597 ++ .../components/style/stylesheets/namespace_rule.rs | 43 + servo/components/style/stylesheets/origin.rs | 248 + servo/components/style/stylesheets/page_rule.rs | 366 + .../components/style/stylesheets/property_rule.rs | 5 + servo/components/style/stylesheets/rule_list.rs | 189 + servo/components/style/stylesheets/rule_parser.rs | 982 +++ .../components/style/stylesheets/rules_iterator.rs | 331 + servo/components/style/stylesheets/style_rule.rs | 104 + servo/components/style/stylesheets/stylesheet.rs | 566 ++ .../components/style/stylesheets/supports_rule.rs | 397 + servo/components/style/stylist.rs | 3503 ++++++++ servo/components/style/thread_state.rs | 98 + servo/components/style/traversal.rs | 842 ++ servo/components/style/traversal_flags.rs | 68 + servo/components/style/use_counters/mod.rs | 96 + servo/components/style/values/animated/color.rs | 88 + servo/components/style/values/animated/effects.rs | 27 + servo/components/style/values/animated/font.rs | 37 + servo/components/style/values/animated/grid.rs | 165 + servo/components/style/values/animated/lists.rs | 141 + servo/components/style/values/animated/mod.rs | 487 ++ servo/components/style/values/animated/svg.rs | 46 + .../components/style/values/animated/transform.rs | 1667 ++++ servo/components/style/values/computed/align.rs | 91 + servo/components/style/values/computed/angle.rs | 101 + .../components/style/values/computed/animation.rs | 70 + .../components/style/values/computed/background.rs | 13 + .../style/values/computed/basic_shape.rs | 37 + servo/components/style/values/computed/border.rs | 84 + servo/components/style/values/computed/box.rs | 388 + servo/components/style/values/computed/color.rs | 95 + servo/components/style/values/computed/column.rs | 11 + servo/components/style/values/computed/counters.rs | 26 + servo/components/style/values/computed/easing.rs | 109 + servo/components/style/values/computed/effects.rs | 44 + servo/components/style/values/computed/flex.rs | 19 + servo/components/style/values/computed/font.rs | 1369 +++ servo/components/style/values/computed/image.rs | 205 + servo/components/style/values/computed/length.rs | 531 ++ .../style/values/computed/length_percentage.rs | 1055 +++ servo/components/style/values/computed/list.rs | 17 + servo/components/style/values/computed/mod.rs | 1035 +++ servo/components/style/values/computed/motion.rs | 70 + servo/components/style/values/computed/outline.rs | 7 + servo/components/style/values/computed/page.rs | 75 + .../components/style/values/computed/percentage.rs | 136 + servo/components/style/values/computed/position.rs | 74 + servo/components/style/values/computed/ratio.rs | 115 + servo/components/style/values/computed/rect.rs | 11 + .../components/style/values/computed/resolution.rs | 56 + servo/components/style/values/computed/svg.rs | 66 + servo/components/style/values/computed/table.rs | 7 + servo/components/style/values/computed/text.rs | 228 + servo/components/style/values/computed/time.rs | 45 + .../components/style/values/computed/transform.rs | 559 ++ servo/components/style/values/computed/ui.rs | 21 + servo/components/style/values/computed/url.rs | 15 + servo/components/style/values/distance.rs | 138 + .../components/style/values/generics/animation.rs | 140 + .../components/style/values/generics/background.rs | 54 + .../style/values/generics/basic_shape.rs | 567 ++ servo/components/style/values/generics/border.rs | 261 + servo/components/style/values/generics/box.rs | 211 + servo/components/style/values/generics/calc.rs | 1820 ++++ servo/components/style/values/generics/color.rs | 209 + servo/components/style/values/generics/column.rs | 45 + servo/components/style/values/generics/counters.rs | 295 + servo/components/style/values/generics/easing.rs | 143 + servo/components/style/values/generics/effects.rs | 121 + servo/components/style/values/generics/flex.rs | 33 + servo/components/style/values/generics/font.rs | 316 + servo/components/style/values/generics/grid.rs | 867 ++ servo/components/style/values/generics/image.rs | 631 ++ servo/components/style/values/generics/length.rs | 304 + servo/components/style/values/generics/mod.rs | 388 + servo/components/style/values/generics/motion.rs | 270 + servo/components/style/values/generics/page.rs | 162 + servo/components/style/values/generics/position.rs | 238 + servo/components/style/values/generics/ratio.rs | 50 + servo/components/style/values/generics/rect.rs | 146 + servo/components/style/values/generics/size.rs | 101 + servo/components/style/values/generics/svg.rs | 221 + servo/components/style/values/generics/text.rs | 148 + .../components/style/values/generics/transform.rs | 879 ++ servo/components/style/values/generics/ui.rs | 129 + servo/components/style/values/generics/url.rs | 47 + servo/components/style/values/mod.rs | 796 ++ servo/components/style/values/resolved/color.rs | 48 + servo/components/style/values/resolved/counters.rs | 51 + servo/components/style/values/resolved/mod.rs | 275 + servo/components/style/values/specified/align.rs | 820 ++ servo/components/style/values/specified/angle.rs | 276 + .../components/style/values/specified/animation.rs | 463 + .../style/values/specified/background.rs | 143 + .../style/values/specified/basic_shape.rs | 719 ++ servo/components/style/values/specified/border.rs | 398 + servo/components/style/values/specified/box.rs | 1945 +++++ servo/components/style/values/specified/calc.rs | 1086 +++ servo/components/style/values/specified/color.rs | 1175 +++ servo/components/style/values/specified/column.rs | 11 + .../components/style/values/specified/counters.rs | 279 + servo/components/style/values/specified/easing.rs | 192 + servo/components/style/values/specified/effects.rs | 453 + servo/components/style/values/specified/flex.rs | 25 + servo/components/style/values/specified/font.rs | 2222 +++++ servo/components/style/values/specified/gecko.rs | 82 + servo/components/style/values/specified/grid.rs | 441 + servo/components/style/values/specified/image.rs | 1340 +++ servo/components/style/values/specified/length.rs | 2031 +++++ servo/components/style/values/specified/list.rs | 202 + servo/components/style/values/specified/mod.rs | 992 +++ servo/components/style/values/specified/motion.rs | 343 + servo/components/style/values/specified/outline.rs | 71 + servo/components/style/values/specified/page.rs | 99 + .../style/values/specified/percentage.rs | 225 + .../components/style/values/specified/position.rs | 955 +++ servo/components/style/values/specified/ratio.rs | 32 + servo/components/style/values/specified/rect.rs | 11 + .../style/values/specified/resolution.rs | 141 + .../style/values/specified/source_size_list.rs | 136 + servo/components/style/values/specified/svg.rs | 404 + .../components/style/values/specified/svg_path.rs | 1029 +++ servo/components/style/values/specified/table.rs | 36 + servo/components/style/values/specified/text.rs | 1193 +++ servo/components/style/values/specified/time.rs | 183 + .../components/style/values/specified/transform.rs | 530 ++ servo/components/style/values/specified/ui.rs | 257 + servo/components/style/values/specified/url.rs | 15 + servo/components/style_derive/Cargo.toml | 18 + servo/components/style_derive/animate.rs | 135 + .../style_derive/compute_squared_distance.rs | 125 + servo/components/style_derive/lib.rs | 82 + servo/components/style_derive/parse.rs | 323 + .../style_derive/specified_value_info.rs | 195 + servo/components/style_derive/to_animated_value.rs | 35 + servo/components/style_derive/to_animated_zero.rs | 65 + servo/components/style_derive/to_computed_value.rs | 205 + servo/components/style_derive/to_css.rs | 396 + servo/components/style_derive/to_resolved_value.rs | 52 + servo/components/style_traits/Cargo.toml | 32 + servo/components/style_traits/arc_slice.rs | 162 + servo/components/style_traits/dom.rs | 26 + servo/components/style_traits/lib.rs | 295 + servo/components/style_traits/owned_slice.rs | 198 + servo/components/style_traits/owned_str.rs | 81 + .../style_traits/specified_value_info.rs | 138 + servo/components/style_traits/values.rs | 569 ++ servo/components/to_shmem/Cargo.toml | 22 + servo/components/to_shmem/lib.rs | 618 ++ servo/components/to_shmem_derive/Cargo.toml | 18 + servo/components/to_shmem_derive/lib.rs | 26 + servo/components/to_shmem_derive/to_shmem.rs | 78 + servo/moz.build | 29 + servo/ports/geckolib/Cargo.toml | 33 + servo/ports/geckolib/cbindgen.toml | 1076 +++ servo/ports/geckolib/error_reporter.rs | 514 ++ servo/ports/geckolib/glue.rs | 8921 ++++++++++++++++++++ servo/ports/geckolib/lib.rs | 36 + servo/ports/geckolib/stylesheet_loader.rs | 200 + servo/ports/geckolib/tests/Cargo.toml | 14 + servo/ports/geckolib/tests/lib.rs | 17 + servo/ports/geckolib/tests/piecewise_linear.rs | 345 + servo/rustfmt.toml | 3 + servo/tests/unit/malloc_size_of/Cargo.toml | 13 + servo/tests/unit/malloc_size_of/lib.rs | 105 + servo/tests/unit/style/Cargo.toml | 30 + servo/tests/unit/style/animated_properties.rs | 225 + servo/tests/unit/style/attr.rs | 94 + servo/tests/unit/style/custom_properties.rs | 49 + servo/tests/unit/style/lib.rs | 35 + servo/tests/unit/style/logical_geometry.rs | 81 + servo/tests/unit/style/media_queries.rs | 578 ++ servo/tests/unit/style/parsing/animation.rs | 21 + servo/tests/unit/style/parsing/background.rs | 233 + servo/tests/unit/style/parsing/border.rs | 219 + servo/tests/unit/style/parsing/box_.rs | 22 + servo/tests/unit/style/parsing/column.rs | 30 + servo/tests/unit/style/parsing/effects.rs | 99 + servo/tests/unit/style/parsing/image.rs | 154 + servo/tests/unit/style/parsing/inherited_text.rs | 46 + servo/tests/unit/style/parsing/mod.rs | 150 + servo/tests/unit/style/parsing/outline.rs | 26 + servo/tests/unit/style/parsing/position.rs | 145 + servo/tests/unit/style/parsing/selectors.rs | 35 + servo/tests/unit/style/parsing/supports.rs | 19 + servo/tests/unit/style/parsing/text_overflow.rs | 30 + .../unit/style/parsing/transition_duration.rs | 17 + .../style/parsing/transition_timing_function.rs | 62 + servo/tests/unit/style/properties/mod.rs | 70 + servo/tests/unit/style/properties/scaffolding.rs | 85 + servo/tests/unit/style/properties/serialization.rs | 773 ++ servo/tests/unit/style/rule_tree/bench.rs | 239 + servo/tests/unit/style/rule_tree/mod.rs | 5 + servo/tests/unit/style/size_of.rs | 51 + servo/tests/unit/style/str.rs | 50 + servo/tests/unit/style/stylesheets.rs | 564 ++ servo/tests/unit/style/stylist.rs | 238 + 403 files changed, 148816 insertions(+) create mode 100644 servo/components/derive_common/Cargo.toml create mode 100644 servo/components/derive_common/cg.rs create mode 100644 servo/components/derive_common/lib.rs create mode 100644 servo/components/malloc_size_of/Cargo.toml create mode 100644 servo/components/malloc_size_of/LICENSE-APACHE create mode 100644 servo/components/malloc_size_of/LICENSE-MIT create mode 100644 servo/components/malloc_size_of/lib.rs create mode 100644 servo/components/selectors/CHANGES.md create mode 100644 servo/components/selectors/Cargo.toml create mode 100644 servo/components/selectors/README.md create mode 100644 servo/components/selectors/attr.rs create mode 100644 servo/components/selectors/bloom.rs create mode 100644 servo/components/selectors/build.rs create mode 100644 servo/components/selectors/builder.rs create mode 100644 servo/components/selectors/context.rs create mode 100644 servo/components/selectors/lib.rs create mode 100644 servo/components/selectors/matching.rs create mode 100644 servo/components/selectors/nth_index_cache.rs create mode 100644 servo/components/selectors/parser.rs create mode 100644 servo/components/selectors/relative_selector/cache.rs create mode 100644 servo/components/selectors/relative_selector/filter.rs create mode 100644 servo/components/selectors/relative_selector/mod.rs create mode 100644 servo/components/selectors/sink.rs create mode 100644 servo/components/selectors/tree.rs create mode 100644 servo/components/selectors/visitor.rs create mode 100644 servo/components/servo_arc/Cargo.toml create mode 100644 servo/components/servo_arc/lib.rs create mode 100644 servo/components/style/Cargo.toml create mode 100644 servo/components/style/README.md create mode 100644 servo/components/style/animation.rs create mode 100644 servo/components/style/applicable_declarations.rs create mode 100644 servo/components/style/attr.rs create mode 100644 servo/components/style/author_styles.rs create mode 100644 servo/components/style/bezier.rs create mode 100644 servo/components/style/bloom.rs create mode 100644 servo/components/style/build.rs create mode 100644 servo/components/style/build_gecko.rs create mode 100644 servo/components/style/color/convert.rs create mode 100644 servo/components/style/color/mix.rs create mode 100644 servo/components/style/color/mod.rs create mode 100644 servo/components/style/color/parsing.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/custom_properties_map.rs create mode 100644 servo/components/style/data.rs create mode 100644 servo/components/style/dom.rs create mode 100644 servo/components/style/dom_apis.rs create mode 100644 servo/components/style/driver.rs create mode 100644 servo/components/style/encoding_support.rs create mode 100644 servo/components/style/error_reporting.rs create mode 100644 servo/components/style/font_face.rs create mode 100644 servo/components/style/font_metrics.rs create mode 100644 servo/components/style/gecko/arc_types.rs create mode 100644 servo/components/style/gecko/conversions.rs create mode 100644 servo/components/style/gecko/data.rs create mode 100644 servo/components/style/gecko/media_features.rs create mode 100644 servo/components/style/gecko/media_queries.rs create mode 100644 servo/components/style/gecko/mod.rs create mode 100644 servo/components/style/gecko/non_ts_pseudo_class_list.rs create mode 100644 servo/components/style/gecko/pseudo_element.rs create mode 100644 servo/components/style/gecko/pseudo_element_definition.mako.rs create mode 100755 servo/components/style/gecko/regen_atoms.py create mode 100644 servo/components/style/gecko/restyle_damage.rs create mode 100644 servo/components/style/gecko/selector_parser.rs create mode 100644 servo/components/style/gecko/snapshot.rs create mode 100644 servo/components/style/gecko/snapshot_helpers.rs create mode 100644 servo/components/style/gecko/traversal.rs create mode 100644 servo/components/style/gecko/url.rs create mode 100644 servo/components/style/gecko/values.rs create mode 100644 servo/components/style/gecko/wrapper.rs create mode 100644 servo/components/style/gecko_bindings/mod.rs create mode 100644 servo/components/style/gecko_bindings/sugar/mod.rs create mode 100644 servo/components/style/gecko_bindings/sugar/ns_com_ptr.rs create mode 100644 servo/components/style/gecko_bindings/sugar/ns_compatibility.rs create mode 100644 servo/components/style/gecko_bindings/sugar/ns_style_auto_array.rs create mode 100644 servo/components/style/gecko_bindings/sugar/ns_t_array.rs create mode 100644 servo/components/style/gecko_bindings/sugar/origin_flags.rs create mode 100644 servo/components/style/gecko_bindings/sugar/ownership.rs create mode 100644 servo/components/style/gecko_bindings/sugar/refptr.rs create mode 100644 servo/components/style/gecko_string_cache/mod.rs create mode 100644 servo/components/style/gecko_string_cache/namespace.rs create mode 100644 servo/components/style/global_style_data.rs create mode 100644 servo/components/style/invalidation/element/document_state.rs create mode 100644 servo/components/style/invalidation/element/element_wrapper.rs create mode 100644 servo/components/style/invalidation/element/invalidation_map.rs create mode 100644 servo/components/style/invalidation/element/invalidator.rs create mode 100644 servo/components/style/invalidation/element/mod.rs create mode 100644 servo/components/style/invalidation/element/relative_selector.rs create mode 100644 servo/components/style/invalidation/element/restyle_hints.rs create mode 100644 servo/components/style/invalidation/element/state_and_attributes.rs create mode 100644 servo/components/style/invalidation/media_queries.rs create mode 100644 servo/components/style/invalidation/mod.rs create mode 100644 servo/components/style/invalidation/stylesheets.rs create mode 100644 servo/components/style/invalidation/viewport_units.rs create mode 100644 servo/components/style/lib.rs create mode 100644 servo/components/style/logical_geometry.rs create mode 100644 servo/components/style/macros.rs create mode 100644 servo/components/style/matching.rs create mode 100644 servo/components/style/media_queries/media_list.rs create mode 100644 servo/components/style/media_queries/media_query.rs create mode 100644 servo/components/style/media_queries/mod.rs create mode 100644 servo/components/style/parallel.rs create mode 100644 servo/components/style/parser.rs create mode 100644 servo/components/style/piecewise_linear.rs create mode 100644 servo/components/style/properties/Mako-1.1.2-py2.py3-none-any.whl create mode 100644 servo/components/style/properties/build.py create mode 100644 servo/components/style/properties/cascade.rs create mode 100644 servo/components/style/properties/computed_value_flags.rs create mode 100644 servo/components/style/properties/counted_unknown_properties.py create mode 100644 servo/components/style/properties/data.py create mode 100644 servo/components/style/properties/declaration_block.rs create mode 100644 servo/components/style/properties/gecko.mako.rs create mode 100644 servo/components/style/properties/helpers.mako.rs create mode 100644 servo/components/style/properties/helpers/animated_properties.mako.rs create mode 100644 servo/components/style/properties/longhands/background.mako.rs create mode 100644 servo/components/style/properties/longhands/border.mako.rs create mode 100644 servo/components/style/properties/longhands/box.mako.rs create mode 100644 servo/components/style/properties/longhands/column.mako.rs create mode 100644 servo/components/style/properties/longhands/counters.mako.rs create mode 100644 servo/components/style/properties/longhands/effects.mako.rs create mode 100644 servo/components/style/properties/longhands/font.mako.rs create mode 100644 servo/components/style/properties/longhands/inherited_box.mako.rs create mode 100644 servo/components/style/properties/longhands/inherited_svg.mako.rs create mode 100644 servo/components/style/properties/longhands/inherited_table.mako.rs create mode 100644 servo/components/style/properties/longhands/inherited_text.mako.rs create mode 100644 servo/components/style/properties/longhands/inherited_ui.mako.rs create mode 100644 servo/components/style/properties/longhands/list.mako.rs create mode 100644 servo/components/style/properties/longhands/margin.mako.rs create mode 100644 servo/components/style/properties/longhands/outline.mako.rs create mode 100644 servo/components/style/properties/longhands/padding.mako.rs create mode 100644 servo/components/style/properties/longhands/page.mako.rs create mode 100644 servo/components/style/properties/longhands/position.mako.rs create mode 100644 servo/components/style/properties/longhands/svg.mako.rs create mode 100644 servo/components/style/properties/longhands/table.mako.rs create mode 100644 servo/components/style/properties/longhands/text.mako.rs create mode 100644 servo/components/style/properties/longhands/ui.mako.rs create mode 100644 servo/components/style/properties/longhands/xul.mako.rs create mode 100644 servo/components/style/properties/mod.rs create mode 100644 servo/components/style/properties/properties.html.mako create mode 100644 servo/components/style/properties/properties.mako.rs create mode 100644 servo/components/style/properties/shorthands/background.mako.rs create mode 100644 servo/components/style/properties/shorthands/border.mako.rs create mode 100644 servo/components/style/properties/shorthands/box.mako.rs create mode 100644 servo/components/style/properties/shorthands/column.mako.rs create mode 100644 servo/components/style/properties/shorthands/font.mako.rs create mode 100644 servo/components/style/properties/shorthands/inherited_svg.mako.rs create mode 100644 servo/components/style/properties/shorthands/inherited_text.mako.rs create mode 100644 servo/components/style/properties/shorthands/list.mako.rs create mode 100644 servo/components/style/properties/shorthands/margin.mako.rs create mode 100644 servo/components/style/properties/shorthands/outline.mako.rs create mode 100644 servo/components/style/properties/shorthands/padding.mako.rs create mode 100644 servo/components/style/properties/shorthands/position.mako.rs create mode 100644 servo/components/style/properties/shorthands/svg.mako.rs create mode 100644 servo/components/style/properties/shorthands/text.mako.rs create mode 100644 servo/components/style/properties/shorthands/ui.mako.rs create mode 100644 servo/components/style/properties_and_values/mod.rs create mode 100644 servo/components/style/properties_and_values/registry.rs create mode 100644 servo/components/style/properties_and_values/rule.rs create mode 100644 servo/components/style/properties_and_values/syntax/ascii.rs create mode 100644 servo/components/style/properties_and_values/syntax/data_type.rs create mode 100644 servo/components/style/properties_and_values/syntax/mod.rs create mode 100644 servo/components/style/properties_and_values/value.rs create mode 100644 servo/components/style/queries/condition.rs create mode 100644 servo/components/style/queries/feature.rs create mode 100644 servo/components/style/queries/feature_expression.rs create mode 100644 servo/components/style/queries/mod.rs create mode 100644 servo/components/style/queries/values.rs create mode 100644 servo/components/style/rule_cache.rs create mode 100644 servo/components/style/rule_collector.rs create mode 100644 servo/components/style/rule_tree/core.rs create mode 100644 servo/components/style/rule_tree/level.rs create mode 100644 servo/components/style/rule_tree/map.rs create mode 100644 servo/components/style/rule_tree/mod.rs create mode 100644 servo/components/style/rule_tree/source.rs create mode 100644 servo/components/style/rule_tree/unsafe_box.rs create mode 100644 servo/components/style/scoped_tls.rs create mode 100644 servo/components/style/selector_map.rs create mode 100644 servo/components/style/selector_parser.rs create mode 100644 servo/components/style/servo/media_queries.rs create mode 100644 servo/components/style/servo/mod.rs create mode 100644 servo/components/style/servo/restyle_damage.rs create mode 100644 servo/components/style/servo/selector_parser.rs create mode 100644 servo/components/style/servo/url.rs create mode 100644 servo/components/style/shared_lock.rs create mode 100644 servo/components/style/sharing/checks.rs create mode 100644 servo/components/style/sharing/mod.rs create mode 100644 servo/components/style/str.rs create mode 100644 servo/components/style/style_adjuster.rs create mode 100644 servo/components/style/style_resolver.rs create mode 100644 servo/components/style/stylesheet_set.rs create mode 100644 servo/components/style/stylesheets/container_rule.rs create mode 100644 servo/components/style/stylesheets/counter_style_rule.rs create mode 100644 servo/components/style/stylesheets/document_rule.rs create mode 100644 servo/components/style/stylesheets/font_face_rule.rs create mode 100644 servo/components/style/stylesheets/font_feature_values_rule.rs create mode 100644 servo/components/style/stylesheets/font_palette_values_rule.rs create mode 100644 servo/components/style/stylesheets/import_rule.rs create mode 100644 servo/components/style/stylesheets/keyframes_rule.rs create mode 100644 servo/components/style/stylesheets/layer_rule.rs create mode 100644 servo/components/style/stylesheets/loader.rs create mode 100644 servo/components/style/stylesheets/margin_rule.rs create mode 100644 servo/components/style/stylesheets/media_rule.rs create mode 100644 servo/components/style/stylesheets/mod.rs create mode 100644 servo/components/style/stylesheets/namespace_rule.rs create mode 100644 servo/components/style/stylesheets/origin.rs create mode 100644 servo/components/style/stylesheets/page_rule.rs create mode 100644 servo/components/style/stylesheets/property_rule.rs create mode 100644 servo/components/style/stylesheets/rule_list.rs create mode 100644 servo/components/style/stylesheets/rule_parser.rs create mode 100644 servo/components/style/stylesheets/rules_iterator.rs create mode 100644 servo/components/style/stylesheets/style_rule.rs create mode 100644 servo/components/style/stylesheets/stylesheet.rs create mode 100644 servo/components/style/stylesheets/supports_rule.rs create mode 100644 servo/components/style/stylist.rs create mode 100644 servo/components/style/thread_state.rs create mode 100644 servo/components/style/traversal.rs create mode 100644 servo/components/style/traversal_flags.rs create mode 100644 servo/components/style/use_counters/mod.rs create mode 100644 servo/components/style/values/animated/color.rs create mode 100644 servo/components/style/values/animated/effects.rs create mode 100644 servo/components/style/values/animated/font.rs create mode 100644 servo/components/style/values/animated/grid.rs create mode 100644 servo/components/style/values/animated/lists.rs create mode 100644 servo/components/style/values/animated/mod.rs create mode 100644 servo/components/style/values/animated/svg.rs create mode 100644 servo/components/style/values/animated/transform.rs create mode 100644 servo/components/style/values/computed/align.rs create mode 100644 servo/components/style/values/computed/angle.rs create mode 100644 servo/components/style/values/computed/animation.rs create mode 100644 servo/components/style/values/computed/background.rs create mode 100644 servo/components/style/values/computed/basic_shape.rs create mode 100644 servo/components/style/values/computed/border.rs create mode 100644 servo/components/style/values/computed/box.rs create mode 100644 servo/components/style/values/computed/color.rs create mode 100644 servo/components/style/values/computed/column.rs create mode 100644 servo/components/style/values/computed/counters.rs create mode 100644 servo/components/style/values/computed/easing.rs create mode 100644 servo/components/style/values/computed/effects.rs create mode 100644 servo/components/style/values/computed/flex.rs create mode 100644 servo/components/style/values/computed/font.rs create mode 100644 servo/components/style/values/computed/image.rs create mode 100644 servo/components/style/values/computed/length.rs create mode 100644 servo/components/style/values/computed/length_percentage.rs create mode 100644 servo/components/style/values/computed/list.rs create mode 100644 servo/components/style/values/computed/mod.rs create mode 100644 servo/components/style/values/computed/motion.rs create mode 100644 servo/components/style/values/computed/outline.rs create mode 100644 servo/components/style/values/computed/page.rs create mode 100644 servo/components/style/values/computed/percentage.rs create mode 100644 servo/components/style/values/computed/position.rs create mode 100644 servo/components/style/values/computed/ratio.rs create mode 100644 servo/components/style/values/computed/rect.rs create mode 100644 servo/components/style/values/computed/resolution.rs create mode 100644 servo/components/style/values/computed/svg.rs create mode 100644 servo/components/style/values/computed/table.rs create mode 100644 servo/components/style/values/computed/text.rs create mode 100644 servo/components/style/values/computed/time.rs create mode 100644 servo/components/style/values/computed/transform.rs create mode 100644 servo/components/style/values/computed/ui.rs create mode 100644 servo/components/style/values/computed/url.rs create mode 100644 servo/components/style/values/distance.rs create mode 100644 servo/components/style/values/generics/animation.rs create mode 100644 servo/components/style/values/generics/background.rs create mode 100644 servo/components/style/values/generics/basic_shape.rs create mode 100644 servo/components/style/values/generics/border.rs create mode 100644 servo/components/style/values/generics/box.rs create mode 100644 servo/components/style/values/generics/calc.rs create mode 100644 servo/components/style/values/generics/color.rs create mode 100644 servo/components/style/values/generics/column.rs create mode 100644 servo/components/style/values/generics/counters.rs create mode 100644 servo/components/style/values/generics/easing.rs create mode 100644 servo/components/style/values/generics/effects.rs create mode 100644 servo/components/style/values/generics/flex.rs create mode 100644 servo/components/style/values/generics/font.rs create mode 100644 servo/components/style/values/generics/grid.rs create mode 100644 servo/components/style/values/generics/image.rs create mode 100644 servo/components/style/values/generics/length.rs create mode 100644 servo/components/style/values/generics/mod.rs create mode 100644 servo/components/style/values/generics/motion.rs create mode 100644 servo/components/style/values/generics/page.rs create mode 100644 servo/components/style/values/generics/position.rs create mode 100644 servo/components/style/values/generics/ratio.rs create mode 100644 servo/components/style/values/generics/rect.rs create mode 100644 servo/components/style/values/generics/size.rs create mode 100644 servo/components/style/values/generics/svg.rs create mode 100644 servo/components/style/values/generics/text.rs create mode 100644 servo/components/style/values/generics/transform.rs create mode 100644 servo/components/style/values/generics/ui.rs create mode 100644 servo/components/style/values/generics/url.rs create mode 100644 servo/components/style/values/mod.rs create mode 100644 servo/components/style/values/resolved/color.rs create mode 100644 servo/components/style/values/resolved/counters.rs create mode 100644 servo/components/style/values/resolved/mod.rs create mode 100644 servo/components/style/values/specified/align.rs create mode 100644 servo/components/style/values/specified/angle.rs create mode 100644 servo/components/style/values/specified/animation.rs create mode 100644 servo/components/style/values/specified/background.rs create mode 100644 servo/components/style/values/specified/basic_shape.rs create mode 100644 servo/components/style/values/specified/border.rs create mode 100644 servo/components/style/values/specified/box.rs create mode 100644 servo/components/style/values/specified/calc.rs create mode 100644 servo/components/style/values/specified/color.rs create mode 100644 servo/components/style/values/specified/column.rs create mode 100644 servo/components/style/values/specified/counters.rs create mode 100644 servo/components/style/values/specified/easing.rs create mode 100644 servo/components/style/values/specified/effects.rs create mode 100644 servo/components/style/values/specified/flex.rs create mode 100644 servo/components/style/values/specified/font.rs create mode 100644 servo/components/style/values/specified/gecko.rs create mode 100644 servo/components/style/values/specified/grid.rs create mode 100644 servo/components/style/values/specified/image.rs create mode 100644 servo/components/style/values/specified/length.rs create mode 100644 servo/components/style/values/specified/list.rs create mode 100644 servo/components/style/values/specified/mod.rs create mode 100644 servo/components/style/values/specified/motion.rs create mode 100644 servo/components/style/values/specified/outline.rs create mode 100644 servo/components/style/values/specified/page.rs create mode 100644 servo/components/style/values/specified/percentage.rs create mode 100644 servo/components/style/values/specified/position.rs create mode 100644 servo/components/style/values/specified/ratio.rs create mode 100644 servo/components/style/values/specified/rect.rs create mode 100644 servo/components/style/values/specified/resolution.rs create mode 100644 servo/components/style/values/specified/source_size_list.rs create mode 100644 servo/components/style/values/specified/svg.rs create mode 100644 servo/components/style/values/specified/svg_path.rs create mode 100644 servo/components/style/values/specified/table.rs create mode 100644 servo/components/style/values/specified/text.rs create mode 100644 servo/components/style/values/specified/time.rs create mode 100644 servo/components/style/values/specified/transform.rs create mode 100644 servo/components/style/values/specified/ui.rs create mode 100644 servo/components/style/values/specified/url.rs create mode 100644 servo/components/style_derive/Cargo.toml create mode 100644 servo/components/style_derive/animate.rs create mode 100644 servo/components/style_derive/compute_squared_distance.rs create mode 100644 servo/components/style_derive/lib.rs create mode 100644 servo/components/style_derive/parse.rs create mode 100644 servo/components/style_derive/specified_value_info.rs create mode 100644 servo/components/style_derive/to_animated_value.rs create mode 100644 servo/components/style_derive/to_animated_zero.rs create mode 100644 servo/components/style_derive/to_computed_value.rs create mode 100644 servo/components/style_derive/to_css.rs create mode 100644 servo/components/style_derive/to_resolved_value.rs create mode 100644 servo/components/style_traits/Cargo.toml create mode 100644 servo/components/style_traits/arc_slice.rs create mode 100644 servo/components/style_traits/dom.rs create mode 100644 servo/components/style_traits/lib.rs create mode 100644 servo/components/style_traits/owned_slice.rs create mode 100644 servo/components/style_traits/owned_str.rs create mode 100644 servo/components/style_traits/specified_value_info.rs create mode 100644 servo/components/style_traits/values.rs create mode 100644 servo/components/to_shmem/Cargo.toml create mode 100644 servo/components/to_shmem/lib.rs create mode 100644 servo/components/to_shmem_derive/Cargo.toml create mode 100644 servo/components/to_shmem_derive/lib.rs create mode 100644 servo/components/to_shmem_derive/to_shmem.rs create mode 100644 servo/moz.build create mode 100644 servo/ports/geckolib/Cargo.toml create mode 100644 servo/ports/geckolib/cbindgen.toml create mode 100644 servo/ports/geckolib/error_reporter.rs create mode 100644 servo/ports/geckolib/glue.rs create mode 100644 servo/ports/geckolib/lib.rs create mode 100644 servo/ports/geckolib/stylesheet_loader.rs create mode 100644 servo/ports/geckolib/tests/Cargo.toml create mode 100644 servo/ports/geckolib/tests/lib.rs create mode 100644 servo/ports/geckolib/tests/piecewise_linear.rs create mode 100644 servo/rustfmt.toml create mode 100644 servo/tests/unit/malloc_size_of/Cargo.toml create mode 100644 servo/tests/unit/malloc_size_of/lib.rs create mode 100644 servo/tests/unit/style/Cargo.toml create mode 100644 servo/tests/unit/style/animated_properties.rs create mode 100644 servo/tests/unit/style/attr.rs create mode 100644 servo/tests/unit/style/custom_properties.rs create mode 100644 servo/tests/unit/style/lib.rs create mode 100644 servo/tests/unit/style/logical_geometry.rs create mode 100644 servo/tests/unit/style/media_queries.rs create mode 100644 servo/tests/unit/style/parsing/animation.rs create mode 100644 servo/tests/unit/style/parsing/background.rs create mode 100644 servo/tests/unit/style/parsing/border.rs create mode 100644 servo/tests/unit/style/parsing/box_.rs create mode 100644 servo/tests/unit/style/parsing/column.rs create mode 100644 servo/tests/unit/style/parsing/effects.rs create mode 100644 servo/tests/unit/style/parsing/image.rs create mode 100644 servo/tests/unit/style/parsing/inherited_text.rs create mode 100644 servo/tests/unit/style/parsing/mod.rs create mode 100644 servo/tests/unit/style/parsing/outline.rs create mode 100644 servo/tests/unit/style/parsing/position.rs create mode 100644 servo/tests/unit/style/parsing/selectors.rs create mode 100644 servo/tests/unit/style/parsing/supports.rs create mode 100644 servo/tests/unit/style/parsing/text_overflow.rs create mode 100644 servo/tests/unit/style/parsing/transition_duration.rs create mode 100644 servo/tests/unit/style/parsing/transition_timing_function.rs create mode 100644 servo/tests/unit/style/properties/mod.rs create mode 100644 servo/tests/unit/style/properties/scaffolding.rs create mode 100644 servo/tests/unit/style/properties/serialization.rs create mode 100644 servo/tests/unit/style/rule_tree/bench.rs create mode 100644 servo/tests/unit/style/rule_tree/mod.rs create mode 100644 servo/tests/unit/style/size_of.rs create mode 100644 servo/tests/unit/style/str.rs create mode 100644 servo/tests/unit/style/stylesheets.rs create mode 100644 servo/tests/unit/style/stylist.rs (limited to 'servo') diff --git a/servo/components/derive_common/Cargo.toml b/servo/components/derive_common/Cargo.toml new file mode 100644 index 0000000000..b493988026 --- /dev/null +++ b/servo/components/derive_common/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "derive_common" +version = "0.0.1" +authors = ["The Servo Project Developers"] +license = "MPL-2.0" +publish = false + +[lib] +path = "lib.rs" + +[dependencies] +darling = { version = "0.20", default-features = false } +proc-macro2 = "1" +quote = "1" +syn = { version = "2", default-features = false, features = ["clone-impls", "parsing"] } +synstructure = "0.13" diff --git a/servo/components/derive_common/cg.rs b/servo/components/derive_common/cg.rs new file mode 100644 index 0000000000..73301af2ff --- /dev/null +++ b/servo/components/derive_common/cg.rs @@ -0,0 +1,396 @@ +/* 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 darling::{FromDeriveInput, FromField, FromVariant}; +use proc_macro2::{Span, TokenStream}; +use quote::TokenStreamExt; +use syn::{self, AngleBracketedGenericArguments, AssocType, DeriveInput, Field}; +use syn::{GenericArgument, GenericParam, Ident, Path}; +use syn::{PathArguments, PathSegment, QSelf, Type, TypeArray, TypeGroup}; +use syn::{TypeParam, TypeParen, TypePath, TypeSlice, TypeTuple}; +use syn::{Variant, WherePredicate}; +use synstructure::{self, BindStyle, BindingInfo, VariantAst, VariantInfo}; + +/// Given an input type which has some where clauses already, like: +/// +/// struct InputType +/// where +/// T: Zero, +/// { +/// ... +/// } +/// +/// Add the necessary `where` clauses so that the output type of a trait +/// fulfils them. +/// +/// For example: +/// +/// ```ignore +/// ::ComputedValue: Zero, +/// ``` +/// +/// This needs to run before adding other bounds to the type parameters. +pub fn propagate_clauses_to_output_type( + where_clause: &mut Option, + generics: &syn::Generics, + trait_path: &Path, + trait_output: &Ident, +) { + let where_clause = match *where_clause { + Some(ref mut clause) => clause, + None => return, + }; + let mut extra_bounds = vec![]; + for pred in &where_clause.predicates { + let ty = match *pred { + syn::WherePredicate::Type(ref ty) => ty, + ref predicate => panic!("Unhanded complex where predicate: {:?}", predicate), + }; + + let path = match ty.bounded_ty { + syn::Type::Path(ref p) => &p.path, + ref ty => panic!("Unhanded complex where type: {:?}", ty), + }; + + assert!( + ty.lifetimes.is_none(), + "Unhanded complex lifetime bound: {:?}", + ty, + ); + + let ident = match path_to_ident(path) { + Some(i) => i, + None => panic!("Unhanded complex where type path: {:?}", path), + }; + + if generics.type_params().any(|param| param.ident == *ident) { + extra_bounds.push(ty.clone()); + } + } + + for bound in extra_bounds { + let ty = bound.bounded_ty; + let bounds = bound.bounds; + where_clause + .predicates + .push(parse_quote!(<#ty as #trait_path>::#trait_output: #bounds)) + } +} + +pub fn add_predicate(where_clause: &mut Option, pred: WherePredicate) { + where_clause + .get_or_insert(parse_quote!(where)) + .predicates + .push(pred); +} + +pub fn fmap_match(input: &DeriveInput, bind_style: BindStyle, f: F) -> TokenStream +where + F: FnMut(&BindingInfo) -> TokenStream, +{ + fmap2_match(input, bind_style, f, |_| None) +} + +pub fn fmap2_match( + input: &DeriveInput, + bind_style: BindStyle, + mut f: F, + mut g: G, +) -> TokenStream +where + F: FnMut(&BindingInfo) -> TokenStream, + G: FnMut(&BindingInfo) -> Option, +{ + let mut s = synstructure::Structure::new(input); + s.variants_mut().iter_mut().for_each(|v| { + v.bind_with(|_| bind_style); + }); + s.each_variant(|variant| { + let (mapped, mapped_fields) = value(variant, "mapped"); + let fields_pairs = variant.bindings().iter().zip(mapped_fields.iter()); + let mut computations = quote!(); + computations.append_all(fields_pairs.map(|(field, mapped_field)| { + let expr = f(field); + quote! { let #mapped_field = #expr; } + })); + computations.append_all( + mapped_fields + .iter() + .map(|mapped_field| match g(mapped_field) { + Some(expr) => quote! { let #mapped_field = #expr; }, + None => quote!(), + }), + ); + computations.append_all(mapped); + Some(computations) + }) +} + +pub fn fmap_trait_output(input: &DeriveInput, trait_path: &Path, trait_output: &Ident) -> Path { + let segment = PathSegment { + ident: input.ident.clone(), + arguments: PathArguments::AngleBracketed(AngleBracketedGenericArguments { + args: input + .generics + .params + .iter() + .map(|arg| match arg { + &GenericParam::Lifetime(ref data) => { + GenericArgument::Lifetime(data.lifetime.clone()) + }, + &GenericParam::Type(ref data) => { + let ident = &data.ident; + GenericArgument::Type(parse_quote!(<#ident as #trait_path>::#trait_output)) + }, + &GenericParam::Const(ref inner) => { + let ident = &inner.ident; + GenericArgument::Const(parse_quote!(#ident)) + }, + }) + .collect(), + colon2_token: Default::default(), + gt_token: Default::default(), + lt_token: Default::default(), + }), + }; + segment.into() +} + +pub fn map_type_params(ty: &Type, params: &[&TypeParam], self_type: &Path, f: &mut F) -> Type +where + F: FnMut(&Ident) -> Type, +{ + match *ty { + Type::Slice(ref inner) => Type::from(TypeSlice { + elem: Box::new(map_type_params(&inner.elem, params, self_type, f)), + ..inner.clone() + }), + Type::Array(ref inner) => { + //ref ty, ref expr) => { + Type::from(TypeArray { + elem: Box::new(map_type_params(&inner.elem, params, self_type, f)), + ..inner.clone() + }) + }, + ref ty @ Type::Never(_) => ty.clone(), + Type::Tuple(ref inner) => Type::from(TypeTuple { + elems: inner + .elems + .iter() + .map(|ty| map_type_params(&ty, params, self_type, f)) + .collect(), + ..inner.clone() + }), + Type::Path(TypePath { + qself: None, + ref path, + }) => { + if let Some(ident) = path_to_ident(path) { + if params.iter().any(|ref param| ¶m.ident == ident) { + return f(ident); + } + if ident == "Self" { + return Type::from(TypePath { + qself: None, + path: self_type.clone(), + }); + } + } + Type::from(TypePath { + qself: None, + path: map_type_params_in_path(path, params, self_type, f), + }) + }, + Type::Path(TypePath { + ref qself, + ref path, + }) => Type::from(TypePath { + qself: qself.as_ref().map(|qself| QSelf { + ty: Box::new(map_type_params(&qself.ty, params, self_type, f)), + position: qself.position, + ..qself.clone() + }), + path: map_type_params_in_path(path, params, self_type, f), + }), + Type::Paren(ref inner) => Type::from(TypeParen { + elem: Box::new(map_type_params(&inner.elem, params, self_type, f)), + ..inner.clone() + }), + Type::Group(ref inner) => Type::from(TypeGroup { + elem: Box::new(map_type_params(&inner.elem, params, self_type, f)), + ..inner.clone() + }), + ref ty => panic!("type {:?} cannot be mapped yet", ty), + } +} + +fn map_type_params_in_path( + path: &Path, + params: &[&TypeParam], + self_type: &Path, + f: &mut F, +) -> Path +where + F: FnMut(&Ident) -> Type, +{ + Path { + leading_colon: path.leading_colon, + segments: path + .segments + .iter() + .map(|segment| PathSegment { + ident: segment.ident.clone(), + arguments: match segment.arguments { + PathArguments::AngleBracketed(ref data) => { + PathArguments::AngleBracketed(AngleBracketedGenericArguments { + args: data + .args + .iter() + .map(|arg| match arg { + ty @ &GenericArgument::Lifetime(_) => ty.clone(), + &GenericArgument::Type(ref data) => GenericArgument::Type( + map_type_params(data, params, self_type, f), + ), + &GenericArgument::AssocType(ref data) => { + GenericArgument::AssocType(AssocType { + ty: map_type_params(&data.ty, params, self_type, f), + ..data.clone() + }) + }, + ref arg => panic!("arguments {:?} cannot be mapped yet", arg), + }) + .collect(), + ..data.clone() + }) + }, + ref arg @ PathArguments::None => arg.clone(), + ref parameters => panic!("parameters {:?} cannot be mapped yet", parameters), + }, + }) + .collect(), + } +} + +fn path_to_ident(path: &Path) -> Option<&Ident> { + match *path { + Path { + leading_colon: None, + ref segments, + } if segments.len() == 1 => { + if segments[0].arguments.is_empty() { + Some(&segments[0].ident) + } else { + None + } + }, + _ => None, + } +} + +pub fn parse_field_attrs(field: &Field) -> A +where + A: FromField, +{ + match A::from_field(field) { + Ok(attrs) => attrs, + Err(e) => panic!("failed to parse field attributes: {}", e), + } +} + +pub fn parse_input_attrs(input: &DeriveInput) -> A +where + A: FromDeriveInput, +{ + match A::from_derive_input(input) { + Ok(attrs) => attrs, + Err(e) => panic!("failed to parse input attributes: {}", e), + } +} + +pub fn parse_variant_attrs_from_ast(variant: &VariantAst) -> A +where + A: FromVariant, +{ + let v = Variant { + ident: variant.ident.clone(), + attrs: variant.attrs.to_vec(), + fields: variant.fields.clone(), + discriminant: variant.discriminant.clone(), + }; + parse_variant_attrs(&v) +} + +pub fn parse_variant_attrs(variant: &Variant) -> A +where + A: FromVariant, +{ + match A::from_variant(variant) { + Ok(attrs) => attrs, + Err(e) => panic!("failed to parse variant attributes: {}", e), + } +} + +pub fn ref_pattern<'a>( + variant: &'a VariantInfo, + prefix: &str, +) -> (TokenStream, Vec>) { + let mut v = variant.clone(); + v.bind_with(|_| BindStyle::Ref); + v.bindings_mut().iter_mut().for_each(|b| { + b.binding = Ident::new(&format!("{}_{}", b.binding, prefix), Span::call_site()) + }); + (v.pat(), v.bindings().to_vec()) +} + +pub fn value<'a>(variant: &'a VariantInfo, prefix: &str) -> (TokenStream, Vec>) { + let mut v = variant.clone(); + v.bindings_mut().iter_mut().for_each(|b| { + b.binding = Ident::new(&format!("{}_{}", b.binding, prefix), Span::call_site()) + }); + v.bind_with(|_| BindStyle::Move); + (v.pat(), v.bindings().to_vec()) +} + +/// Transforms "FooBar" to "foo-bar". +/// +/// If the first Camel segment is "Moz", "Webkit", or "Servo", the result string +/// is prepended with "-". +pub fn to_css_identifier(mut camel_case: &str) -> String { + camel_case = camel_case.trim_end_matches('_'); + let mut first = true; + let mut result = String::with_capacity(camel_case.len()); + while let Some(segment) = split_camel_segment(&mut camel_case) { + if first { + match segment { + "Moz" | "Webkit" | "Servo" => first = false, + _ => {}, + } + } + if !first { + result.push('-'); + } + first = false; + result.push_str(&segment.to_lowercase()); + } + result +} + +/// Transforms foo-bar to FOO_BAR. +pub fn to_scream_case(css_case: &str) -> String { + css_case.to_uppercase().replace('-', "_") +} + +/// Given "FooBar", returns "Foo" and sets `camel_case` to "Bar". +fn split_camel_segment<'input>(camel_case: &mut &'input str) -> Option<&'input str> { + let index = match camel_case.chars().next() { + None => return None, + Some(ch) => ch.len_utf8(), + }; + let end_position = camel_case[index..] + .find(char::is_uppercase) + .map_or(camel_case.len(), |pos| index + pos); + let result = &camel_case[..end_position]; + *camel_case = &camel_case[end_position..]; + Some(result) +} diff --git a/servo/components/derive_common/lib.rs b/servo/components/derive_common/lib.rs new file mode 100644 index 0000000000..1441535144 --- /dev/null +++ b/servo/components/derive_common/lib.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/. */ + +extern crate darling; +extern crate proc_macro2; +#[macro_use] +extern crate quote; +#[macro_use] +extern crate syn; +extern crate synstructure; + +pub mod cg; diff --git a/servo/components/malloc_size_of/Cargo.toml b/servo/components/malloc_size_of/Cargo.toml new file mode 100644 index 0000000000..cd5deaea44 --- /dev/null +++ b/servo/components/malloc_size_of/Cargo.toml @@ -0,0 +1,53 @@ +[package] +name = "malloc_size_of" +version = "0.0.1" +authors = ["The Servo Project Developers"] +license = "MIT/Apache-2.0" +publish = false + +[lib] +path = "lib.rs" + +[features] +servo = [ + "accountable-refcell", + "content-security-policy", + "crossbeam-channel", + "hyper", + "hyper_serde", + "keyboard-types", + "serde", + "serde_bytes", + "string_cache", + "time", + "url", + "uuid", + "webrender_api", + "xml5ever", +] + +[dependencies] +accountable-refcell = { version = "0.2.0", optional = true } +app_units = "0.7" +content-security-policy = { version = "0.4.0", features = ["serde"], optional = true } +crossbeam-channel = { version = "0.4", optional = true } +cssparser = "0.33" +dom = { path = "../../../dom/base/rust" } +euclid = "0.22" +hyper = { version = "0.12", optional = true } +hyper_serde = { version = "0.11", optional = true } +keyboard-types = { version = "0.4.3", optional = true } +selectors = { path = "../selectors" } +serde = { version = "1.0.27", optional = true } +serde_bytes = { version = "0.11", optional = true } +servo_arc = { path = "../servo_arc" } +smallbitvec = "2.3.0" +smallvec = "1.0" +string_cache = { version = "0.8", optional = true } +thin-vec = { version = "0.2.1", features = ["gecko-ffi"] } +time = { version = "0.1.17", optional = true } +url = { version = "2.4", optional = true } +uuid = { version = "0.8", features = ["v4"], optional = true } +void = "1.0.2" +webrender_api = { git = "https://github.com/servo/webrender", optional = true } +xml5ever = { version = "0.16", optional = true } diff --git a/servo/components/malloc_size_of/LICENSE-APACHE b/servo/components/malloc_size_of/LICENSE-APACHE new file mode 100644 index 0000000000..16fe87b06e --- /dev/null +++ b/servo/components/malloc_size_of/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/servo/components/malloc_size_of/LICENSE-MIT b/servo/components/malloc_size_of/LICENSE-MIT new file mode 100644 index 0000000000..31aa79387f --- /dev/null +++ b/servo/components/malloc_size_of/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/servo/components/malloc_size_of/lib.rs b/servo/components/malloc_size_of/lib.rs new file mode 100644 index 0000000000..32d7f03bfb --- /dev/null +++ b/servo/components/malloc_size_of/lib.rs @@ -0,0 +1,1003 @@ +// Copyright 2016-2017 The Servo Project Developers. See the COPYRIGHT +// file at the top-level directory of this distribution and at +// http://rust-lang.org/COPYRIGHT. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +//! A crate for measuring the heap usage of data structures in a way that +//! integrates with Firefox's memory reporting, particularly the use of +//! mozjemalloc and DMD. In particular, it has the following features. +//! - It isn't bound to a particular heap allocator. +//! - It provides traits for both "shallow" and "deep" measurement, which gives +//! flexibility in the cases where the traits can't be used. +//! - It allows for measuring blocks even when only an interior pointer can be +//! obtained for heap allocations, e.g. `HashSet` and `HashMap`. (This relies +//! on the heap allocator having suitable support, which mozjemalloc has.) +//! - It allows handling of types like `Rc` and `Arc` by providing traits that +//! are different to the ones for non-graph structures. +//! +//! Suggested uses are as follows. +//! - When possible, use the `MallocSizeOf` trait. (Deriving support is +//! provided by the `malloc_size_of_derive` crate.) +//! - If you need an additional synchronization argument, provide a function +//! that is like the standard trait method, but with the extra argument. +//! - If you need multiple measurements for a type, provide a function named +//! `add_size_of` that takes a mutable reference to a struct that contains +//! the multiple measurement fields. +//! - When deep measurement (via `MallocSizeOf`) cannot be implemented for a +//! type, shallow measurement (via `MallocShallowSizeOf`) in combination with +//! iteration can be a useful substitute. +//! - `Rc` and `Arc` are always tricky, which is why `MallocSizeOf` is not (and +//! should not be) implemented for them. +//! - If an `Rc` or `Arc` is known to be a "primary" reference and can always +//! be measured, it should be measured via the `MallocUnconditionalSizeOf` +//! trait. +//! - If an `Rc` or `Arc` should be measured only if it hasn't been seen +//! before, it should be measured via the `MallocConditionalSizeOf` trait. +//! - Using universal function call syntax is a good idea when measuring boxed +//! fields in structs, because it makes it clear that the Box is being +//! measured as well as the thing it points to. E.g. +//! ` as MallocSizeOf>::size_of(field, ops)`. +//! +//! Note: WebRender has a reduced fork of this crate, so that we can avoid +//! publishing this crate on crates.io. + +#[cfg(feature = "servo")] +extern crate accountable_refcell; +extern crate app_units; +#[cfg(feature = "servo")] +extern crate content_security_policy; +#[cfg(feature = "servo")] +extern crate crossbeam_channel; +extern crate cssparser; +extern crate euclid; +#[cfg(feature = "servo")] +extern crate hyper; +#[cfg(feature = "servo")] +extern crate hyper_serde; +#[cfg(feature = "servo")] +extern crate keyboard_types; +extern crate selectors; +#[cfg(feature = "servo")] +extern crate serde; +#[cfg(feature = "servo")] +extern crate serde_bytes; +extern crate servo_arc; +extern crate smallbitvec; +extern crate smallvec; +#[cfg(feature = "servo")] +extern crate string_cache; +#[cfg(feature = "servo")] +extern crate time; +#[cfg(feature = "url")] +extern crate url; +#[cfg(feature = "servo")] +extern crate uuid; +extern crate void; +#[cfg(feature = "webrender_api")] +extern crate webrender_api; +#[cfg(feature = "servo")] +extern crate xml5ever; + +#[cfg(feature = "servo")] +use content_security_policy as csp; +#[cfg(feature = "servo")] +use serde_bytes::ByteBuf; +use std::hash::{BuildHasher, Hash}; +use std::mem::size_of; +use std::ops::Range; +use std::ops::{Deref, DerefMut}; +use std::os::raw::c_void; +#[cfg(feature = "servo")] +use uuid::Uuid; +use void::Void; + +/// A C function that takes a pointer to a heap allocation and returns its size. +type VoidPtrToSizeFn = unsafe extern "C" fn(ptr: *const c_void) -> usize; + +/// A closure implementing a stateful predicate on pointers. +type VoidPtrToBoolFnMut = dyn FnMut(*const c_void) -> bool; + +/// Operations used when measuring heap usage of data structures. +pub struct MallocSizeOfOps { + /// A function that returns the size of a heap allocation. + size_of_op: VoidPtrToSizeFn, + + /// Like `size_of_op`, but can take an interior pointer. Optional because + /// not all allocators support this operation. If it's not provided, some + /// memory measurements will actually be computed estimates rather than + /// real and accurate measurements. + enclosing_size_of_op: Option, + + /// Check if a pointer has been seen before, and remember it for next time. + /// Useful when measuring `Rc`s and `Arc`s. Optional, because many places + /// don't need it. + have_seen_ptr_op: Option>, +} + +impl MallocSizeOfOps { + pub fn new( + size_of: VoidPtrToSizeFn, + malloc_enclosing_size_of: Option, + have_seen_ptr: Option>, + ) -> Self { + MallocSizeOfOps { + size_of_op: size_of, + enclosing_size_of_op: malloc_enclosing_size_of, + have_seen_ptr_op: have_seen_ptr, + } + } + + /// Check if an allocation is empty. This relies on knowledge of how Rust + /// handles empty allocations, which may change in the future. + fn is_empty(ptr: *const T) -> bool { + // The correct condition is this: + // `ptr as usize <= ::std::mem::align_of::()` + // But we can't call align_of() on a ?Sized T. So we approximate it + // with the following. 256 is large enough that it should always be + // larger than the required alignment, but small enough that it is + // always in the first page of memory and therefore not a legitimate + // address. + return ptr as *const usize as usize <= 256; + } + + /// Call `size_of_op` on `ptr`, first checking that the allocation isn't + /// empty, because some types (such as `Vec`) utilize empty allocations. + pub unsafe fn malloc_size_of(&self, ptr: *const T) -> usize { + if MallocSizeOfOps::is_empty(ptr) { + 0 + } else { + (self.size_of_op)(ptr as *const c_void) + } + } + + /// Is an `enclosing_size_of_op` available? + pub fn has_malloc_enclosing_size_of(&self) -> bool { + self.enclosing_size_of_op.is_some() + } + + /// Call `enclosing_size_of_op`, which must be available, on `ptr`, which + /// must not be empty. + pub unsafe fn malloc_enclosing_size_of(&self, ptr: *const T) -> usize { + assert!(!MallocSizeOfOps::is_empty(ptr)); + (self.enclosing_size_of_op.unwrap())(ptr as *const c_void) + } + + /// Call `have_seen_ptr_op` on `ptr`. + pub fn have_seen_ptr(&mut self, ptr: *const T) -> bool { + let have_seen_ptr_op = self + .have_seen_ptr_op + .as_mut() + .expect("missing have_seen_ptr_op"); + have_seen_ptr_op(ptr as *const c_void) + } +} + +/// Trait for measuring the "deep" heap usage of a data structure. This is the +/// most commonly-used of the traits. +pub trait MallocSizeOf { + /// Measure the heap usage of all descendant heap-allocated structures, but + /// not the space taken up by the value itself. + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize; +} + +/// Trait for measuring the "shallow" heap usage of a container. +pub trait MallocShallowSizeOf { + /// Measure the heap usage of immediate heap-allocated descendant + /// structures, but not the space taken up by the value itself. Anything + /// beyond the immediate descendants must be measured separately, using + /// iteration. + fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize; +} + +/// Like `MallocSizeOf`, but with a different name so it cannot be used +/// accidentally with derive(MallocSizeOf). For use with types like `Rc` and +/// `Arc` when appropriate (e.g. when measuring a "primary" reference). +pub trait MallocUnconditionalSizeOf { + /// Measure the heap usage of all heap-allocated descendant structures, but + /// not the space taken up by the value itself. + fn unconditional_size_of(&self, ops: &mut MallocSizeOfOps) -> usize; +} + +/// `MallocUnconditionalSizeOf` combined with `MallocShallowSizeOf`. +pub trait MallocUnconditionalShallowSizeOf { + /// `unconditional_size_of` combined with `shallow_size_of`. + fn unconditional_shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize; +} + +/// Like `MallocSizeOf`, but only measures if the value hasn't already been +/// measured. For use with types like `Rc` and `Arc` when appropriate (e.g. +/// when there is no "primary" reference). +pub trait MallocConditionalSizeOf { + /// Measure the heap usage of all heap-allocated descendant structures, but + /// not the space taken up by the value itself, and only if that heap usage + /// hasn't already been measured. + fn conditional_size_of(&self, ops: &mut MallocSizeOfOps) -> usize; +} + +/// `MallocConditionalSizeOf` combined with `MallocShallowSizeOf`. +pub trait MallocConditionalShallowSizeOf { + /// `conditional_size_of` combined with `shallow_size_of`. + fn conditional_shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize; +} + +impl MallocSizeOf for String { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + unsafe { ops.malloc_size_of(self.as_ptr()) } + } +} + +impl<'a, T: ?Sized> MallocSizeOf for &'a T { + fn size_of(&self, _ops: &mut MallocSizeOfOps) -> usize { + // Zero makes sense for a non-owning reference. + 0 + } +} + +impl MallocShallowSizeOf for Box { + fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + unsafe { ops.malloc_size_of(&**self) } + } +} + +impl MallocSizeOf for Box { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + self.shallow_size_of(ops) + (**self).size_of(ops) + } +} + +impl MallocSizeOf for () { + fn size_of(&self, _ops: &mut MallocSizeOfOps) -> usize { + 0 + } +} + +impl MallocSizeOf for (T1, T2) +where + T1: MallocSizeOf, + T2: MallocSizeOf, +{ + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + self.0.size_of(ops) + self.1.size_of(ops) + } +} + +impl MallocSizeOf for (T1, T2, T3) +where + T1: MallocSizeOf, + T2: MallocSizeOf, + T3: MallocSizeOf, +{ + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + self.0.size_of(ops) + self.1.size_of(ops) + self.2.size_of(ops) + } +} + +impl MallocSizeOf for (T1, T2, T3, T4) +where + T1: MallocSizeOf, + T2: MallocSizeOf, + T3: MallocSizeOf, + T4: MallocSizeOf, +{ + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + self.0.size_of(ops) + self.1.size_of(ops) + self.2.size_of(ops) + self.3.size_of(ops) + } +} + +impl MallocSizeOf for Option { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + if let Some(val) = self.as_ref() { + val.size_of(ops) + } else { + 0 + } + } +} + +impl MallocSizeOf for Result { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + match *self { + Ok(ref x) => x.size_of(ops), + Err(ref e) => e.size_of(ops), + } + } +} + +impl MallocSizeOf for std::cell::Cell { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + self.get().size_of(ops) + } +} + +impl MallocSizeOf for std::cell::RefCell { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + self.borrow().size_of(ops) + } +} + +impl<'a, B: ?Sized + ToOwned> MallocSizeOf for std::borrow::Cow<'a, B> +where + B::Owned: MallocSizeOf, +{ + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + match *self { + std::borrow::Cow::Borrowed(_) => 0, + std::borrow::Cow::Owned(ref b) => b.size_of(ops), + } + } +} + +impl MallocSizeOf for [T] { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + let mut n = 0; + for elem in self.iter() { + n += elem.size_of(ops); + } + n + } +} + +#[cfg(feature = "servo")] +impl MallocShallowSizeOf for ByteBuf { + fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + unsafe { ops.malloc_size_of(self.as_ptr()) } + } +} + +#[cfg(feature = "servo")] +impl MallocSizeOf for ByteBuf { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + let mut n = self.shallow_size_of(ops); + for elem in self.iter() { + n += elem.size_of(ops); + } + n + } +} + +impl MallocShallowSizeOf for Vec { + fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + unsafe { ops.malloc_size_of(self.as_ptr()) } + } +} + +impl MallocSizeOf for Vec { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + let mut n = self.shallow_size_of(ops); + for elem in self.iter() { + n += elem.size_of(ops); + } + n + } +} + +impl MallocShallowSizeOf for std::collections::VecDeque { + fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + if ops.has_malloc_enclosing_size_of() { + if let Some(front) = self.front() { + // The front element is an interior pointer. + unsafe { ops.malloc_enclosing_size_of(&*front) } + } else { + // This assumes that no memory is allocated when the VecDeque is empty. + 0 + } + } else { + // An estimate. + self.capacity() * size_of::() + } + } +} + +impl MallocSizeOf for std::collections::VecDeque { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + let mut n = self.shallow_size_of(ops); + for elem in self.iter() { + n += elem.size_of(ops); + } + n + } +} + +impl MallocShallowSizeOf for smallvec::SmallVec { + fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + if self.spilled() { + unsafe { ops.malloc_size_of(self.as_ptr()) } + } else { + 0 + } + } +} + +impl MallocSizeOf for smallvec::SmallVec +where + A: smallvec::Array, + A::Item: MallocSizeOf, +{ + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + let mut n = self.shallow_size_of(ops); + for elem in self.iter() { + n += elem.size_of(ops); + } + n + } +} + +impl MallocShallowSizeOf for thin_vec::ThinVec { + fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + if self.capacity() == 0 { + // If it's the singleton we might not be a heap pointer. + return 0; + } + + assert_eq!( + std::mem::size_of::(), + std::mem::size_of::<*const ()>() + ); + unsafe { ops.malloc_size_of(*(self as *const Self as *const *const ())) } + } +} + +impl MallocSizeOf for thin_vec::ThinVec { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + let mut n = self.shallow_size_of(ops); + for elem in self.iter() { + n += elem.size_of(ops); + } + n + } +} + +macro_rules! malloc_size_of_hash_set { + ($ty:ty) => { + impl MallocShallowSizeOf for $ty + where + T: Eq + Hash, + S: BuildHasher, + { + fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + if ops.has_malloc_enclosing_size_of() { + // The first value from the iterator gives us an interior pointer. + // `ops.malloc_enclosing_size_of()` then gives us the storage size. + // This assumes that the `HashSet`'s contents (values and hashes) + // are all stored in a single contiguous heap allocation. + self.iter() + .next() + .map_or(0, |t| unsafe { ops.malloc_enclosing_size_of(t) }) + } else { + // An estimate. + self.capacity() * (size_of::() + size_of::()) + } + } + } + + impl MallocSizeOf for $ty + where + T: Eq + Hash + MallocSizeOf, + S: BuildHasher, + { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + let mut n = self.shallow_size_of(ops); + for t in self.iter() { + n += t.size_of(ops); + } + n + } + } + }; +} + +malloc_size_of_hash_set!(std::collections::HashSet); + +macro_rules! malloc_size_of_hash_map { + ($ty:ty) => { + impl MallocShallowSizeOf for $ty + where + K: Eq + Hash, + S: BuildHasher, + { + fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + // See the implementation for std::collections::HashSet for details. + if ops.has_malloc_enclosing_size_of() { + self.values() + .next() + .map_or(0, |v| unsafe { ops.malloc_enclosing_size_of(v) }) + } else { + self.capacity() * (size_of::() + size_of::() + size_of::()) + } + } + } + + impl MallocSizeOf for $ty + where + K: Eq + Hash + MallocSizeOf, + V: MallocSizeOf, + S: BuildHasher, + { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + let mut n = self.shallow_size_of(ops); + for (k, v) in self.iter() { + n += k.size_of(ops); + n += v.size_of(ops); + } + n + } + } + }; +} + +malloc_size_of_hash_map!(std::collections::HashMap); + +impl MallocShallowSizeOf for std::collections::BTreeMap +where + K: Eq + Hash, +{ + fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + if ops.has_malloc_enclosing_size_of() { + self.values() + .next() + .map_or(0, |v| unsafe { ops.malloc_enclosing_size_of(v) }) + } else { + self.len() * (size_of::() + size_of::() + size_of::()) + } + } +} + +impl MallocSizeOf for std::collections::BTreeMap +where + K: Eq + Hash + MallocSizeOf, + V: MallocSizeOf, +{ + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + let mut n = self.shallow_size_of(ops); + for (k, v) in self.iter() { + n += k.size_of(ops); + n += v.size_of(ops); + } + n + } +} + +// PhantomData is always 0. +impl MallocSizeOf for std::marker::PhantomData { + fn size_of(&self, _ops: &mut MallocSizeOfOps) -> usize { + 0 + } +} + +// XXX: we don't want MallocSizeOf to be defined for Rc and Arc. If negative +// trait bounds are ever allowed, this code should be uncommented. +// (We do have a compile-fail test for this: +// rc_arc_must_not_derive_malloc_size_of.rs) +//impl !MallocSizeOf for Arc { } +//impl !MallocShallowSizeOf for Arc { } + +impl MallocUnconditionalShallowSizeOf for servo_arc::Arc { + fn unconditional_shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + unsafe { ops.malloc_size_of(self.heap_ptr()) } + } +} + +impl MallocUnconditionalSizeOf for servo_arc::Arc { + fn unconditional_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + self.unconditional_shallow_size_of(ops) + (**self).size_of(ops) + } +} + +impl MallocConditionalShallowSizeOf for servo_arc::Arc { + fn conditional_shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + if ops.have_seen_ptr(self.heap_ptr()) { + 0 + } else { + self.unconditional_shallow_size_of(ops) + } + } +} + +impl MallocConditionalSizeOf for servo_arc::Arc { + fn conditional_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + if ops.have_seen_ptr(self.heap_ptr()) { + 0 + } else { + self.unconditional_size_of(ops) + } + } +} + +/// If a mutex is stored directly as a member of a data type that is being measured, +/// it is the unique owner of its contents and deserves to be measured. +/// +/// If a mutex is stored inside of an Arc value as a member of a data type that is being measured, +/// the Arc will not be automatically measured so there is no risk of overcounting the mutex's +/// contents. +impl MallocSizeOf for std::sync::Mutex { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + (*self.lock().unwrap()).size_of(ops) + } +} + +impl MallocSizeOf for smallbitvec::SmallBitVec { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + if let Some(ptr) = self.heap_ptr() { + unsafe { ops.malloc_size_of(ptr) } + } else { + 0 + } + } +} + +impl MallocSizeOf for euclid::Length { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + self.0.size_of(ops) + } +} + +impl MallocSizeOf for euclid::Scale { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + self.0.size_of(ops) + } +} + +impl MallocSizeOf for euclid::Point2D { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + self.x.size_of(ops) + self.y.size_of(ops) + } +} + +impl MallocSizeOf for euclid::Rect { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + self.origin.size_of(ops) + self.size.size_of(ops) + } +} + +impl MallocSizeOf for euclid::SideOffsets2D { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + self.top.size_of(ops) + + self.right.size_of(ops) + + self.bottom.size_of(ops) + + self.left.size_of(ops) + } +} + +impl MallocSizeOf for euclid::Size2D { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + self.width.size_of(ops) + self.height.size_of(ops) + } +} + +impl MallocSizeOf for euclid::Transform2D { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + self.m11.size_of(ops) + + self.m12.size_of(ops) + + self.m21.size_of(ops) + + self.m22.size_of(ops) + + self.m31.size_of(ops) + + self.m32.size_of(ops) + } +} + +impl MallocSizeOf for euclid::Transform3D { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + self.m11.size_of(ops) + + self.m12.size_of(ops) + + self.m13.size_of(ops) + + self.m14.size_of(ops) + + self.m21.size_of(ops) + + self.m22.size_of(ops) + + self.m23.size_of(ops) + + self.m24.size_of(ops) + + self.m31.size_of(ops) + + self.m32.size_of(ops) + + self.m33.size_of(ops) + + self.m34.size_of(ops) + + self.m41.size_of(ops) + + self.m42.size_of(ops) + + self.m43.size_of(ops) + + self.m44.size_of(ops) + } +} + +impl MallocSizeOf for euclid::Vector2D { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + self.x.size_of(ops) + self.y.size_of(ops) + } +} + +impl MallocSizeOf for selectors::parser::AncestorHashes { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + let selectors::parser::AncestorHashes { ref packed_hashes } = *self; + packed_hashes.size_of(ops) + } +} + +impl MallocUnconditionalSizeOf + for selectors::parser::Selector +where + Impl::NonTSPseudoClass: MallocSizeOf, + Impl::PseudoElement: MallocSizeOf, +{ + fn unconditional_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + let mut n = 0; + + // It's OK to measure this ThinArc directly because it's the + // "primary" reference. (The secondary references are on the + // Stylist.) + n += unsafe { ops.malloc_size_of(self.thin_arc_heap_ptr()) }; + for component in self.iter_raw_match_order() { + n += component.size_of(ops); + } + + n + } +} + +impl MallocUnconditionalSizeOf + for selectors::parser::SelectorList +where + Impl::NonTSPseudoClass: MallocSizeOf, + Impl::PseudoElement: MallocSizeOf, +{ + fn unconditional_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + let mut n = 0; + + // It's OK to measure this ThinArc directly because it's the "primary" reference. (The + // secondary references are on the Stylist.) + n += unsafe { ops.malloc_size_of(self.thin_arc_heap_ptr()) }; + if self.len() > 1 { + for selector in self.slice().iter() { + n += selector.size_of(ops); + } + } + n + } +} + +impl MallocUnconditionalSizeOf + for selectors::parser::Component +where + Impl::NonTSPseudoClass: MallocSizeOf, + Impl::PseudoElement: MallocSizeOf, +{ + fn unconditional_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + use selectors::parser::Component; + + match self { + Component::AttributeOther(ref attr_selector) => attr_selector.size_of(ops), + Component::Negation(ref components) => components.unconditional_size_of(ops), + Component::NonTSPseudoClass(ref pseudo) => (*pseudo).size_of(ops), + Component::Slotted(ref selector) | Component::Host(Some(ref selector)) => { + selector.unconditional_size_of(ops) + }, + Component::Is(ref list) | Component::Where(ref list) => list.unconditional_size_of(ops), + Component::Has(ref relative_selectors) => relative_selectors.size_of(ops), + Component::NthOf(ref nth_of_data) => nth_of_data.size_of(ops), + Component::PseudoElement(ref pseudo) => (*pseudo).size_of(ops), + Component::Combinator(..) | + Component::ExplicitAnyNamespace | + Component::ExplicitNoNamespace | + Component::DefaultNamespace(..) | + Component::Namespace(..) | + Component::ExplicitUniversalType | + Component::LocalName(..) | + Component::ID(..) | + Component::Part(..) | + Component::Class(..) | + Component::AttributeInNoNamespaceExists { .. } | + Component::AttributeInNoNamespace { .. } | + Component::Root | + Component::Empty | + Component::Scope | + Component::ParentSelector | + Component::Nth(..) | + Component::Host(None) | + Component::RelativeSelectorAnchor | + Component::Invalid(..) => 0, + } + } +} + +impl MallocSizeOf + for selectors::attr::AttrSelectorWithOptionalNamespace +{ + fn size_of(&self, _ops: &mut MallocSizeOfOps) -> usize { + 0 + } +} + +impl MallocSizeOf for Void { + #[inline] + fn size_of(&self, _ops: &mut MallocSizeOfOps) -> usize { + void::unreachable(*self) + } +} + +#[cfg(feature = "servo")] +impl MallocSizeOf for string_cache::Atom { + fn size_of(&self, _ops: &mut MallocSizeOfOps) -> usize { + 0 + } +} + +/// For use on types where size_of() returns 0. +#[macro_export] +macro_rules! malloc_size_of_is_0( + ($($ty:ty),+) => ( + $( + impl $crate::MallocSizeOf for $ty { + #[inline(always)] + fn size_of(&self, _: &mut $crate::MallocSizeOfOps) -> usize { + 0 + } + } + )+ + ); + ($($ty:ident<$($gen:ident),+>),+) => ( + $( + impl<$($gen: $crate::MallocSizeOf),+> $crate::MallocSizeOf for $ty<$($gen),+> { + #[inline(always)] + fn size_of(&self, _: &mut $crate::MallocSizeOfOps) -> usize { + 0 + } + } + )+ + ); +); + +malloc_size_of_is_0!(bool, char, str); +malloc_size_of_is_0!(u8, u16, u32, u64, u128, usize); +malloc_size_of_is_0!(i8, i16, i32, i64, i128, isize); +malloc_size_of_is_0!(f32, f64); + +malloc_size_of_is_0!(std::sync::atomic::AtomicBool); +malloc_size_of_is_0!(std::sync::atomic::AtomicIsize); +malloc_size_of_is_0!(std::sync::atomic::AtomicUsize); +malloc_size_of_is_0!(std::num::NonZeroUsize); + +malloc_size_of_is_0!(Range, Range, Range, Range, Range); +malloc_size_of_is_0!(Range, Range, Range, Range, Range); +malloc_size_of_is_0!(Range, Range); + +malloc_size_of_is_0!(app_units::Au); + +malloc_size_of_is_0!(cssparser::TokenSerializationType, cssparser::SourceLocation, cssparser::SourcePosition); + +malloc_size_of_is_0!(dom::ElementState, dom::DocumentState); + +#[cfg(feature = "servo")] +malloc_size_of_is_0!(csp::Destination); + +#[cfg(feature = "servo")] +malloc_size_of_is_0!(Uuid); + +#[cfg(feature = "url")] +impl MallocSizeOf for url::Host { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + match *self { + url::Host::Domain(ref s) => s.size_of(ops), + _ => 0, + } + } +} +#[cfg(feature = "webrender_api")] +malloc_size_of_is_0!(webrender_api::BorderRadius); +#[cfg(feature = "webrender_api")] +malloc_size_of_is_0!(webrender_api::BorderStyle); +#[cfg(feature = "webrender_api")] +malloc_size_of_is_0!(webrender_api::BoxShadowClipMode); +#[cfg(feature = "webrender_api")] +malloc_size_of_is_0!(webrender_api::ColorF); +#[cfg(feature = "webrender_api")] +malloc_size_of_is_0!(webrender_api::ComplexClipRegion); +#[cfg(feature = "webrender_api")] +malloc_size_of_is_0!(webrender_api::ExtendMode); +#[cfg(feature = "webrender_api")] +malloc_size_of_is_0!(webrender_api::FilterOp); +#[cfg(feature = "webrender_api")] +malloc_size_of_is_0!(webrender_api::ExternalScrollId); +#[cfg(feature = "webrender_api")] +malloc_size_of_is_0!(webrender_api::FontInstanceKey); +#[cfg(feature = "webrender_api")] +malloc_size_of_is_0!(webrender_api::GradientStop); +#[cfg(feature = "webrender_api")] +malloc_size_of_is_0!(webrender_api::GlyphInstance); +#[cfg(feature = "webrender_api")] +malloc_size_of_is_0!(webrender_api::NinePatchBorder); +#[cfg(feature = "webrender_api")] +malloc_size_of_is_0!(webrender_api::ImageKey); +#[cfg(feature = "webrender_api")] +malloc_size_of_is_0!(webrender_api::ImageRendering); +#[cfg(feature = "webrender_api")] +malloc_size_of_is_0!(webrender_api::LineStyle); +#[cfg(feature = "webrender_api")] +malloc_size_of_is_0!(webrender_api::MixBlendMode); +#[cfg(feature = "webrender_api")] +malloc_size_of_is_0!(webrender_api::NormalBorder); +#[cfg(feature = "webrender_api")] +malloc_size_of_is_0!(webrender_api::RepeatMode); +#[cfg(feature = "webrender_api")] +malloc_size_of_is_0!(webrender_api::StickyOffsetBounds); +#[cfg(feature = "webrender_api")] +malloc_size_of_is_0!(webrender_api::TransformStyle); + +#[cfg(feature = "servo")] +impl MallocSizeOf for keyboard_types::Key { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + match self { + keyboard_types::Key::Character(ref s) => s.size_of(ops), + _ => 0, + } + } +} + +#[cfg(feature = "servo")] +malloc_size_of_is_0!(keyboard_types::Modifiers); + +#[cfg(feature = "servo")] +impl MallocSizeOf for xml5ever::QualName { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + self.prefix.size_of(ops) + self.ns.size_of(ops) + self.local.size_of(ops) + } +} + +#[cfg(feature = "servo")] +malloc_size_of_is_0!(time::Duration); +#[cfg(feature = "servo")] +malloc_size_of_is_0!(time::Tm); + +#[cfg(feature = "servo")] +impl MallocSizeOf for hyper_serde::Serde +where + for<'de> hyper_serde::De: serde::Deserialize<'de>, + for<'a> hyper_serde::Ser<'a, T>: serde::Serialize, + T: MallocSizeOf, +{ + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + self.0.size_of(ops) + } +} + +// Placeholder for unique case where internals of Sender cannot be measured. +// malloc size of is 0 macro complains about type supplied! +#[cfg(feature = "servo")] +impl MallocSizeOf for crossbeam_channel::Sender { + fn size_of(&self, _ops: &mut MallocSizeOfOps) -> usize { + 0 + } +} + +#[cfg(feature = "servo")] +impl MallocSizeOf for hyper::StatusCode { + fn size_of(&self, _ops: &mut MallocSizeOfOps) -> usize { + 0 + } +} + +/// Measurable that defers to inner value and used to verify MallocSizeOf implementation in a +/// struct. +#[derive(Clone)] +pub struct Measurable(pub T); + +impl Deref for Measurable { + type Target = T; + + fn deref(&self) -> &T { + &self.0 + } +} + +impl DerefMut for Measurable { + fn deref_mut(&mut self) -> &mut T { + &mut self.0 + } +} + +#[cfg(feature = "servo")] +impl MallocSizeOf for accountable_refcell::RefCell { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + self.borrow().size_of(ops) + } +} diff --git a/servo/components/selectors/CHANGES.md b/servo/components/selectors/CHANGES.md new file mode 100644 index 0000000000..b1e9511dca --- /dev/null +++ b/servo/components/selectors/CHANGES.md @@ -0,0 +1 @@ +- `parser.rs` no longer wraps values in quotes (`"..."`) but expects their `to_css` impl to already wrap it ([Gecko Bug 1854809](https://bugzilla.mozilla.org/show_bug.cgi?id=1854809)) diff --git a/servo/components/selectors/Cargo.toml b/servo/components/selectors/Cargo.toml new file mode 100644 index 0000000000..88360d7dd2 --- /dev/null +++ b/servo/components/selectors/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "selectors" +version = "0.22.0" +authors = ["The Servo Project Developers"] +documentation = "https://docs.rs/selectors/" +description = "CSS Selectors matching for Rust" +repository = "https://github.com/servo/servo" +readme = "README.md" +keywords = ["css", "selectors"] +license = "MPL-2.0" +build = "build.rs" + +[lib] +name = "selectors" +path = "lib.rs" + +[features] +bench = [] + +[dependencies] +bitflags = "2" +cssparser = "0.33" +derive_more = { version = "0.99", default-features = false, features = ["add", "add_assign"] } +fxhash = "0.2" +log = "0.4" +phf = "0.11" +precomputed-hash = "0.1" +servo_arc = { version = "0.1", path = "../servo_arc" } +smallvec = "1.0" +to_shmem = { path = "../to_shmem" } +to_shmem_derive = { path = "../to_shmem_derive" } +new_debug_unreachable = "1" + +[build-dependencies] +phf_codegen = "0.11" diff --git a/servo/components/selectors/README.md b/servo/components/selectors/README.md new file mode 100644 index 0000000000..dac4a7ff91 --- /dev/null +++ b/servo/components/selectors/README.md @@ -0,0 +1,25 @@ +rust-selectors +============== + +* [![Build Status](https://travis-ci.com/servo/rust-selectors.svg?branch=master)]( + https://travis-ci.com/servo/rust-selectors) +* [Documentation](https://docs.rs/selectors/) +* [crates.io](https://crates.io/crates/selectors) + +CSS Selectors library for Rust. +Includes parsing and serilization of selectors, +as well as matching against a generic tree of elements. +Pseudo-elements and most pseudo-classes are generic as well. + +**Warning:** breaking changes are made to this library fairly frequently +(13 times in 2016, for example). +However you can use this crate without updating it that often, +old versions stay available on crates.io and Cargo will only automatically update +to versions that are numbered as compatible. + +To see how to use this library with your own tree representation, +see [Kuchiki’s `src/select.rs`](https://github.com/kuchiki-rs/kuchiki/blob/master/src/select.rs). +(Note however that Kuchiki is not always up to date with the latest rust-selectors version, +so that code may need to be tweaked.) +If you don’t already have a tree data structure, +consider using [Kuchiki](https://github.com/kuchiki-rs/kuchiki) itself. diff --git a/servo/components/selectors/attr.rs b/servo/components/selectors/attr.rs new file mode 100644 index 0000000000..fee2962237 --- /dev/null +++ b/servo/components/selectors/attr.rs @@ -0,0 +1,183 @@ +/* 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 crate::parser::SelectorImpl; +use cssparser::ToCss; +use std::fmt; + +#[derive(Clone, Eq, PartialEq, ToShmem)] +#[shmem(no_bounds)] +pub struct AttrSelectorWithOptionalNamespace { + #[shmem(field_bound)] + pub namespace: Option>, + #[shmem(field_bound)] + pub local_name: Impl::LocalName, + pub local_name_lower: Impl::LocalName, + #[shmem(field_bound)] + pub operation: ParsedAttrSelectorOperation, +} + +impl AttrSelectorWithOptionalNamespace { + pub fn namespace(&self) -> Option> { + self.namespace.as_ref().map(|ns| match ns { + NamespaceConstraint::Any => NamespaceConstraint::Any, + NamespaceConstraint::Specific((_, ref url)) => NamespaceConstraint::Specific(url), + }) + } +} + +#[derive(Clone, Eq, PartialEq, ToShmem)] +pub enum NamespaceConstraint { + Any, + + /// Empty string for no namespace + Specific(NamespaceUrl), +} + +#[derive(Clone, Eq, PartialEq, ToShmem)] +pub enum ParsedAttrSelectorOperation { + Exists, + WithValue { + operator: AttrSelectorOperator, + case_sensitivity: ParsedCaseSensitivity, + value: AttrValue, + }, +} + +#[derive(Clone, Eq, PartialEq)] +pub enum AttrSelectorOperation { + Exists, + WithValue { + operator: AttrSelectorOperator, + case_sensitivity: CaseSensitivity, + value: AttrValue, + }, +} + +impl AttrSelectorOperation { + pub fn eval_str(&self, element_attr_value: &str) -> bool + where + AttrValue: AsRef, + { + match *self { + AttrSelectorOperation::Exists => true, + AttrSelectorOperation::WithValue { + operator, + case_sensitivity, + ref value, + } => operator.eval_str(element_attr_value, value.as_ref(), case_sensitivity), + } + } +} + +#[derive(Clone, Copy, Eq, PartialEq, ToShmem)] +pub enum AttrSelectorOperator { + Equal, + Includes, + DashMatch, + Prefix, + Substring, + Suffix, +} + +impl ToCss for AttrSelectorOperator { + fn to_css(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + // https://drafts.csswg.org/cssom/#serializing-selectors + // See "attribute selector". + dest.write_str(match *self { + AttrSelectorOperator::Equal => "=", + AttrSelectorOperator::Includes => "~=", + AttrSelectorOperator::DashMatch => "|=", + AttrSelectorOperator::Prefix => "^=", + AttrSelectorOperator::Substring => "*=", + AttrSelectorOperator::Suffix => "$=", + }) + } +} + +impl AttrSelectorOperator { + pub fn eval_str( + self, + element_attr_value: &str, + attr_selector_value: &str, + case_sensitivity: CaseSensitivity, + ) -> bool { + let e = element_attr_value.as_bytes(); + let s = attr_selector_value.as_bytes(); + let case = case_sensitivity; + match self { + AttrSelectorOperator::Equal => case.eq(e, s), + AttrSelectorOperator::Prefix => e.len() >= s.len() && case.eq(&e[..s.len()], s), + AttrSelectorOperator::Suffix => { + e.len() >= s.len() && case.eq(&e[(e.len() - s.len())..], s) + }, + AttrSelectorOperator::Substring => { + case.contains(element_attr_value, attr_selector_value) + }, + AttrSelectorOperator::Includes => element_attr_value + .split(SELECTOR_WHITESPACE) + .any(|part| case.eq(part.as_bytes(), s)), + AttrSelectorOperator::DashMatch => { + case.eq(e, s) || (e.get(s.len()) == Some(&b'-') && case.eq(&e[..s.len()], s)) + }, + } + } +} + +/// The definition of whitespace per CSS Selectors Level 3 § 4. +pub static SELECTOR_WHITESPACE: &[char] = &[' ', '\t', '\n', '\r', '\x0C']; + +#[derive(Clone, Copy, Debug, Eq, PartialEq, ToShmem)] +pub enum ParsedCaseSensitivity { + /// 's' was specified. + ExplicitCaseSensitive, + /// 'i' was specified. + AsciiCaseInsensitive, + /// No flags were specified and HTML says this is a case-sensitive attribute. + CaseSensitive, + /// No flags were specified and HTML says this is a case-insensitive attribute. + AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum CaseSensitivity { + CaseSensitive, + AsciiCaseInsensitive, +} + +impl CaseSensitivity { + pub fn eq(self, a: &[u8], b: &[u8]) -> bool { + match self { + CaseSensitivity::CaseSensitive => a == b, + CaseSensitivity::AsciiCaseInsensitive => a.eq_ignore_ascii_case(b), + } + } + + pub fn contains(self, haystack: &str, needle: &str) -> bool { + match self { + CaseSensitivity::CaseSensitive => haystack.contains(needle), + CaseSensitivity::AsciiCaseInsensitive => { + if let Some((&n_first_byte, n_rest)) = needle.as_bytes().split_first() { + haystack.bytes().enumerate().any(|(i, byte)| { + if !byte.eq_ignore_ascii_case(&n_first_byte) { + return false; + } + let after_this_byte = &haystack.as_bytes()[i + 1..]; + match after_this_byte.get(..n_rest.len()) { + None => false, + Some(haystack_slice) => haystack_slice.eq_ignore_ascii_case(n_rest), + } + }) + } else { + // any_str.contains("") == true, + // though these cases should be handled with *NeverMatches and never go here. + true + } + }, + } + } +} diff --git a/servo/components/selectors/bloom.rs b/servo/components/selectors/bloom.rs new file mode 100644 index 0000000000..98461d1ba2 --- /dev/null +++ b/servo/components/selectors/bloom.rs @@ -0,0 +1,422 @@ +/* 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/. */ + +//! Counting and non-counting Bloom filters tuned for use as ancestor filters +//! for selector matching. + +use std::fmt::{self, Debug}; + +// The top 8 bits of the 32-bit hash value are not used by the bloom filter. +// Consumers may rely on this to pack hashes more efficiently. +pub const BLOOM_HASH_MASK: u32 = 0x00ffffff; +const KEY_SIZE: usize = 12; + +const ARRAY_SIZE: usize = 1 << KEY_SIZE; +const KEY_MASK: u32 = (1 << KEY_SIZE) - 1; + +/// A counting Bloom filter with 8-bit counters. +pub type BloomFilter = CountingBloomFilter; + +/// A counting Bloom filter with parameterized storage to handle +/// counters of different sizes. For now we assume that having two hash +/// functions is enough, but we may revisit that decision later. +/// +/// The filter uses an array with 2**KeySize entries. +/// +/// Assuming a well-distributed hash function, a Bloom filter with +/// array size M containing N elements and +/// using k hash function has expected false positive rate exactly +/// +/// $ (1 - (1 - 1/M)^{kN})^k $ +/// +/// because each array slot has a +/// +/// $ (1 - 1/M)^{kN} $ +/// +/// chance of being 0, and the expected false positive rate is the +/// probability that all of the k hash functions will hit a nonzero +/// slot. +/// +/// For reasonable assumptions (M large, kN large, which should both +/// hold if we're worried about false positives) about M and kN this +/// becomes approximately +/// +/// $$ (1 - \exp(-kN/M))^k $$ +/// +/// For our special case of k == 2, that's $(1 - \exp(-2N/M))^2$, +/// or in other words +/// +/// $$ N/M = -0.5 * \ln(1 - \sqrt(r)) $$ +/// +/// where r is the false positive rate. This can be used to compute +/// the desired KeySize for a given load N and false positive rate r. +/// +/// If N/M is assumed small, then the false positive rate can +/// further be approximated as 4*N^2/M^2. So increasing KeySize by +/// 1, which doubles M, reduces the false positive rate by about a +/// factor of 4, and a false positive rate of 1% corresponds to +/// about M/N == 20. +/// +/// What this means in practice is that for a few hundred keys using a +/// KeySize of 12 gives false positive rates on the order of 0.25-4%. +/// +/// Similarly, using a KeySize of 10 would lead to a 4% false +/// positive rate for N == 100 and to quite bad false positive +/// rates for larger N. +#[derive(Clone, Default)] +pub struct CountingBloomFilter +where + S: BloomStorage, +{ + storage: S, +} + +impl CountingBloomFilter +where + S: BloomStorage, +{ + /// Creates a new bloom filter. + #[inline] + pub fn new() -> Self { + Default::default() + } + + #[inline] + pub fn clear(&mut self) { + self.storage = Default::default(); + } + + // Slow linear accessor to make sure the bloom filter is zeroed. This should + // never be used in release builds. + #[cfg(debug_assertions)] + pub fn is_zeroed(&self) -> bool { + self.storage.is_zeroed() + } + + #[cfg(not(debug_assertions))] + pub fn is_zeroed(&self) -> bool { + unreachable!() + } + + /// Inserts an item with a particular hash into the bloom filter. + #[inline] + pub fn insert_hash(&mut self, hash: u32) { + self.storage.adjust_first_slot(hash, true); + self.storage.adjust_second_slot(hash, true); + } + + /// Removes an item with a particular hash from the bloom filter. + #[inline] + pub fn remove_hash(&mut self, hash: u32) { + self.storage.adjust_first_slot(hash, false); + self.storage.adjust_second_slot(hash, false); + } + + /// Check whether the filter might contain an item with the given hash. + /// This can sometimes return true even if the item is not in the filter, + /// but will never return false for items that are actually in the filter. + #[inline] + pub fn might_contain_hash(&self, hash: u32) -> bool { + !self.storage.first_slot_is_empty(hash) && !self.storage.second_slot_is_empty(hash) + } +} + +impl Debug for CountingBloomFilter +where + S: BloomStorage, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mut slots_used = 0; + for i in 0..ARRAY_SIZE { + if !self.storage.slot_is_empty(i) { + slots_used += 1; + } + } + write!(f, "BloomFilter({}/{})", slots_used, ARRAY_SIZE) + } +} + +pub trait BloomStorage: Clone + Default { + fn slot_is_empty(&self, index: usize) -> bool; + fn adjust_slot(&mut self, index: usize, increment: bool); + fn is_zeroed(&self) -> bool; + + #[inline] + fn first_slot_is_empty(&self, hash: u32) -> bool { + self.slot_is_empty(Self::first_slot_index(hash)) + } + + #[inline] + fn second_slot_is_empty(&self, hash: u32) -> bool { + self.slot_is_empty(Self::second_slot_index(hash)) + } + + #[inline] + fn adjust_first_slot(&mut self, hash: u32, increment: bool) { + self.adjust_slot(Self::first_slot_index(hash), increment) + } + + #[inline] + fn adjust_second_slot(&mut self, hash: u32, increment: bool) { + self.adjust_slot(Self::second_slot_index(hash), increment) + } + + #[inline] + fn first_slot_index(hash: u32) -> usize { + hash1(hash) as usize + } + + #[inline] + fn second_slot_index(hash: u32) -> usize { + hash2(hash) as usize + } +} + +/// Storage class for a CountingBloomFilter that has 8-bit counters. +pub struct BloomStorageU8 { + counters: [u8; ARRAY_SIZE], +} + +impl BloomStorage for BloomStorageU8 { + #[inline] + fn adjust_slot(&mut self, index: usize, increment: bool) { + let slot = &mut self.counters[index]; + if *slot != 0xff { + // full + if increment { + *slot += 1; + } else { + *slot -= 1; + } + } + } + + #[inline] + fn slot_is_empty(&self, index: usize) -> bool { + self.counters[index] == 0 + } + + #[inline] + fn is_zeroed(&self) -> bool { + self.counters.iter().all(|x| *x == 0) + } +} + +impl Default for BloomStorageU8 { + fn default() -> Self { + BloomStorageU8 { + counters: [0; ARRAY_SIZE], + } + } +} + +impl Clone for BloomStorageU8 { + fn clone(&self) -> Self { + BloomStorageU8 { + counters: self.counters, + } + } +} + +/// Storage class for a CountingBloomFilter that has 1-bit counters. +pub struct BloomStorageBool { + counters: [u8; ARRAY_SIZE / 8], +} + +impl BloomStorage for BloomStorageBool { + #[inline] + fn adjust_slot(&mut self, index: usize, increment: bool) { + let bit = 1 << (index % 8); + let byte = &mut self.counters[index / 8]; + + // Since we have only one bit for storage, decrementing it + // should never do anything. Assert against an accidental + // decrementing of a bit that was never set. + assert!( + increment || (*byte & bit) != 0, + "should not decrement if slot is already false" + ); + + if increment { + *byte |= bit; + } + } + + #[inline] + fn slot_is_empty(&self, index: usize) -> bool { + let bit = 1 << (index % 8); + (self.counters[index / 8] & bit) == 0 + } + + #[inline] + fn is_zeroed(&self) -> bool { + self.counters.iter().all(|x| *x == 0) + } +} + +impl Default for BloomStorageBool { + fn default() -> Self { + BloomStorageBool { + counters: [0; ARRAY_SIZE / 8], + } + } +} + +impl Clone for BloomStorageBool { + fn clone(&self) -> Self { + BloomStorageBool { + counters: self.counters, + } + } +} + +#[inline] +fn hash1(hash: u32) -> u32 { + hash & KEY_MASK +} + +#[inline] +fn hash2(hash: u32) -> u32 { + (hash >> KEY_SIZE) & KEY_MASK +} + +#[test] +fn create_and_insert_some_stuff() { + use fxhash::FxHasher; + use std::hash::{Hash, Hasher}; + use std::mem::transmute; + + fn hash_as_str(i: usize) -> u32 { + let mut hasher = FxHasher::default(); + let s = i.to_string(); + s.hash(&mut hasher); + let hash: u64 = hasher.finish(); + (hash >> 32) as u32 ^ (hash as u32) + } + + let mut bf = BloomFilter::new(); + + // Statically assert that ARRAY_SIZE is a multiple of 8, which + // BloomStorageBool relies on. + unsafe { + transmute::<[u8; ARRAY_SIZE % 8], [u8; 0]>([]); + } + + for i in 0_usize..1000 { + bf.insert_hash(hash_as_str(i)); + } + + for i in 0_usize..1000 { + assert!(bf.might_contain_hash(hash_as_str(i))); + } + + let false_positives = (1001_usize..2000) + .filter(|i| bf.might_contain_hash(hash_as_str(*i))) + .count(); + + assert!(false_positives < 190, "{} is not < 190", false_positives); // 19%. + + for i in 0_usize..100 { + bf.remove_hash(hash_as_str(i)); + } + + for i in 100_usize..1000 { + assert!(bf.might_contain_hash(hash_as_str(i))); + } + + let false_positives = (0_usize..100) + .filter(|i| bf.might_contain_hash(hash_as_str(*i))) + .count(); + + assert!(false_positives < 20, "{} is not < 20", false_positives); // 20%. + + bf.clear(); + + for i in 0_usize..2000 { + assert!(!bf.might_contain_hash(hash_as_str(i))); + } +} + +#[cfg(feature = "bench")] +#[cfg(test)] +mod bench { + extern crate test; + use super::BloomFilter; + + #[derive(Default)] + struct HashGenerator(u32); + + impl HashGenerator { + fn next(&mut self) -> u32 { + // Each hash is split into two twelve-bit segments, which are used + // as an index into an array. We increment each by 64 so that we + // hit the next cache line, and then another 1 so that our wrapping + // behavior leads us to different entries each time. + // + // Trying to simulate cold caches is rather difficult with the cargo + // benchmarking setup, so it may all be moot depending on the number + // of iterations that end up being run. But we might as well. + self.0 += (65) + (65 << super::KEY_SIZE); + self.0 + } + } + + #[bench] + fn create_insert_1000_remove_100_lookup_100(b: &mut test::Bencher) { + b.iter(|| { + let mut gen1 = HashGenerator::default(); + let mut gen2 = HashGenerator::default(); + let mut bf = BloomFilter::new(); + for _ in 0_usize..1000 { + bf.insert_hash(gen1.next()); + } + for _ in 0_usize..100 { + bf.remove_hash(gen2.next()); + } + for _ in 100_usize..200 { + test::black_box(bf.might_contain_hash(gen2.next())); + } + }); + } + + #[bench] + fn might_contain_10(b: &mut test::Bencher) { + let bf = BloomFilter::new(); + let mut gen = HashGenerator::default(); + b.iter(|| { + for _ in 0..10 { + test::black_box(bf.might_contain_hash(gen.next())); + } + }); + } + + #[bench] + fn clear(b: &mut test::Bencher) { + let mut bf = Box::new(BloomFilter::new()); + b.iter(|| test::black_box(&mut bf).clear()); + } + + #[bench] + fn insert_10(b: &mut test::Bencher) { + let mut bf = BloomFilter::new(); + let mut gen = HashGenerator::default(); + b.iter(|| { + for _ in 0..10 { + test::black_box(bf.insert_hash(gen.next())); + } + }); + } + + #[bench] + fn remove_10(b: &mut test::Bencher) { + let mut bf = BloomFilter::new(); + let mut gen = HashGenerator::default(); + // Note: this will underflow, and that's ok. + b.iter(|| { + for _ in 0..10 { + bf.remove_hash(gen.next()) + } + }); + } +} diff --git a/servo/components/selectors/build.rs b/servo/components/selectors/build.rs new file mode 100644 index 0000000000..c5c3803991 --- /dev/null +++ b/servo/components/selectors/build.rs @@ -0,0 +1,77 @@ +/* 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/. */ + +extern crate phf_codegen; + +use std::env; +use std::fs::File; +use std::io::{BufWriter, Write}; +use std::path::Path; + +fn main() { + let path = Path::new(&env::var_os("OUT_DIR").unwrap()) + .join("ascii_case_insensitive_html_attributes.rs"); + let mut file = BufWriter::new(File::create(&path).unwrap()); + + let mut set = phf_codegen::Set::new(); + for name in ASCII_CASE_INSENSITIVE_HTML_ATTRIBUTES.split_whitespace() { + set.entry(name); + } + write!( + &mut file, + "{{ static SET: ::phf::Set<&'static str> = {}; &SET }}", + set.build(), + ) + .unwrap(); +} + +/// +static ASCII_CASE_INSENSITIVE_HTML_ATTRIBUTES: &str = r#" + accept + accept-charset + align + alink + axis + bgcolor + charset + checked + clear + codetype + color + compact + declare + defer + dir + direction + disabled + enctype + face + frame + hreflang + http-equiv + lang + language + link + media + method + multiple + nohref + noresize + noshade + nowrap + readonly + rel + rev + rules + scope + scrolling + selected + shape + target + text + type + valign + valuetype + vlink +"#; diff --git a/servo/components/selectors/builder.rs b/servo/components/selectors/builder.rs new file mode 100644 index 0000000000..2406cd84f6 --- /dev/null +++ b/servo/components/selectors/builder.rs @@ -0,0 +1,391 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Helper module to build up a selector safely and efficiently. +//! +//! Our selector representation is designed to optimize matching, and has +//! several requirements: +//! * All simple selectors and combinators are stored inline in the same buffer +//! as Component instances. +//! * We store the top-level compound selectors from right to left, i.e. in +//! matching order. +//! * We store the simple selectors for each combinator from left to right, so +//! that we match the cheaper simple selectors first. +//! +//! Meeting all these constraints without extra memmove traffic during parsing +//! is non-trivial. This module encapsulates those details and presents an +//! easy-to-use API for the parser. + +use crate::parser::{Combinator, Component, RelativeSelector, Selector, SelectorImpl}; +use crate::sink::Push; +use servo_arc::{Arc, ThinArc}; +use smallvec::{self, SmallVec}; +use std::cmp; +use std::iter; +use std::ptr; +use std::slice; + +/// Top-level SelectorBuilder struct. This should be stack-allocated by the +/// consumer and never moved (because it contains a lot of inline data that +/// would be slow to memmov). +/// +/// After instantation, callers may call the push_simple_selector() and +/// push_combinator() methods to append selector data as it is encountered +/// (from left to right). Once the process is complete, callers should invoke +/// build(), which transforms the contents of the SelectorBuilder into a heap- +/// allocated Selector and leaves the builder in a drained state. +#[derive(Debug)] +pub struct SelectorBuilder { + /// The entire sequence of simple selectors, from left to right, without combinators. + /// + /// We make this large because the result of parsing a selector is fed into a new + /// Arc-ed allocation, so any spilled vec would be a wasted allocation. Also, + /// Components are large enough that we don't have much cache locality benefit + /// from reserving stack space for fewer of them. + simple_selectors: SmallVec<[Component; 32]>, + /// The combinators, and the length of the compound selector to their left. + combinators: SmallVec<[(Combinator, usize); 16]>, + /// The length of the current compount selector. + current_len: usize, +} + +impl Default for SelectorBuilder { + #[inline(always)] + fn default() -> Self { + SelectorBuilder { + simple_selectors: SmallVec::new(), + combinators: SmallVec::new(), + current_len: 0, + } + } +} + +impl Push> for SelectorBuilder { + fn push(&mut self, value: Component) { + self.push_simple_selector(value); + } +} + +impl SelectorBuilder { + /// Pushes a simple selector onto the current compound selector. + #[inline(always)] + pub fn push_simple_selector(&mut self, ss: Component) { + assert!(!ss.is_combinator()); + self.simple_selectors.push(ss); + self.current_len += 1; + } + + /// Completes the current compound selector and starts a new one, delimited + /// by the given combinator. + #[inline(always)] + pub fn push_combinator(&mut self, c: Combinator) { + self.combinators.push((c, self.current_len)); + self.current_len = 0; + } + + /// Returns true if combinators have ever been pushed to this builder. + #[inline(always)] + pub fn has_combinators(&self) -> bool { + !self.combinators.is_empty() + } + + /// Consumes the builder, producing a Selector. + #[inline(always)] + pub fn build(&mut self) -> ThinArc> { + // Compute the specificity and flags. + let sf = specificity_and_flags(self.simple_selectors.iter()); + self.build_with_specificity_and_flags(sf) + } + + /// Builds with an explicit SpecificityAndFlags. This is separated from build() so + /// that unit tests can pass an explicit specificity. + #[inline(always)] + pub(crate) fn build_with_specificity_and_flags( + &mut self, + spec: SpecificityAndFlags, + ) -> ThinArc> { + // Create the Arc using an iterator that drains our buffers. + // Panic-safety: if SelectorBuilderIter is not iterated to the end, some simple selectors + // will safely leak. + let raw_simple_selectors = unsafe { + let simple_selectors_len = self.simple_selectors.len(); + self.simple_selectors.set_len(0); + std::slice::from_raw_parts(self.simple_selectors.as_ptr(), simple_selectors_len) + }; + let (rest, current) = split_from_end(raw_simple_selectors, self.current_len); + let iter = SelectorBuilderIter { + current_simple_selectors: current.iter(), + rest_of_simple_selectors: rest, + combinators: self.combinators.drain(..).rev(), + }; + + Arc::from_header_and_iter(spec, iter) + } +} + +struct SelectorBuilderIter<'a, Impl: SelectorImpl> { + current_simple_selectors: slice::Iter<'a, Component>, + rest_of_simple_selectors: &'a [Component], + combinators: iter::Rev>, +} + +impl<'a, Impl: SelectorImpl> ExactSizeIterator for SelectorBuilderIter<'a, Impl> { + fn len(&self) -> usize { + self.current_simple_selectors.len() + + self.rest_of_simple_selectors.len() + + self.combinators.len() + } +} + +impl<'a, Impl: SelectorImpl> Iterator for SelectorBuilderIter<'a, Impl> { + type Item = Component; + #[inline(always)] + fn next(&mut self) -> Option { + if let Some(simple_selector_ref) = self.current_simple_selectors.next() { + // Move a simple selector out of this slice iterator. + // This is safe because we’ve called SmallVec::set_len(0) above, + // so SmallVec::drop won’t drop this simple selector. + unsafe { Some(ptr::read(simple_selector_ref)) } + } else { + self.combinators.next().map(|(combinator, len)| { + let (rest, current) = split_from_end(self.rest_of_simple_selectors, len); + self.rest_of_simple_selectors = rest; + self.current_simple_selectors = current.iter(); + Component::Combinator(combinator) + }) + } + } + + fn size_hint(&self) -> (usize, Option) { + (self.len(), Some(self.len())) + } +} + +fn split_from_end(s: &[T], at: usize) -> (&[T], &[T]) { + s.split_at(s.len() - at) +} + +/// Flags that indicate at which point of parsing a selector are we. +#[derive(Clone, Copy, Default, Eq, PartialEq, ToShmem)] +pub(crate) struct SelectorFlags(u8); + +bitflags! { + impl SelectorFlags: u8 { + const HAS_PSEUDO = 1 << 0; + const HAS_SLOTTED = 1 << 1; + const HAS_PART = 1 << 2; + const HAS_PARENT = 1 << 3; + const HAS_NON_FEATURELESS_COMPONENT = 1 << 4; + const HAS_HOST = 1 << 5; + } +} + +impl core::fmt::Debug for SelectorFlags { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + if self.is_empty() { + write!(f, "{:#x}", Self::empty().bits()) + } else { + bitflags::parser::to_writer(self, f) + } + } +} + +impl SelectorFlags { + /// When you nest a pseudo-element with something like: + /// + /// ::before { & { .. } } + /// + /// It is not supposed to work, because :is(::before) is invalid. We can't propagate the + /// pseudo-flags from inner to outer selectors, to avoid breaking our invariants. + pub(crate) fn for_nesting() -> Self { + Self::all() - (Self::HAS_PSEUDO | Self::HAS_SLOTTED | Self::HAS_PART) + } +} + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ToShmem)] +pub struct SpecificityAndFlags { + /// There are two free bits here, since we use ten bits for each specificity + /// kind (id, class, element). + pub(crate) specificity: u32, + /// There's padding after this field due to the size of the flags. + pub(crate) flags: SelectorFlags, +} + +const MAX_10BIT: u32 = (1u32 << 10) - 1; + +#[derive(Add, AddAssign, Clone, Copy, Default, Eq, Ord, PartialEq, PartialOrd)] +pub(crate) struct Specificity { + id_selectors: u32, + class_like_selectors: u32, + element_selectors: u32, +} + +impl From for Specificity { + #[inline] + fn from(value: u32) -> Specificity { + assert!(value <= MAX_10BIT << 20 | MAX_10BIT << 10 | MAX_10BIT); + Specificity { + id_selectors: value >> 20, + class_like_selectors: (value >> 10) & MAX_10BIT, + element_selectors: value & MAX_10BIT, + } + } +} + +impl From for u32 { + #[inline] + fn from(specificity: Specificity) -> u32 { + cmp::min(specificity.id_selectors, MAX_10BIT) << 20 | + cmp::min(specificity.class_like_selectors, MAX_10BIT) << 10 | + cmp::min(specificity.element_selectors, MAX_10BIT) + } +} + +pub(crate) fn specificity_and_flags(iter: slice::Iter>) -> SpecificityAndFlags +where + Impl: SelectorImpl, +{ + complex_selector_specificity_and_flags(iter).into() +} + +fn complex_selector_specificity_and_flags( + iter: slice::Iter>, +) -> SpecificityAndFlags +where + Impl: SelectorImpl, +{ + fn component_specificity( + simple_selector: &Component, + specificity: &mut Specificity, + flags: &mut SelectorFlags, + ) where + Impl: SelectorImpl, + { + match *simple_selector { + Component::Combinator(..) => {}, + Component::ParentSelector => flags.insert(SelectorFlags::HAS_PARENT), + Component::Part(..) => { + flags.insert(SelectorFlags::HAS_PART); + specificity.element_selectors += 1 + }, + Component::PseudoElement(..) => { + flags.insert(SelectorFlags::HAS_PSEUDO); + specificity.element_selectors += 1 + }, + Component::LocalName(..) => { + flags.insert(SelectorFlags::HAS_NON_FEATURELESS_COMPONENT); + specificity.element_selectors += 1 + }, + Component::Slotted(ref selector) => { + flags.insert( + SelectorFlags::HAS_SLOTTED | SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + ); + specificity.element_selectors += 1; + // Note that due to the way ::slotted works we only compete with + // other ::slotted rules, so the above rule doesn't really + // matter, but we do it still for consistency with other + // pseudo-elements. + // + // See: https://github.com/w3c/csswg-drafts/issues/1915 + *specificity += Specificity::from(selector.specificity()); + flags.insert(selector.flags()); + }, + Component::Host(ref selector) => { + flags.insert(SelectorFlags::HAS_HOST); + specificity.class_like_selectors += 1; + if let Some(ref selector) = *selector { + // See: https://github.com/w3c/csswg-drafts/issues/1915 + *specificity += Specificity::from(selector.specificity()); + flags.insert(selector.flags() - SelectorFlags::HAS_NON_FEATURELESS_COMPONENT); + } + }, + Component::ID(..) => { + flags.insert(SelectorFlags::HAS_NON_FEATURELESS_COMPONENT); + specificity.id_selectors += 1; + }, + Component::Class(..) | + Component::AttributeInNoNamespace { .. } | + Component::AttributeInNoNamespaceExists { .. } | + Component::AttributeOther(..) | + Component::Root | + Component::Empty | + Component::Scope | + Component::Nth(..) | + Component::NonTSPseudoClass(..) => { + flags.insert(SelectorFlags::HAS_NON_FEATURELESS_COMPONENT); + specificity.class_like_selectors += 1; + }, + Component::NthOf(ref nth_of_data) => { + // https://drafts.csswg.org/selectors/#specificity-rules: + // + // The specificity of the :nth-last-child() pseudo-class, + // like the :nth-child() pseudo-class, combines the + // specificity of a regular pseudo-class with that of its + // selector argument S. + specificity.class_like_selectors += 1; + let sf = selector_list_specificity_and_flags(nth_of_data.selectors().iter()); + *specificity += Specificity::from(sf.specificity); + flags.insert(sf.flags | SelectorFlags::HAS_NON_FEATURELESS_COMPONENT); + }, + // https://drafts.csswg.org/selectors/#specificity-rules: + // + // The specificity of an :is(), :not(), or :has() pseudo-class + // is replaced by the specificity of the most specific complex + // selector in its selector list argument. + Component::Where(ref list) | + Component::Negation(ref list) | + Component::Is(ref list) => { + let sf = selector_list_specificity_and_flags(list.slice().iter()); + if !matches!(*simple_selector, Component::Where(..)) { + *specificity += Specificity::from(sf.specificity); + } + flags.insert(sf.flags); + }, + Component::Has(ref relative_selectors) => { + let sf = relative_selector_list_specificity_and_flags(relative_selectors); + *specificity += Specificity::from(sf.specificity); + flags.insert(sf.flags | SelectorFlags::HAS_NON_FEATURELESS_COMPONENT); + }, + Component::ExplicitUniversalType | + Component::ExplicitAnyNamespace | + Component::ExplicitNoNamespace | + Component::DefaultNamespace(..) | + Component::Namespace(..) | + Component::RelativeSelectorAnchor | + Component::Invalid(..) => { + // Does not affect specificity + flags.insert(SelectorFlags::HAS_NON_FEATURELESS_COMPONENT); + }, + } + } + + let mut specificity = Default::default(); + let mut flags = Default::default(); + for simple_selector in iter { + component_specificity(&simple_selector, &mut specificity, &mut flags); + } + SpecificityAndFlags { + specificity: specificity.into(), + flags, + } +} + +/// Finds the maximum specificity of elements in the list and returns it. +pub(crate) fn selector_list_specificity_and_flags<'a, Impl: SelectorImpl>( + itr: impl Iterator>, +) -> SpecificityAndFlags { + let mut specificity = 0; + let mut flags = SelectorFlags::empty(); + for selector in itr { + specificity = std::cmp::max(specificity, selector.specificity()); + flags.insert(selector.flags()); + } + SpecificityAndFlags { specificity, flags } +} + +pub(crate) fn relative_selector_list_specificity_and_flags( + list: &[RelativeSelector], +) -> SpecificityAndFlags { + selector_list_specificity_and_flags(list.iter().map(|rel| &rel.selector)) +} diff --git a/servo/components/selectors/context.rs b/servo/components/selectors/context.rs new file mode 100644 index 0000000000..84ee262dfe --- /dev/null +++ b/servo/components/selectors/context.rs @@ -0,0 +1,418 @@ +/* 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 crate::attr::CaseSensitivity; +use crate::bloom::BloomFilter; +use crate::nth_index_cache::{NthIndexCache, NthIndexCacheInner}; +use crate::parser::{Selector, SelectorImpl}; +use crate::relative_selector::cache::RelativeSelectorCache; +use crate::relative_selector::filter::RelativeSelectorFilterMap; +use crate::tree::{Element, OpaqueElement}; + +/// What kind of selector matching mode we should use. +/// +/// There are two modes of selector matching. The difference is only noticeable +/// in presence of pseudo-elements. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum MatchingMode { + /// Don't ignore any pseudo-element selectors. + Normal, + + /// Ignores any stateless pseudo-element selectors in the rightmost sequence + /// of simple selectors. + /// + /// This is useful, for example, to match against ::before when you aren't a + /// pseudo-element yourself. + /// + /// For example, in presence of `::before:hover`, it would never match, but + /// `::before` would be ignored as in "matching". + /// + /// It's required for all the selectors you match using this mode to have a + /// pseudo-element. + ForStatelessPseudoElement, +} + +/// The mode to use when matching unvisited and visited links. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum VisitedHandlingMode { + /// All links are matched as if they are unvisted. + AllLinksUnvisited, + /// All links are matched as if they are visited and unvisited (both :link + /// and :visited match). + /// + /// This is intended to be used from invalidation code, to be conservative + /// about whether we need to restyle a link. + AllLinksVisitedAndUnvisited, + /// A element's "relevant link" is the element being matched if it is a link + /// or the nearest ancestor link. The relevant link is matched as though it + /// is visited, and all other links are matched as if they are unvisited. + RelevantLinkVisited, +} + +impl VisitedHandlingMode { + #[inline] + pub fn matches_visited(&self) -> bool { + matches!( + *self, + VisitedHandlingMode::RelevantLinkVisited | + VisitedHandlingMode::AllLinksVisitedAndUnvisited + ) + } + + #[inline] + pub fn matches_unvisited(&self) -> bool { + matches!( + *self, + VisitedHandlingMode::AllLinksUnvisited | + VisitedHandlingMode::AllLinksVisitedAndUnvisited + ) + } +} + +/// Whether we need to set selector invalidation flags on elements for this +/// match request. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum NeedsSelectorFlags { + No, + Yes, +} + +/// Whether we're matching in the contect of invalidation. +#[derive(PartialEq)] +pub enum MatchingForInvalidation { + No, + Yes, +} + +/// Which quirks mode is this document in. +/// +/// See: https://quirks.spec.whatwg.org/ +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum QuirksMode { + /// Quirks mode. + Quirks, + /// Limited quirks mode. + LimitedQuirks, + /// No quirks mode. + NoQuirks, +} + +impl QuirksMode { + #[inline] + pub fn classes_and_ids_case_sensitivity(self) -> CaseSensitivity { + match self { + QuirksMode::NoQuirks | QuirksMode::LimitedQuirks => CaseSensitivity::CaseSensitive, + QuirksMode::Quirks => CaseSensitivity::AsciiCaseInsensitive, + } + } +} + +/// Whether or not this matching considered relative selector. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum RelativeSelectorMatchingState { + /// Was not considered for any relative selector. + None, + /// Relative selector was considered for a match, but the element to be + /// under matching would not anchor the relative selector. i.e. The + /// relative selector was not part of the first compound selector (in match + /// order). + Considered, + /// Same as above, but the relative selector was part of the first compound + /// selector (in match order). + ConsideredAnchor, +} + +impl RelativeSelectorMatchingState { + /// Update the matching state to indicate that the relative selector matching + /// happened in the subject position. + pub fn considered_anchor(&mut self) { + *self = Self::ConsideredAnchor; + } + + /// Update the matching state to indicate that the relative selector matching + /// happened in a non-subject position. + pub fn considered(&mut self) { + // Being considered an anchor is stronger (e.g. `:has(.a):is(:has(.b) .c)`). + if *self == Self::ConsideredAnchor { + *self = Self::ConsideredAnchor; + } else { + *self = Self::Considered; + } + } +} + +/// Set of caches (And cache-likes) that speed up expensive selector matches. +#[derive(Default)] +pub struct SelectorCaches { + /// A cache to speed up nth-index-like selectors. + pub nth_index: NthIndexCache, + /// A cache to speed up relative selector matches. See module documentation. + pub relative_selector: RelativeSelectorCache, + /// A map of bloom filters to fast-reject relative selector matches. + pub relative_selector_filter_map: RelativeSelectorFilterMap, +} + +/// Data associated with the matching process for a element. This context is +/// used across many selectors for an element, so it's not appropriate for +/// transient data that applies to only a single selector. +pub struct MatchingContext<'a, Impl> +where + Impl: SelectorImpl, +{ + /// Input with the matching mode we should use when matching selectors. + matching_mode: MatchingMode, + /// Input with the bloom filter used to fast-reject selectors. + pub bloom_filter: Option<&'a BloomFilter>, + /// The element which is going to match :scope pseudo-class. It can be + /// either one :scope element, or the scoping element. + /// + /// Note that, although in theory there can be multiple :scope elements, + /// in current specs, at most one is specified, and when there is one, + /// scoping element is not relevant anymore, so we use a single field for + /// them. + /// + /// When this is None, :scope will match the root element. + /// + /// See https://drafts.csswg.org/selectors-4/#scope-pseudo + pub scope_element: Option, + + /// The current shadow host we're collecting :host rules for. + pub current_host: Option, + + /// Controls how matching for links is handled. + visited_handling: VisitedHandlingMode, + + /// The current nesting level of selectors that we're matching. + nesting_level: usize, + + /// Whether we're inside a negation or not. + in_negation: bool, + + /// An optional hook function for checking whether a pseudo-element + /// should match when matching_mode is ForStatelessPseudoElement. + pub pseudo_element_matching_fn: Option<&'a dyn Fn(&Impl::PseudoElement) -> bool>, + + /// Extra implementation-dependent matching data. + pub extra_data: Impl::ExtraMatchingData<'a>, + + /// The current element we're anchoring on for evaluating the relative selector. + current_relative_selector_anchor: Option, + pub considered_relative_selector: RelativeSelectorMatchingState, + + quirks_mode: QuirksMode, + needs_selector_flags: NeedsSelectorFlags, + + /// Whether we're matching in the contect of invalidation. + matching_for_invalidation: MatchingForInvalidation, + + /// Caches to speed up expensive selector matches. + pub selector_caches: &'a mut SelectorCaches, + + classes_and_ids_case_sensitivity: CaseSensitivity, + _impl: ::std::marker::PhantomData, +} + +impl<'a, Impl> MatchingContext<'a, Impl> +where + Impl: SelectorImpl, +{ + /// Constructs a new `MatchingContext`. + pub fn new( + matching_mode: MatchingMode, + bloom_filter: Option<&'a BloomFilter>, + selector_caches: &'a mut SelectorCaches, + quirks_mode: QuirksMode, + needs_selector_flags: NeedsSelectorFlags, + matching_for_invalidation: MatchingForInvalidation, + ) -> Self { + Self::new_for_visited( + matching_mode, + bloom_filter, + selector_caches, + VisitedHandlingMode::AllLinksUnvisited, + quirks_mode, + needs_selector_flags, + matching_for_invalidation, + ) + } + + /// Constructs a new `MatchingContext` for use in visited matching. + pub fn new_for_visited( + matching_mode: MatchingMode, + bloom_filter: Option<&'a BloomFilter>, + selector_caches: &'a mut SelectorCaches, + visited_handling: VisitedHandlingMode, + quirks_mode: QuirksMode, + needs_selector_flags: NeedsSelectorFlags, + matching_for_invalidation: MatchingForInvalidation, + ) -> Self { + Self { + matching_mode, + bloom_filter, + visited_handling, + quirks_mode, + classes_and_ids_case_sensitivity: quirks_mode.classes_and_ids_case_sensitivity(), + needs_selector_flags, + matching_for_invalidation, + scope_element: None, + current_host: None, + nesting_level: 0, + in_negation: false, + pseudo_element_matching_fn: None, + extra_data: Default::default(), + current_relative_selector_anchor: None, + considered_relative_selector: RelativeSelectorMatchingState::None, + selector_caches, + _impl: ::std::marker::PhantomData, + } + } + + // Grab a reference to the appropriate cache. + #[inline] + pub fn nth_index_cache( + &mut self, + is_of_type: bool, + is_from_end: bool, + selectors: &[Selector], + ) -> &mut NthIndexCacheInner { + self.selector_caches + .nth_index + .get(is_of_type, is_from_end, selectors) + } + + /// Whether we're matching a nested selector. + #[inline] + pub fn is_nested(&self) -> bool { + self.nesting_level != 0 + } + + /// Whether we're matching inside a :not(..) selector. + #[inline] + pub fn in_negation(&self) -> bool { + self.in_negation + } + + /// The quirks mode of the document. + #[inline] + pub fn quirks_mode(&self) -> QuirksMode { + self.quirks_mode + } + + /// The matching-mode for this selector-matching operation. + #[inline] + pub fn matching_mode(&self) -> MatchingMode { + self.matching_mode + } + + /// Whether we need to set selector flags. + #[inline] + pub fn needs_selector_flags(&self) -> bool { + self.needs_selector_flags == NeedsSelectorFlags::Yes + } + + /// Whether or not we're matching to invalidate. + #[inline] + pub fn matching_for_invalidation(&self) -> bool { + self.matching_for_invalidation == MatchingForInvalidation::Yes + } + + /// The case-sensitivity for class and ID selectors + #[inline] + pub fn classes_and_ids_case_sensitivity(&self) -> CaseSensitivity { + self.classes_and_ids_case_sensitivity + } + + /// Runs F with a deeper nesting level. + #[inline] + pub fn nest(&mut self, f: F) -> R + where + F: FnOnce(&mut Self) -> R, + { + self.nesting_level += 1; + let result = f(self); + self.nesting_level -= 1; + result + } + + /// Runs F with a deeper nesting level, and marking ourselves in a negation, + /// for a :not(..) selector, for example. + #[inline] + pub fn nest_for_negation(&mut self, f: F) -> R + where + F: FnOnce(&mut Self) -> R, + { + let old_in_negation = self.in_negation; + self.in_negation = true; + let result = self.nest(f); + self.in_negation = old_in_negation; + result + } + + #[inline] + pub fn visited_handling(&self) -> VisitedHandlingMode { + self.visited_handling + } + + /// Runs F with a different VisitedHandlingMode. + #[inline] + pub fn with_visited_handling_mode( + &mut self, + handling_mode: VisitedHandlingMode, + f: F, + ) -> R + where + F: FnOnce(&mut Self) -> R, + { + let original_handling_mode = self.visited_handling; + self.visited_handling = handling_mode; + let result = f(self); + self.visited_handling = original_handling_mode; + result + } + + /// Runs F with a given shadow host which is the root of the tree whose + /// rules we're matching. + #[inline] + pub fn with_shadow_host(&mut self, host: Option, f: F) -> R + where + E: Element, + F: FnOnce(&mut Self) -> R, + { + let original_host = self.current_host.take(); + self.current_host = host.map(|h| h.opaque()); + let result = f(self); + self.current_host = original_host; + result + } + + /// Returns the current shadow host whose shadow root we're matching rules + /// against. + #[inline] + pub fn shadow_host(&self) -> Option { + self.current_host + } + + /// Runs F with a deeper nesting level, with the given element as the anchor, + /// for a :has(...) selector, for example. + #[inline] + pub fn nest_for_relative_selector(&mut self, anchor: OpaqueElement, f: F) -> R + where + F: FnOnce(&mut Self) -> R, + { + debug_assert!( + self.current_relative_selector_anchor.is_none(), + "Nesting should've been rejected at parse time" + ); + self.current_relative_selector_anchor = Some(anchor); + let result = self.nest(f); + self.current_relative_selector_anchor = None; + result + } + + /// Returns the current anchor element to evaluate the relative selector against. + #[inline] + pub fn relative_selector_anchor(&self) -> Option { + self.current_relative_selector_anchor + } +} diff --git a/servo/components/selectors/lib.rs b/servo/components/selectors/lib.rs new file mode 100644 index 0000000000..d909059ccf --- /dev/null +++ b/servo/components/selectors/lib.rs @@ -0,0 +1,41 @@ +/* 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/. */ + +// Make |cargo bench| work. +#![cfg_attr(feature = "bench", feature(test))] + +#[macro_use] +extern crate bitflags; +#[macro_use] +extern crate cssparser; +#[macro_use] +extern crate debug_unreachable; +#[macro_use] +extern crate derive_more; +extern crate fxhash; +#[macro_use] +extern crate log; +extern crate phf; +extern crate precomputed_hash; +extern crate servo_arc; +extern crate smallvec; +extern crate to_shmem; +#[macro_use] +extern crate to_shmem_derive; + +pub mod attr; +pub mod bloom; +mod builder; +pub mod context; +pub mod matching; +mod nth_index_cache; +pub mod parser; +pub mod relative_selector; +pub mod sink; +mod tree; +pub mod visitor; + +pub use crate::nth_index_cache::NthIndexCache; +pub use crate::parser::{Parser, SelectorImpl, SelectorList}; +pub use crate::tree::{Element, OpaqueElement}; diff --git a/servo/components/selectors/matching.rs b/servo/components/selectors/matching.rs new file mode 100644 index 0000000000..763f65d547 --- /dev/null +++ b/servo/components/selectors/matching.rs @@ -0,0 +1,1370 @@ +/* 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 crate::attr::{ + AttrSelectorOperation, AttrSelectorWithOptionalNamespace, CaseSensitivity, NamespaceConstraint, + ParsedAttrSelectorOperation, ParsedCaseSensitivity, +}; +use crate::bloom::{BloomFilter, BLOOM_HASH_MASK}; +use crate::parser::{ + AncestorHashes, Combinator, Component, LocalName, NthSelectorData, RelativeSelectorMatchHint, +}; +use crate::parser::{ + NonTSPseudoClass, RelativeSelector, Selector, SelectorImpl, SelectorIter, SelectorList, +}; +use crate::relative_selector::cache::RelativeSelectorCachedMatch; +use crate::tree::Element; +use smallvec::SmallVec; +use std::borrow::Borrow; + +pub use crate::context::*; + +// The bloom filter for descendant CSS selectors will have a <1% false +// positive rate until it has this many selectors in it, then it will +// rapidly increase. +pub static RECOMMENDED_SELECTOR_BLOOM_FILTER_SIZE: usize = 4096; + +bitflags! { + /// Set of flags that are set on either the element or its parent (depending + /// on the flag) if the element could potentially match a selector. + #[derive(Clone, Copy)] + pub struct ElementSelectorFlags: usize { + /// When a child is added or removed from the parent, all the children + /// must be restyled, because they may match :nth-last-child, + /// :last-of-type, :nth-last-of-type, or :only-of-type. + const HAS_SLOW_SELECTOR = 1 << 0; + + /// When a child is added or removed from the parent, any later + /// children must be restyled, because they may match :nth-child, + /// :first-of-type, or :nth-of-type. + const HAS_SLOW_SELECTOR_LATER_SIBLINGS = 1 << 1; + + /// HAS_SLOW_SELECTOR* was set by the presence of :nth (But not of). + const HAS_SLOW_SELECTOR_NTH = 1 << 2; + + /// When a DOM mutation occurs on a child that might be matched by + /// :nth-last-child(.. of ), earlier children must be + /// restyled, and HAS_SLOW_SELECTOR will be set (which normally + /// indicates that all children will be restyled). + /// + /// Similarly, when a DOM mutation occurs on a child that might be + /// matched by :nth-child(.. of ), later children must be + /// restyled, and HAS_SLOW_SELECTOR_LATER_SIBLINGS will be set. + const HAS_SLOW_SELECTOR_NTH_OF = 1 << 3; + + /// When a child is added or removed from the parent, the first and + /// last children must be restyled, because they may match :first-child, + /// :last-child, or :only-child. + const HAS_EDGE_CHILD_SELECTOR = 1 << 4; + + /// The element has an empty selector, so when a child is appended we + /// might need to restyle the parent completely. + const HAS_EMPTY_SELECTOR = 1 << 5; + + /// The element may anchor a relative selector. + const ANCHORS_RELATIVE_SELECTOR = 1 << 6; + + /// The element may anchor a relative selector that is not the subject + /// of the whole selector. + const ANCHORS_RELATIVE_SELECTOR_NON_SUBJECT = 1 << 7; + + /// The element is reached by a relative selector search in the sibling direction. + const RELATIVE_SELECTOR_SEARCH_DIRECTION_SIBLING = 1 << 8; + + /// The element is reached by a relative selector search in the ancestor direction. + const RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR = 1 << 9; + + // The element is reached by a relative selector search in both sibling and ancestor directions. + const RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR_SIBLING = + Self::RELATIVE_SELECTOR_SEARCH_DIRECTION_SIBLING.bits() | + Self::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR.bits(); + } +} + +impl ElementSelectorFlags { + /// Returns the subset of flags that apply to the element. + pub fn for_self(self) -> ElementSelectorFlags { + self & (ElementSelectorFlags::HAS_EMPTY_SELECTOR | + ElementSelectorFlags::ANCHORS_RELATIVE_SELECTOR | + ElementSelectorFlags::ANCHORS_RELATIVE_SELECTOR_NON_SUBJECT | + ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_SIBLING | + ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR) + } + + /// Returns the subset of flags that apply to the parent. + pub fn for_parent(self) -> ElementSelectorFlags { + self & (ElementSelectorFlags::HAS_SLOW_SELECTOR | + ElementSelectorFlags::HAS_SLOW_SELECTOR_LATER_SIBLINGS | + ElementSelectorFlags::HAS_SLOW_SELECTOR_NTH | + ElementSelectorFlags::HAS_SLOW_SELECTOR_NTH_OF | + ElementSelectorFlags::HAS_EDGE_CHILD_SELECTOR) + } +} + +/// Holds per-compound-selector data. +struct LocalMatchingContext<'a, 'b: 'a, Impl: SelectorImpl> { + shared: &'a mut MatchingContext<'b, Impl>, + rightmost: SubjectOrPseudoElement, + quirks_data: Option>, +} + +#[inline(always)] +pub fn matches_selector_list( + selector_list: &SelectorList, + element: &E, + context: &mut MatchingContext, +) -> bool +where + E: Element, +{ + // This is pretty much any(..) but manually inlined because the compiler + // refuses to do so from querySelector / querySelectorAll. + for selector in selector_list.slice() { + let matches = matches_selector(selector, 0, None, element, context); + if matches { + return true; + } + } + + false +} + +#[inline(always)] +fn may_match(hashes: &AncestorHashes, bf: &BloomFilter) -> bool { + // Check the first three hashes. Note that we can check for zero before + // masking off the high bits, since if any of the first three hashes is + // zero the fourth will be as well. We also take care to avoid the + // special-case complexity of the fourth hash until we actually reach it, + // because we usually don't. + // + // To be clear: this is all extremely hot. + for i in 0..3 { + let packed = hashes.packed_hashes[i]; + if packed == 0 { + // No more hashes left - unable to fast-reject. + return true; + } + + if !bf.might_contain_hash(packed & BLOOM_HASH_MASK) { + // Hooray! We fast-rejected on this hash. + return false; + } + } + + // Now do the slighty-more-complex work of synthesizing the fourth hash, + // and check it against the filter if it exists. + let fourth = hashes.fourth_hash(); + fourth == 0 || bf.might_contain_hash(fourth) +} + +/// A result of selector matching, includes 3 failure types, +/// +/// NotMatchedAndRestartFromClosestLaterSibling +/// NotMatchedAndRestartFromClosestDescendant +/// NotMatchedGlobally +/// +/// When NotMatchedGlobally appears, stop selector matching completely since +/// the succeeding selectors never matches. +/// It is raised when +/// Child combinator cannot find the candidate element. +/// Descendant combinator cannot find the candidate element. +/// +/// When NotMatchedAndRestartFromClosestDescendant appears, the selector +/// matching does backtracking and restarts from the closest Descendant +/// combinator. +/// It is raised when +/// NextSibling combinator cannot find the candidate element. +/// LaterSibling combinator cannot find the candidate element. +/// Child combinator doesn't match on the found element. +/// +/// When NotMatchedAndRestartFromClosestLaterSibling appears, the selector +/// matching does backtracking and restarts from the closest LaterSibling +/// combinator. +/// It is raised when +/// NextSibling combinator doesn't match on the found element. +/// +/// For example, when the selector "d1 d2 a" is provided and we cannot *find* +/// an appropriate ancestor element for "d1", this selector matching raises +/// NotMatchedGlobally since even if "d2" is moved to more upper element, the +/// candidates for "d1" becomes less than before and d1 . +/// +/// The next example is siblings. When the selector "b1 + b2 ~ d1 a" is +/// provided and we cannot *find* an appropriate brother element for b1, +/// the selector matching raises NotMatchedAndRestartFromClosestDescendant. +/// The selectors ("b1 + b2 ~") doesn't match and matching restart from "d1". +/// +/// The additional example is child and sibling. When the selector +/// "b1 + c1 > b2 ~ d1 a" is provided and the selector "b1" doesn't match on +/// the element, this "b1" raises NotMatchedAndRestartFromClosestLaterSibling. +/// However since the selector "c1" raises +/// NotMatchedAndRestartFromClosestDescendant. So the selector +/// "b1 + c1 > b2 ~ " doesn't match and restart matching from "d1". +#[derive(Clone, Copy, Eq, PartialEq)] +enum SelectorMatchingResult { + Matched, + NotMatchedAndRestartFromClosestLaterSibling, + NotMatchedAndRestartFromClosestDescendant, + NotMatchedGlobally, +} + +/// Matches a selector, fast-rejecting against a bloom filter. +/// +/// We accept an offset to allow consumers to represent and match against +/// partial selectors (indexed from the right). We use this API design, rather +/// than having the callers pass a SelectorIter, because creating a SelectorIter +/// requires dereferencing the selector to get the length, which adds an +/// unncessary cache miss for cases when we can fast-reject with AncestorHashes +/// (which the caller can store inline with the selector pointer). +#[inline(always)] +pub fn matches_selector( + selector: &Selector, + offset: usize, + hashes: Option<&AncestorHashes>, + element: &E, + context: &mut MatchingContext, +) -> bool +where + E: Element, +{ + // Use the bloom filter to fast-reject. + if let Some(hashes) = hashes { + if let Some(filter) = context.bloom_filter { + if !may_match(hashes, filter) { + return false; + } + } + } + matches_complex_selector( + selector.iter_from(offset), + element, + context, + if selector.is_rightmost(offset) { + SubjectOrPseudoElement::Yes + } else { + SubjectOrPseudoElement::No + }, + ) +} + +/// Whether a compound selector matched, and whether it was the rightmost +/// selector inside the complex selector. +pub enum CompoundSelectorMatchingResult { + /// The selector was fully matched. + FullyMatched, + /// The compound selector matched, and the next combinator offset is + /// `next_combinator_offset`. + Matched { next_combinator_offset: usize }, + /// The selector didn't match. + NotMatched, +} + +/// Matches a compound selector belonging to `selector`, starting at offset +/// `from_offset`, matching left to right. +/// +/// Requires that `from_offset` points to a `Combinator`. +/// +/// NOTE(emilio): This doesn't allow to match in the leftmost sequence of the +/// complex selector, but it happens to be the case we don't need it. +pub fn matches_compound_selector_from( + selector: &Selector, + mut from_offset: usize, + context: &mut MatchingContext, + element: &E, +) -> CompoundSelectorMatchingResult +where + E: Element, +{ + if cfg!(debug_assertions) && from_offset != 0 { + selector.combinator_at_parse_order(from_offset - 1); // This asserts. + } + + let mut local_context = LocalMatchingContext { + shared: context, + // We have no info if this is an outer selector. This function is called in + // an invalidation context, which only calls this for non-subject (i.e. + // Non-rightmost) positions. + rightmost: SubjectOrPseudoElement::No, + quirks_data: None, + }; + + // Find the end of the selector or the next combinator, then match + // backwards, so that we match in the same order as + // matches_complex_selector, which is usually faster. + let start_offset = from_offset; + for component in selector.iter_raw_parse_order_from(from_offset) { + if matches!(*component, Component::Combinator(..)) { + debug_assert_ne!(from_offset, 0, "Selector started with a combinator?"); + break; + } + + from_offset += 1; + } + + debug_assert!(from_offset >= 1); + debug_assert!(from_offset <= selector.len()); + + let iter = selector.iter_from(selector.len() - from_offset); + debug_assert!( + iter.clone().next().is_some() || + (from_offset != selector.len() && + matches!( + selector.combinator_at_parse_order(from_offset), + Combinator::SlotAssignment | Combinator::PseudoElement + )), + "Got the math wrong: {:?} | {:?} | {} {}", + selector, + selector.iter_raw_match_order().as_slice(), + from_offset, + start_offset + ); + + for component in iter { + if !matches_simple_selector(component, element, &mut local_context) { + return CompoundSelectorMatchingResult::NotMatched; + } + } + + if from_offset != selector.len() { + return CompoundSelectorMatchingResult::Matched { + next_combinator_offset: from_offset, + }; + } + + CompoundSelectorMatchingResult::FullyMatched +} + +/// Matches a complex selector. +#[inline(always)] +fn matches_complex_selector( + mut iter: SelectorIter, + element: &E, + context: &mut MatchingContext, + rightmost: SubjectOrPseudoElement, +) -> bool +where + E: Element, +{ + // If this is the special pseudo-element mode, consume the ::pseudo-element + // before proceeding, since the caller has already handled that part. + if context.matching_mode() == MatchingMode::ForStatelessPseudoElement && !context.is_nested() { + // Consume the pseudo. + match *iter.next().unwrap() { + Component::PseudoElement(ref pseudo) => { + if let Some(ref f) = context.pseudo_element_matching_fn { + if !f(pseudo) { + return false; + } + } + }, + ref other => { + debug_assert!( + false, + "Used MatchingMode::ForStatelessPseudoElement \ + in a non-pseudo selector {:?}", + other + ); + return false; + }, + } + + if !iter.matches_for_stateless_pseudo_element() { + return false; + } + + // Advance to the non-pseudo-element part of the selector. + let next_sequence = iter.next_sequence().unwrap(); + debug_assert_eq!(next_sequence, Combinator::PseudoElement); + } + + let result = matches_complex_selector_internal(iter, element, context, rightmost); + + matches!(result, SelectorMatchingResult::Matched) +} + +/// Matches each selector of a list as a complex selector +fn matches_complex_selector_list( + list: &[Selector], + element: &E, + context: &mut MatchingContext, + rightmost: SubjectOrPseudoElement, +) -> bool { + for selector in list { + if matches_complex_selector(selector.iter(), element, context, rightmost) { + return true; + } + } + false +} + +fn matches_relative_selector( + relative_selector: &RelativeSelector, + element: &E, + context: &mut MatchingContext, + rightmost: SubjectOrPseudoElement, +) -> bool { + // Overall, we want to mark the path that we've traversed so that when an element + // is invalidated, we early-reject unnecessary relative selector invalidations. + if relative_selector.match_hint.is_descendant_direction() { + if context.needs_selector_flags() { + element.apply_selector_flags( + ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR, + ); + } + let mut next_element = element.first_element_child(); + while let Some(el) = next_element { + if context.needs_selector_flags() { + el.apply_selector_flags( + ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR, + ); + } + let mut matched = matches_complex_selector( + relative_selector.selector.iter(), + &el, + context, + rightmost, + ); + if !matched && relative_selector.match_hint.is_subtree() { + matched = matches_relative_selector_subtree( + &relative_selector.selector, + &el, + context, + rightmost, + ); + } + if matched { + return true; + } + next_element = el.next_sibling_element(); + } + } else { + debug_assert!( + matches!( + relative_selector.match_hint, + RelativeSelectorMatchHint::InNextSibling | + RelativeSelectorMatchHint::InNextSiblingSubtree | + RelativeSelectorMatchHint::InSibling | + RelativeSelectorMatchHint::InSiblingSubtree + ), + "Not descendant direction, but also not sibling direction?" + ); + if context.needs_selector_flags() { + element.apply_selector_flags( + ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_SIBLING, + ); + } + let sibling_flag = if relative_selector.match_hint.is_subtree() { + ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR_SIBLING + } else { + ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_SIBLING + }; + let mut next_element = element.next_sibling_element(); + while let Some(el) = next_element { + if context.needs_selector_flags() { + el.apply_selector_flags(sibling_flag); + } + let matched = if relative_selector.match_hint.is_subtree() { + matches_relative_selector_subtree( + &relative_selector.selector, + &el, + context, + rightmost, + ) + } else { + matches_complex_selector(relative_selector.selector.iter(), &el, context, rightmost) + }; + if matched { + return true; + } + if relative_selector.match_hint.is_next_sibling() { + break; + } + next_element = el.next_sibling_element(); + } + } + return false; +} + +fn relative_selector_match_early( + selector: &RelativeSelector, + element: &E, + context: &mut MatchingContext, +) -> Option { + if context.matching_for_invalidation() { + // In the context of invalidation, we can't use caching/filtering due to + // now/then matches. DOM structure also may have changed, so just pretend + // that we always match. + return Some(!context.in_negation()); + } + // See if we can return a cached result. + if let Some(cached) = context + .selector_caches + .relative_selector + .lookup(element.opaque(), selector) + { + return Some(cached.matched()); + } + // See if we can fast-reject. + if context + .selector_caches + .relative_selector_filter_map + .fast_reject(element, selector, context.quirks_mode()) + { + // Alright, add as unmatched to cache. + context.selector_caches.relative_selector.add( + element.opaque(), + selector, + RelativeSelectorCachedMatch::NotMatched, + ); + return Some(false); + } + None +} + +fn match_relative_selectors( + selectors: &[RelativeSelector], + element: &E, + context: &mut MatchingContext, + rightmost: SubjectOrPseudoElement, +) -> bool { + if context.relative_selector_anchor().is_some() { + // FIXME(emilio): This currently can happen with nesting, and it's not fully + // correct, arguably. But the ideal solution isn't super-clear either. For now, + // cope with it and explicitly reject it at match time. See [1] for discussion. + // + // [1]: https://github.com/w3c/csswg-drafts/issues/9600 + return false; + } + context.nest_for_relative_selector(element.opaque(), |context| { + do_match_relative_selectors(selectors, element, context, rightmost) + }) +} + +/// Matches a relative selector in a list of relative selectors. +fn do_match_relative_selectors( + selectors: &[RelativeSelector], + element: &E, + context: &mut MatchingContext, + rightmost: SubjectOrPseudoElement, +) -> bool { + // Due to style sharing implications (See style sharing code), we mark the current styling context + // to mark elements considered for :has matching. Additionally, we want to mark the elements themselves, + // since we don't want to indiscriminately invalidate every element as a potential anchor. + if rightmost == SubjectOrPseudoElement::Yes { + context.considered_relative_selector.considered_anchor(); + if context.needs_selector_flags() { + element.apply_selector_flags(ElementSelectorFlags::ANCHORS_RELATIVE_SELECTOR); + } + } else { + context.considered_relative_selector.considered(); + if context.needs_selector_flags() { + element + .apply_selector_flags(ElementSelectorFlags::ANCHORS_RELATIVE_SELECTOR_NON_SUBJECT); + } + } + + for relative_selector in selectors.iter() { + if let Some(result) = relative_selector_match_early(relative_selector, element, context) { + if result { + return true; + } + // Early return indicates no match, continue to next selector. + continue; + } + + let matched = matches_relative_selector(relative_selector, element, context, rightmost); + context.selector_caches.relative_selector.add( + element.opaque(), + relative_selector, + if matched { + RelativeSelectorCachedMatch::Matched + } else { + RelativeSelectorCachedMatch::NotMatched + }, + ); + if matched { + return true; + } + } + + false +} + +fn matches_relative_selector_subtree( + selector: &Selector, + element: &E, + context: &mut MatchingContext, + rightmost: SubjectOrPseudoElement, +) -> bool { + let mut current = element.first_element_child(); + + while let Some(el) = current { + if context.needs_selector_flags() { + el.apply_selector_flags( + ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR, + ); + } + if matches_complex_selector(selector.iter(), &el, context, rightmost) { + return true; + } + + if matches_relative_selector_subtree(selector, &el, context, rightmost) { + return true; + } + + current = el.next_sibling_element(); + } + + false +} + +/// Whether the :hover and :active quirk applies. +/// +/// https://quirks.spec.whatwg.org/#the-active-and-hover-quirk +fn hover_and_active_quirk_applies( + selector_iter: &SelectorIter, + context: &MatchingContext, + rightmost: SubjectOrPseudoElement, +) -> bool { + if context.quirks_mode() != QuirksMode::Quirks { + return false; + } + + if context.is_nested() { + return false; + } + + // This compound selector had a pseudo-element to the right that we + // intentionally skipped. + if rightmost == SubjectOrPseudoElement::Yes && + context.matching_mode() == MatchingMode::ForStatelessPseudoElement + { + return false; + } + + selector_iter.clone().all(|simple| match *simple { + Component::LocalName(_) | + Component::AttributeInNoNamespaceExists { .. } | + Component::AttributeInNoNamespace { .. } | + Component::AttributeOther(_) | + Component::ID(_) | + Component::Class(_) | + Component::PseudoElement(_) | + Component::Negation(_) | + Component::Empty | + Component::Nth(_) | + Component::NthOf(_) => false, + Component::NonTSPseudoClass(ref pseudo_class) => pseudo_class.is_active_or_hover(), + _ => true, + }) +} + +#[derive(Clone, Copy, PartialEq)] +enum SubjectOrPseudoElement { + Yes, + No, +} + +fn host_for_part(element: &E, context: &MatchingContext) -> Option +where + E: Element, +{ + let scope = context.current_host; + let mut curr = element.containing_shadow_host()?; + if scope == Some(curr.opaque()) { + return Some(curr); + } + loop { + let parent = curr.containing_shadow_host(); + if parent.as_ref().map(|h| h.opaque()) == scope { + return Some(curr); + } + curr = parent?; + } +} + +fn assigned_slot(element: &E, context: &MatchingContext) -> Option +where + E: Element, +{ + debug_assert!(element + .assigned_slot() + .map_or(true, |s| s.is_html_slot_element())); + let scope = context.current_host?; + let mut current_slot = element.assigned_slot()?; + while current_slot.containing_shadow_host().unwrap().opaque() != scope { + current_slot = current_slot.assigned_slot()?; + } + Some(current_slot) +} + +#[inline(always)] +fn next_element_for_combinator( + element: &E, + combinator: Combinator, + selector: &SelectorIter, + context: &MatchingContext, +) -> Option +where + E: Element, +{ + match combinator { + Combinator::NextSibling | Combinator::LaterSibling => element.prev_sibling_element(), + Combinator::Child | Combinator::Descendant => { + match element.parent_element() { + Some(e) => return Some(e), + None => {}, + } + + if !element.parent_node_is_shadow_root() { + return None; + } + + // https://drafts.csswg.org/css-scoping/#host-element-in-tree: + // + // For the purpose of Selectors, a shadow host also appears in + // its shadow tree, with the contents of the shadow tree treated + // as its children. (In other words, the shadow host is treated as + // replacing the shadow root node.) + // + // and also: + // + // When considered within its own shadow trees, the shadow host is + // featureless. Only the :host, :host(), and :host-context() + // pseudo-classes are allowed to match it. + // + // Since we know that the parent is a shadow root, we necessarily + // are in a shadow tree of the host, and the next selector will only + // match if the selector is a featureless :host selector. + if !selector.clone().is_featureless_host_selector() { + return None; + } + + element.containing_shadow_host() + }, + Combinator::Part => host_for_part(element, context), + Combinator::SlotAssignment => assigned_slot(element, context), + Combinator::PseudoElement => element.pseudo_element_originating_element(), + } +} + +fn matches_complex_selector_internal( + mut selector_iter: SelectorIter, + element: &E, + context: &mut MatchingContext, + rightmost: SubjectOrPseudoElement, +) -> SelectorMatchingResult +where + E: Element, +{ + debug!( + "Matching complex selector {:?} for {:?}", + selector_iter, element + ); + + let matches_compound_selector = + matches_compound_selector(&mut selector_iter, element, context, rightmost); + + let combinator = selector_iter.next_sequence(); + if combinator.map_or(false, |c| c.is_sibling()) { + if context.needs_selector_flags() { + element.apply_selector_flags(ElementSelectorFlags::HAS_SLOW_SELECTOR_LATER_SIBLINGS); + } + } + + if !matches_compound_selector { + return SelectorMatchingResult::NotMatchedAndRestartFromClosestLaterSibling; + } + + let combinator = match combinator { + None => return SelectorMatchingResult::Matched, + Some(c) => c, + }; + + let (candidate_not_found, mut rightmost) = match combinator { + Combinator::NextSibling | Combinator::LaterSibling => { + (SelectorMatchingResult::NotMatchedAndRestartFromClosestDescendant, SubjectOrPseudoElement::No) + }, + Combinator::Child | + Combinator::Descendant | + Combinator::SlotAssignment | + Combinator::Part => (SelectorMatchingResult::NotMatchedGlobally, SubjectOrPseudoElement::No), + Combinator::PseudoElement => (SelectorMatchingResult::NotMatchedGlobally, rightmost), + }; + + // Stop matching :visited as soon as we find a link, or a combinator for + // something that isn't an ancestor. + let mut visited_handling = if combinator.is_sibling() { + VisitedHandlingMode::AllLinksUnvisited + } else { + context.visited_handling() + }; + + let mut element = element.clone(); + loop { + if element.is_link() { + visited_handling = VisitedHandlingMode::AllLinksUnvisited; + } + + element = match next_element_for_combinator(&element, combinator, &selector_iter, &context) + { + None => return candidate_not_found, + Some(next_element) => next_element, + }; + + let result = context.with_visited_handling_mode(visited_handling, |context| { + matches_complex_selector_internal( + selector_iter.clone(), + &element, + context, + rightmost, + ) + }); + + if !matches!(combinator, Combinator::PseudoElement) { + rightmost = SubjectOrPseudoElement::No; + } + + match (result, combinator) { + // Return the status immediately. + (SelectorMatchingResult::Matched, _) | + (SelectorMatchingResult::NotMatchedGlobally, _) | + (_, Combinator::NextSibling) => { + return result; + }, + + // Upgrade the failure status to + // NotMatchedAndRestartFromClosestDescendant. + (_, Combinator::PseudoElement) | (_, Combinator::Child) => { + return SelectorMatchingResult::NotMatchedAndRestartFromClosestDescendant; + }, + + // If the failure status is + // NotMatchedAndRestartFromClosestDescendant and combinator is + // Combinator::LaterSibling, give up this Combinator::LaterSibling + // matching and restart from the closest descendant combinator. + ( + SelectorMatchingResult::NotMatchedAndRestartFromClosestDescendant, + Combinator::LaterSibling, + ) => { + return result; + }, + + // The Combinator::Descendant combinator and the status is + // NotMatchedAndRestartFromClosestLaterSibling or + // NotMatchedAndRestartFromClosestDescendant, or the + // Combinator::LaterSibling combinator and the status is + // NotMatchedAndRestartFromClosestDescendant, we can continue to + // matching on the next candidate element. + _ => {}, + } + } +} + +#[inline] +fn matches_local_name(element: &E, local_name: &LocalName) -> bool +where + E: Element, +{ + let name = select_name(element, &local_name.name, &local_name.lower_name).borrow(); + element.has_local_name(name) +} + +fn matches_part( + element: &E, + parts: &[::Identifier], + context: &mut MatchingContext, +) -> bool +where + E: Element, +{ + let mut hosts = SmallVec::<[E; 4]>::new(); + + let mut host = match element.containing_shadow_host() { + Some(h) => h, + None => return false, + }; + + let current_host = context.current_host; + if current_host != Some(host.opaque()) { + loop { + let outer_host = host.containing_shadow_host(); + if outer_host.as_ref().map(|h| h.opaque()) == current_host { + break; + } + let outer_host = match outer_host { + Some(h) => h, + None => return false, + }; + // TODO(emilio): if worth it, we could early return if + // host doesn't have the exportparts attribute. + hosts.push(host); + host = outer_host; + } + } + + // Translate the part into the right scope. + parts.iter().all(|part| { + let mut part = part.clone(); + for host in hosts.iter().rev() { + part = match host.imported_part(&part) { + Some(p) => p, + None => return false, + }; + } + element.is_part(&part) + }) +} + +fn matches_host( + element: &E, + selector: Option<&Selector>, + context: &mut MatchingContext, + rightmost: SubjectOrPseudoElement, +) -> bool +where + E: Element, +{ + let host = match context.shadow_host() { + Some(h) => h, + None => return false, + }; + if host != element.opaque() { + return false; + } + selector.map_or(true, |selector| { + context + .nest(|context| matches_complex_selector(selector.iter(), element, context, rightmost)) + }) +} + +fn matches_slotted( + element: &E, + selector: &Selector, + context: &mut MatchingContext, + rightmost: SubjectOrPseudoElement, +) -> bool +where + E: Element, +{ + // are never flattened tree slottables. + if element.is_html_slot_element() { + return false; + } + context.nest(|context| matches_complex_selector(selector.iter(), element, context, rightmost)) +} + +fn matches_rare_attribute_selector( + element: &E, + attr_sel: &AttrSelectorWithOptionalNamespace, +) -> bool +where + E: Element, +{ + let empty_string; + let namespace = match attr_sel.namespace() { + Some(ns) => ns, + None => { + empty_string = crate::parser::namespace_empty_string::(); + NamespaceConstraint::Specific(&empty_string) + }, + }; + element.attr_matches( + &namespace, + select_name(element, &attr_sel.local_name, &attr_sel.local_name_lower), + &match attr_sel.operation { + ParsedAttrSelectorOperation::Exists => AttrSelectorOperation::Exists, + ParsedAttrSelectorOperation::WithValue { + operator, + case_sensitivity, + ref value, + } => AttrSelectorOperation::WithValue { + operator, + case_sensitivity: to_unconditional_case_sensitivity(case_sensitivity, element), + value, + }, + }, + ) +} + +/// Determines whether the given element matches the given compound selector. +#[inline] +fn matches_compound_selector( + selector_iter: &mut SelectorIter, + element: &E, + context: &mut MatchingContext, + rightmost: SubjectOrPseudoElement, +) -> bool +where + E: Element, +{ + let quirks_data = if context.quirks_mode() == QuirksMode::Quirks { + Some(selector_iter.clone()) + } else { + None + }; + let mut local_context = LocalMatchingContext { + shared: context, + rightmost, + quirks_data, + }; + selector_iter.all(|simple| matches_simple_selector(simple, element, &mut local_context)) +} + +/// Determines whether the given element matches the given single selector. +fn matches_simple_selector( + selector: &Component, + element: &E, + context: &mut LocalMatchingContext, +) -> bool +where + E: Element, +{ + debug_assert!(context.shared.is_nested() || !context.shared.in_negation()); + let rightmost = context.rightmost; + match *selector { + Component::ID(ref id) => { + element.has_id(id, context.shared.classes_and_ids_case_sensitivity()) + }, + Component::Class(ref class) => { + element.has_class(class, context.shared.classes_and_ids_case_sensitivity()) + }, + Component::LocalName(ref local_name) => matches_local_name(element, local_name), + Component::AttributeInNoNamespaceExists { + ref local_name, + ref local_name_lower, + } => element.has_attr_in_no_namespace(select_name(element, local_name, local_name_lower)), + Component::AttributeInNoNamespace { + ref local_name, + ref value, + operator, + case_sensitivity, + } => element.attr_matches( + &NamespaceConstraint::Specific(&crate::parser::namespace_empty_string::()), + local_name, + &AttrSelectorOperation::WithValue { + operator, + case_sensitivity: to_unconditional_case_sensitivity(case_sensitivity, element), + value, + }, + ), + Component::AttributeOther(ref attr_sel) => { + matches_rare_attribute_selector(element, attr_sel) + }, + Component::Part(ref parts) => matches_part(element, parts, &mut context.shared), + Component::Slotted(ref selector) => { + matches_slotted(element, selector, &mut context.shared, rightmost) + }, + Component::PseudoElement(ref pseudo) => { + element.match_pseudo_element(pseudo, context.shared) + }, + Component::ExplicitUniversalType | Component::ExplicitAnyNamespace => true, + Component::Namespace(_, ref url) | Component::DefaultNamespace(ref url) => { + element.has_namespace(&url.borrow()) + }, + Component::ExplicitNoNamespace => { + let ns = crate::parser::namespace_empty_string::(); + element.has_namespace(&ns.borrow()) + }, + Component::NonTSPseudoClass(ref pc) => { + if let Some(ref iter) = context.quirks_data { + if pc.is_active_or_hover() && + !element.is_link() && + hover_and_active_quirk_applies(iter, context.shared, context.rightmost) + { + return false; + } + } + element.match_non_ts_pseudo_class(pc, &mut context.shared) + }, + Component::Root => element.is_root(), + Component::Empty => { + if context.shared.needs_selector_flags() { + element.apply_selector_flags(ElementSelectorFlags::HAS_EMPTY_SELECTOR); + } + element.is_empty() + }, + Component::Host(ref selector) => { + matches_host(element, selector.as_ref(), &mut context.shared, rightmost) + }, + Component::ParentSelector | Component::Scope => match context.shared.scope_element { + Some(ref scope_element) => element.opaque() == *scope_element, + None => element.is_root(), + }, + Component::Nth(ref nth_data) => { + matches_generic_nth_child(element, context.shared, nth_data, &[], rightmost) + }, + Component::NthOf(ref nth_of_data) => context.shared.nest(|context| { + matches_generic_nth_child( + element, + context, + nth_of_data.nth_data(), + nth_of_data.selectors(), + rightmost, + ) + }), + Component::Is(ref list) | Component::Where(ref list) => context.shared.nest(|context| { + matches_complex_selector_list(list.slice(), element, context, rightmost) + }), + Component::Negation(ref list) => context.shared.nest_for_negation(|context| { + !matches_complex_selector_list(list.slice(), element, context, rightmost) + }), + Component::Has(ref relative_selectors) => { + match_relative_selectors(relative_selectors, element, context.shared, rightmost) + }, + Component::Combinator(_) => unsafe { + debug_unreachable!("Shouldn't try to selector-match combinators") + }, + Component::RelativeSelectorAnchor => { + let anchor = context.shared.relative_selector_anchor(); + // We may match inner relative selectors, in which case we want to always match. + anchor.map_or(true, |a| a == element.opaque()) + }, + Component::Invalid(..) => false, + } +} + +#[inline(always)] +pub fn select_name<'a, E: Element, T: PartialEq>( + element: &E, + local_name: &'a T, + local_name_lower: &'a T, +) -> &'a T { + if local_name == local_name_lower || element.is_html_element_in_html_document() { + local_name_lower + } else { + local_name + } +} + +#[inline(always)] +pub fn to_unconditional_case_sensitivity<'a, E: Element>( + parsed: ParsedCaseSensitivity, + element: &E, +) -> CaseSensitivity { + match parsed { + ParsedCaseSensitivity::CaseSensitive | ParsedCaseSensitivity::ExplicitCaseSensitive => { + CaseSensitivity::CaseSensitive + }, + ParsedCaseSensitivity::AsciiCaseInsensitive => CaseSensitivity::AsciiCaseInsensitive, + ParsedCaseSensitivity::AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument => { + if element.is_html_element_in_html_document() { + CaseSensitivity::AsciiCaseInsensitive + } else { + CaseSensitivity::CaseSensitive + } + }, + } +} + +fn matches_generic_nth_child( + element: &E, + context: &mut MatchingContext, + nth_data: &NthSelectorData, + selectors: &[Selector], + rightmost: SubjectOrPseudoElement, +) -> bool +where + E: Element, +{ + if element.ignores_nth_child_selectors() { + return false; + } + let has_selectors = !selectors.is_empty(); + let selectors_match = + !has_selectors || matches_complex_selector_list(selectors, element, context, rightmost); + if context.matching_for_invalidation() { + // Skip expensive indexing math in invalidation. + return selectors_match && !context.in_negation(); + } + + let NthSelectorData { ty, a, b, .. } = *nth_data; + let is_of_type = ty.is_of_type(); + if ty.is_only() { + debug_assert!( + !has_selectors, + ":only-child and :only-of-type cannot have a selector list!" + ); + return matches_generic_nth_child( + element, + context, + &NthSelectorData::first(is_of_type), + selectors, + rightmost, + ) && matches_generic_nth_child( + element, + context, + &NthSelectorData::last(is_of_type), + selectors, + rightmost, + ); + } + + let is_from_end = ty.is_from_end(); + + // It's useful to know whether this can only select the first/last element + // child for optimization purposes, see the `HAS_EDGE_CHILD_SELECTOR` flag. + let is_edge_child_selector = nth_data.is_simple_edge() && !has_selectors; + + if context.needs_selector_flags() { + let mut flags = if is_edge_child_selector { + ElementSelectorFlags::HAS_EDGE_CHILD_SELECTOR + } else if is_from_end { + ElementSelectorFlags::HAS_SLOW_SELECTOR + } else { + ElementSelectorFlags::HAS_SLOW_SELECTOR_LATER_SIBLINGS + }; + flags |= if has_selectors { + ElementSelectorFlags::HAS_SLOW_SELECTOR_NTH_OF + } else { + ElementSelectorFlags::HAS_SLOW_SELECTOR_NTH + }; + element.apply_selector_flags(flags); + } + + if !selectors_match { + return false; + } + + // :first/last-child are rather trivial to match, don't bother with the + // cache. + if is_edge_child_selector { + return if is_from_end { + element.next_sibling_element() + } else { + element.prev_sibling_element() + } + .is_none(); + } + + // Lookup or compute the index. + let index = if let Some(i) = context + .nth_index_cache(is_of_type, is_from_end, selectors) + .lookup(element.opaque()) + { + i + } else { + let i = nth_child_index( + element, + context, + selectors, + is_of_type, + is_from_end, + /* check_cache = */ true, + rightmost, + ); + context + .nth_index_cache(is_of_type, is_from_end, selectors) + .insert(element.opaque(), i); + i + }; + debug_assert_eq!( + index, + nth_child_index( + element, + context, + selectors, + is_of_type, + is_from_end, + /* check_cache = */ false, + rightmost, + ), + "invalid cache" + ); + + // Is there a non-negative integer n such that An+B=index? + match index.checked_sub(b) { + None => false, + Some(an) => match an.checked_div(a) { + Some(n) => n >= 0 && a * n == an, + None /* a == 0 */ => an == 0, + }, + } +} + +#[inline] +fn nth_child_index( + element: &E, + context: &mut MatchingContext, + selectors: &[Selector], + is_of_type: bool, + is_from_end: bool, + check_cache: bool, + rightmost: SubjectOrPseudoElement, +) -> i32 +where + E: Element, +{ + // The traversal mostly processes siblings left to right. So when we walk + // siblings to the right when computing NthLast/NthLastOfType we're unlikely + // to get cache hits along the way. As such, we take the hit of walking the + // siblings to the left checking the cache in the is_from_end case (this + // matches what Gecko does). The indices-from-the-left is handled during the + // regular look further below. + if check_cache && + is_from_end && + !context + .nth_index_cache(is_of_type, is_from_end, selectors) + .is_empty() + { + let mut index: i32 = 1; + let mut curr = element.clone(); + while let Some(e) = curr.prev_sibling_element() { + curr = e; + let matches = if is_of_type { + element.is_same_type(&curr) + } else if !selectors.is_empty() { + matches_complex_selector_list(selectors, &curr, context, rightmost) + } else { + true + }; + if !matches { + continue; + } + if let Some(i) = context + .nth_index_cache(is_of_type, is_from_end, selectors) + .lookup(curr.opaque()) + { + return i - index; + } + index += 1; + } + } + + let mut index: i32 = 1; + let mut curr = element.clone(); + let next = |e: E| { + if is_from_end { + e.next_sibling_element() + } else { + e.prev_sibling_element() + } + }; + while let Some(e) = next(curr) { + curr = e; + let matches = if is_of_type { + element.is_same_type(&curr) + } else if !selectors.is_empty() { + matches_complex_selector_list(selectors, &curr, context, rightmost) + } else { + true + }; + if !matches { + continue; + } + // If we're computing indices from the left, check each element in the + // cache. We handle the indices-from-the-right case at the top of this + // function. + if !is_from_end && check_cache { + if let Some(i) = context + .nth_index_cache(is_of_type, is_from_end, selectors) + .lookup(curr.opaque()) + { + return i + index; + } + } + index += 1; + } + + index +} diff --git a/servo/components/selectors/nth_index_cache.rs b/servo/components/selectors/nth_index_cache.rs new file mode 100644 index 0000000000..b4b41578d0 --- /dev/null +++ b/servo/components/selectors/nth_index_cache.rs @@ -0,0 +1,102 @@ +/* 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 std::hash::Hash; + +use crate::{parser::Selector, tree::OpaqueElement, SelectorImpl}; +use fxhash::FxHashMap; + +/// A cache to speed up matching of nth-index-like selectors. +/// +/// See [1] for some discussion around the design tradeoffs. +/// +/// [1] https://bugzilla.mozilla.org/show_bug.cgi?id=1401855#c3 +#[derive(Default)] +pub struct NthIndexCache { + nth: NthIndexCacheInner, + nth_of_selectors: NthIndexOfSelectorsCaches, + nth_last: NthIndexCacheInner, + nth_last_of_selectors: NthIndexOfSelectorsCaches, + nth_of_type: NthIndexCacheInner, + nth_last_of_type: NthIndexCacheInner, +} + +impl NthIndexCache { + /// Gets the appropriate cache for the given parameters. + pub fn get( + &mut self, + is_of_type: bool, + is_from_end: bool, + selectors: &[Selector], + ) -> &mut NthIndexCacheInner { + if is_of_type { + return if is_from_end { + &mut self.nth_last_of_type + } else { + &mut self.nth_of_type + }; + } + if !selectors.is_empty() { + return if is_from_end { + self.nth_last_of_selectors.lookup(selectors) + } else { + self.nth_of_selectors.lookup(selectors) + }; + } + if is_from_end { + &mut self.nth_last + } else { + &mut self.nth + } + } +} + +#[derive(Hash, Eq, PartialEq)] +struct SelectorListCacheKey(usize); + +/// Use the selector list's pointer as the cache key +impl SelectorListCacheKey { + // :nth-child of selectors are reference-counted with `ThinArc`, so we know their pointers are stable. + fn new(selectors: &[Selector]) -> Self { + Self(selectors.as_ptr() as usize) + } +} + +/// Use a different map of cached indices per :nth-child's or :nth-last-child's selector list +#[derive(Default)] +pub struct NthIndexOfSelectorsCaches(FxHashMap); + +/// Get or insert a map of cached incides for the selector list of this +/// particular :nth-child or :nth-last-child pseudoclass +impl NthIndexOfSelectorsCaches { + pub fn lookup( + &mut self, + selectors: &[Selector], + ) -> &mut NthIndexCacheInner { + self.0 + .entry(SelectorListCacheKey::new(selectors)) + .or_default() + } +} + +/// The concrete per-pseudo-class cache. +#[derive(Default)] +pub struct NthIndexCacheInner(FxHashMap); + +impl NthIndexCacheInner { + /// Does a lookup for a given element in the cache. + pub fn lookup(&mut self, el: OpaqueElement) -> Option { + self.0.get(&el).copied() + } + + /// Inserts an entry into the cache. + pub fn insert(&mut self, element: OpaqueElement, index: i32) { + self.0.insert(element, index); + } + + /// Returns whether the cache is empty. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} diff --git a/servo/components/selectors/parser.rs b/servo/components/selectors/parser.rs new file mode 100644 index 0000000000..792d4eb8bc --- /dev/null +++ b/servo/components/selectors/parser.rs @@ -0,0 +1,4483 @@ +/* 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 crate::attr::{AttrSelectorOperator, AttrSelectorWithOptionalNamespace}; +use crate::attr::{NamespaceConstraint, ParsedAttrSelectorOperation, ParsedCaseSensitivity}; +use crate::bloom::BLOOM_HASH_MASK; +use crate::builder::{ + relative_selector_list_specificity_and_flags, selector_list_specificity_and_flags, + SelectorBuilder, SelectorFlags, Specificity, SpecificityAndFlags, +}; +use crate::context::QuirksMode; +use crate::sink::Push; +use crate::visitor::SelectorListKind; +pub use crate::visitor::SelectorVisitor; +use cssparser::parse_nth; +use cssparser::{BasicParseError, BasicParseErrorKind, ParseError, ParseErrorKind}; +use cssparser::{CowRcStr, Delimiter, SourceLocation}; +use cssparser::{Parser as CssParser, ToCss, Token}; +use precomputed_hash::PrecomputedHash; +use servo_arc::{Arc, ArcUnionBorrow, ThinArc, ThinArcUnion, UniqueArc}; +use smallvec::SmallVec; +use std::borrow::{Borrow, Cow}; +use std::fmt::{self, Debug}; +use std::iter::Rev; +use std::slice; + +/// A trait that represents a pseudo-element. +pub trait PseudoElement: Sized + ToCss { + /// The `SelectorImpl` this pseudo-element is used for. + type Impl: SelectorImpl; + + /// Whether the pseudo-element supports a given state selector to the right + /// of it. + fn accepts_state_pseudo_classes(&self) -> bool { + false + } + + /// Whether this pseudo-element is valid after a ::slotted(..) pseudo. + fn valid_after_slotted(&self) -> bool { + false + } +} + +/// A trait that represents a pseudo-class. +pub trait NonTSPseudoClass: Sized + ToCss { + /// The `SelectorImpl` this pseudo-element is used for. + type Impl: SelectorImpl; + + /// Whether this pseudo-class is :active or :hover. + fn is_active_or_hover(&self) -> bool; + + /// Whether this pseudo-class belongs to: + /// + /// https://drafts.csswg.org/selectors-4/#useraction-pseudos + fn is_user_action_state(&self) -> bool; + + fn visit(&self, _visitor: &mut V) -> bool + where + V: SelectorVisitor, + { + true + } +} + +/// Returns a Cow::Borrowed if `s` is already ASCII lowercase, and a +/// Cow::Owned if `s` had to be converted into ASCII lowercase. +fn to_ascii_lowercase(s: &str) -> Cow { + if let Some(first_uppercase) = s.bytes().position(|byte| byte >= b'A' && byte <= b'Z') { + let mut string = s.to_owned(); + string[first_uppercase..].make_ascii_lowercase(); + string.into() + } else { + s.into() + } +} + +bitflags! { + /// Flags that indicate at which point of parsing a selector are we. + #[derive(Copy, Clone)] + struct SelectorParsingState: u8 { + /// Whether we should avoid adding default namespaces to selectors that + /// aren't type or universal selectors. + const SKIP_DEFAULT_NAMESPACE = 1 << 0; + + /// Whether we've parsed a ::slotted() pseudo-element already. + /// + /// If so, then we can only parse a subset of pseudo-elements, and + /// whatever comes after them if so. + const AFTER_SLOTTED = 1 << 1; + /// Whether we've parsed a ::part() pseudo-element already. + /// + /// If so, then we can only parse a subset of pseudo-elements, and + /// whatever comes after them if so. + const AFTER_PART = 1 << 2; + /// Whether we've parsed a pseudo-element (as in, an + /// `Impl::PseudoElement` thus not accounting for `::slotted` or + /// `::part`) already. + /// + /// If so, then other pseudo-elements and most other selectors are + /// disallowed. + const AFTER_PSEUDO_ELEMENT = 1 << 3; + /// Whether we've parsed a non-stateful pseudo-element (again, as-in + /// `Impl::PseudoElement`) already. If so, then other pseudo-classes are + /// disallowed. If this flag is set, `AFTER_PSEUDO_ELEMENT` must be set + /// as well. + const AFTER_NON_STATEFUL_PSEUDO_ELEMENT = 1 << 4; + + /// Whether we are after any of the pseudo-like things. + const AFTER_PSEUDO = Self::AFTER_PART.bits() | Self::AFTER_SLOTTED.bits() | Self::AFTER_PSEUDO_ELEMENT.bits(); + + /// Whether we explicitly disallow combinators. + const DISALLOW_COMBINATORS = 1 << 5; + + /// Whether we explicitly disallow pseudo-element-like things. + const DISALLOW_PSEUDOS = 1 << 6; + + /// Whether we explicitly disallow relative selectors (i.e. `:has()`). + const DISALLOW_RELATIVE_SELECTOR = 1 << 7; + } +} + +impl SelectorParsingState { + #[inline] + fn allows_pseudos(self) -> bool { + // NOTE(emilio): We allow pseudos after ::part and such. + !self.intersects(Self::AFTER_PSEUDO_ELEMENT | Self::DISALLOW_PSEUDOS) + } + + #[inline] + fn allows_slotted(self) -> bool { + !self.intersects(Self::AFTER_PSEUDO | Self::DISALLOW_PSEUDOS) + } + + #[inline] + fn allows_part(self) -> bool { + !self.intersects(Self::AFTER_PSEUDO | Self::DISALLOW_PSEUDOS) + } + + #[inline] + fn allows_non_functional_pseudo_classes(self) -> bool { + !self.intersects(Self::AFTER_SLOTTED | Self::AFTER_NON_STATEFUL_PSEUDO_ELEMENT) + } + + #[inline] + fn allows_tree_structural_pseudo_classes(self) -> bool { + !self.intersects(Self::AFTER_PSEUDO) + } + + #[inline] + fn allows_combinators(self) -> bool { + !self.intersects(Self::DISALLOW_COMBINATORS) + } +} + +pub type SelectorParseError<'i> = ParseError<'i, SelectorParseErrorKind<'i>>; + +#[derive(Clone, Debug, PartialEq)] +pub enum SelectorParseErrorKind<'i> { + NoQualifiedNameInAttributeSelector(Token<'i>), + EmptySelector, + DanglingCombinator, + NonCompoundSelector, + NonPseudoElementAfterSlotted, + InvalidPseudoElementAfterSlotted, + InvalidPseudoElementInsideWhere, + InvalidState, + UnexpectedTokenInAttributeSelector(Token<'i>), + PseudoElementExpectedColon(Token<'i>), + PseudoElementExpectedIdent(Token<'i>), + NoIdentForPseudo(Token<'i>), + UnsupportedPseudoClassOrElement(CowRcStr<'i>), + UnexpectedIdent(CowRcStr<'i>), + ExpectedNamespace(CowRcStr<'i>), + ExpectedBarInAttr(Token<'i>), + BadValueInAttr(Token<'i>), + InvalidQualNameInAttr(Token<'i>), + ExplicitNamespaceUnexpectedToken(Token<'i>), + ClassNeedsIdent(Token<'i>), +} + +macro_rules! with_all_bounds { + ( + [ $( $InSelector: tt )* ] + [ $( $CommonBounds: tt )* ] + [ $( $FromStr: tt )* ] + ) => { + /// This trait allows to define the parser implementation in regards + /// of pseudo-classes/elements + /// + /// NB: We need Clone so that we can derive(Clone) on struct with that + /// are parameterized on SelectorImpl. See + /// + pub trait SelectorImpl: Clone + Debug + Sized + 'static { + type ExtraMatchingData<'a>: Sized + Default; + type AttrValue: $($InSelector)*; + type Identifier: $($InSelector)* + PrecomputedHash; + type LocalName: $($InSelector)* + Borrow + PrecomputedHash; + type NamespaceUrl: $($CommonBounds)* + Default + Borrow + PrecomputedHash; + type NamespacePrefix: $($InSelector)* + Default; + type BorrowedNamespaceUrl: ?Sized + Eq; + type BorrowedLocalName: ?Sized + Eq; + + /// non tree-structural pseudo-classes + /// (see: https://drafts.csswg.org/selectors/#structural-pseudos) + type NonTSPseudoClass: $($CommonBounds)* + NonTSPseudoClass; + + /// pseudo-elements + type PseudoElement: $($CommonBounds)* + PseudoElement; + + /// Whether attribute hashes should be collected for filtering + /// purposes. + fn should_collect_attr_hash(_name: &Self::LocalName) -> bool { + false + } + } + } +} + +macro_rules! with_bounds { + ( [ $( $CommonBounds: tt )* ] [ $( $FromStr: tt )* ]) => { + with_all_bounds! { + [$($CommonBounds)* + $($FromStr)* + ToCss] + [$($CommonBounds)*] + [$($FromStr)*] + } + } +} + +with_bounds! { + [Clone + Eq] + [for<'a> From<&'a str>] +} + +pub trait Parser<'i> { + type Impl: SelectorImpl; + type Error: 'i + From>; + + /// Whether to parse the `::slotted()` pseudo-element. + fn parse_slotted(&self) -> bool { + false + } + + /// Whether to parse the `::part()` pseudo-element. + fn parse_part(&self) -> bool { + false + } + + /// Whether to parse the selector list of nth-child() or nth-last-child(). + fn parse_nth_child_of(&self) -> bool { + false + } + + /// Whether to parse the `:where` pseudo-class. + fn parse_is_and_where(&self) -> bool { + false + } + + /// Whether to parse the :has pseudo-class. + fn parse_has(&self) -> bool { + false + } + + /// Whether to parse the '&' delimiter as a parent selector. + fn parse_parent_selector(&self) -> bool { + false + } + + /// Whether the given function name is an alias for the `:is()` function. + fn is_is_alias(&self, _name: &str) -> bool { + false + } + + /// Whether to parse the `:host` pseudo-class. + fn parse_host(&self) -> bool { + false + } + + /// Whether to allow forgiving selector-list parsing. + fn allow_forgiving_selectors(&self) -> bool { + true + } + + /// This function can return an "Err" pseudo-element in order to support CSS2.1 + /// pseudo-elements. + fn parse_non_ts_pseudo_class( + &self, + location: SourceLocation, + name: CowRcStr<'i>, + ) -> Result<::NonTSPseudoClass, ParseError<'i, Self::Error>> { + Err( + location.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement( + name, + )), + ) + } + + fn parse_non_ts_functional_pseudo_class<'t>( + &self, + name: CowRcStr<'i>, + parser: &mut CssParser<'i, 't>, + _after_part: bool, + ) -> Result<::NonTSPseudoClass, ParseError<'i, Self::Error>> { + Err( + parser.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement( + name, + )), + ) + } + + fn parse_pseudo_element( + &self, + location: SourceLocation, + name: CowRcStr<'i>, + ) -> Result<::PseudoElement, ParseError<'i, Self::Error>> { + Err( + location.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement( + name, + )), + ) + } + + fn parse_functional_pseudo_element<'t>( + &self, + name: CowRcStr<'i>, + arguments: &mut CssParser<'i, 't>, + ) -> Result<::PseudoElement, ParseError<'i, Self::Error>> { + Err( + arguments.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement( + name, + )), + ) + } + + fn default_namespace(&self) -> Option<::NamespaceUrl> { + None + } + + fn namespace_for_prefix( + &self, + _prefix: &::NamespacePrefix, + ) -> Option<::NamespaceUrl> { + None + } +} + +/// A selector list is a tagged pointer with either a single selector, or a ThinArc<()> of multiple +/// selectors. +#[derive(Clone, Eq, Debug, PartialEq, ToShmem)] +#[shmem(no_bounds)] +pub struct SelectorList( + #[shmem(field_bound)] ThinArcUnion, (), Selector>, +); + +impl SelectorList { + pub fn from_one(selector: Selector) -> Self { + #[cfg(debug_assertions)] + let selector_repr = unsafe { *(&selector as *const _ as *const usize) }; + let list = Self(ThinArcUnion::from_first(selector.into_data())); + #[cfg(debug_assertions)] + debug_assert_eq!( + selector_repr, + unsafe { *(&list as *const _ as *const usize) }, + "We rely on the same bit representation for the single selector variant" + ); + list + } + + pub fn from_iter(mut iter: impl ExactSizeIterator>) -> Self { + if iter.len() == 1 { + Self::from_one(iter.next().unwrap()) + } else { + Self(ThinArcUnion::from_second(ThinArc::from_header_and_iter( + (), + iter, + ))) + } + } + + #[inline] + pub fn slice(&self) -> &[Selector] { + match self.0.borrow() { + ArcUnionBorrow::First(..) => { + // SAFETY: see from_one. + let selector: &Selector = unsafe { std::mem::transmute(self) }; + std::slice::from_ref(selector) + }, + ArcUnionBorrow::Second(list) => list.get().slice(), + } + } + + #[inline] + pub fn len(&self) -> usize { + match self.0.borrow() { + ArcUnionBorrow::First(..) => 1, + ArcUnionBorrow::Second(list) => list.len(), + } + } + + /// Returns the address on the heap of the ThinArc for memory reporting. + pub fn thin_arc_heap_ptr(&self) -> *const ::std::os::raw::c_void { + match self.0.borrow() { + ArcUnionBorrow::First(s) => s.with_arc(|a| a.heap_ptr()), + ArcUnionBorrow::Second(s) => s.with_arc(|a| a.heap_ptr()), + } + } +} + +/// Uniquely identify a selector based on its components, which is behind ThinArc and +/// is therefore stable. +#[derive(Clone, Copy, Hash, Eq, PartialEq)] +pub struct SelectorKey(usize); + +impl SelectorKey { + /// Create a new key based on the given selector. + pub fn new(selector: &Selector) -> Self { + Self(selector.0.slice().as_ptr() as usize) + } +} + +/// Whether or not we're using forgiving parsing mode +#[derive(PartialEq)] +enum ForgivingParsing { + /// Discard the entire selector list upon encountering any invalid selector. + /// This is the default behavior for almost all of CSS. + No, + /// Ignore invalid selectors, potentially creating an empty selector list. + /// + /// This is the error recovery mode of :is() and :where() + Yes, +} + +/// Flag indicating if we're parsing relative selectors. +#[derive(Copy, Clone, PartialEq)] +pub enum ParseRelative { + /// Expect selectors to start with a combinator, assuming descendant combinator if not present. + ForHas, + /// Allow selectors to start with a combinator, prepending a parent selector if so. Do nothing + /// otherwise + ForNesting, + /// Treat as parse error if any selector begins with a combinator. + No, +} + +impl SelectorList { + /// Returns a selector list with a single `&` + pub fn ampersand() -> Self { + Self::from_one(Selector::ampersand()) + } + + /// Parse a comma-separated list of Selectors. + /// + /// + /// Return the Selectors or Err if there is an invalid selector. + pub fn parse<'i, 't, P>( + parser: &P, + input: &mut CssParser<'i, 't>, + parse_relative: ParseRelative, + ) -> Result> + where + P: Parser<'i, Impl = Impl>, + { + Self::parse_with_state( + parser, + input, + SelectorParsingState::empty(), + ForgivingParsing::No, + parse_relative, + ) + } + + #[inline] + fn parse_with_state<'i, 't, P>( + parser: &P, + input: &mut CssParser<'i, 't>, + state: SelectorParsingState, + recovery: ForgivingParsing, + parse_relative: ParseRelative, + ) -> Result> + where + P: Parser<'i, Impl = Impl>, + { + let mut values = SmallVec::<[_; 4]>::new(); + let forgiving = recovery == ForgivingParsing::Yes && parser.allow_forgiving_selectors(); + loop { + let selector = input.parse_until_before(Delimiter::Comma, |input| { + let start = input.position(); + let mut selector = parse_selector(parser, input, state, parse_relative); + if forgiving && (selector.is_err() || input.expect_exhausted().is_err()) { + input.expect_no_error_token()?; + selector = Ok(Selector::new_invalid(input.slice_from(start))); + } + selector + })?; + + values.push(selector); + + match input.next() { + Ok(&Token::Comma) => {}, + Ok(_) => unreachable!(), + Err(_) => break, + } + } + Ok(Self::from_iter(values.into_iter())) + } + + /// Replaces the parent selector in all the items of the selector list. + pub fn replace_parent_selector(&self, parent: &SelectorList) -> Self { + Self::from_iter( + self.slice() + .iter() + .map(|selector| selector.replace_parent_selector(parent)), + ) + } + + /// Creates a SelectorList from a Vec of selectors. Used in tests. + #[allow(dead_code)] + pub(crate) fn from_vec(v: Vec>) -> Self { + SelectorList::from_iter(v.into_iter()) + } +} + +/// Parses one compound selector suitable for nested stuff like :-moz-any, etc. +fn parse_inner_compound_selector<'i, 't, P, Impl>( + parser: &P, + input: &mut CssParser<'i, 't>, + state: SelectorParsingState, +) -> Result, ParseError<'i, P::Error>> +where + P: Parser<'i, Impl = Impl>, + Impl: SelectorImpl, +{ + parse_selector( + parser, + input, + state | SelectorParsingState::DISALLOW_PSEUDOS | SelectorParsingState::DISALLOW_COMBINATORS, + ParseRelative::No, + ) +} + +/// Ancestor hashes for the bloom filter. We precompute these and store them +/// inline with selectors to optimize cache performance during matching. +/// This matters a lot. +/// +/// We use 4 hashes, which is copied from Gecko, who copied it from WebKit. +/// Note that increasing the number of hashes here will adversely affect the +/// cache hit when fast-rejecting long lists of Rules with inline hashes. +/// +/// Because the bloom filter only uses the bottom 24 bits of the hash, we pack +/// the fourth hash into the upper bits of the first three hashes in order to +/// shrink Rule (whose size matters a lot). This scheme minimizes the runtime +/// overhead of the packing for the first three hashes (we just need to mask +/// off the upper bits) at the expense of making the fourth somewhat more +/// complicated to assemble, because we often bail out before checking all the +/// hashes. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AncestorHashes { + pub packed_hashes: [u32; 3], +} + +pub(crate) fn collect_selector_hashes<'a, Impl: SelectorImpl, Iter>( + iter: Iter, + quirks_mode: QuirksMode, + hashes: &mut [u32; 4], + len: &mut usize, + create_inner_iterator: fn(&'a Selector) -> Iter, +) -> bool +where + Iter: Iterator>, +{ + for component in iter { + let hash = match *component { + Component::LocalName(LocalName { + ref name, + ref lower_name, + }) => { + // Only insert the local-name into the filter if it's all + // lowercase. Otherwise we would need to test both hashes, and + // our data structures aren't really set up for that. + if name != lower_name { + continue; + } + name.precomputed_hash() + }, + Component::DefaultNamespace(ref url) | Component::Namespace(_, ref url) => { + url.precomputed_hash() + }, + // In quirks mode, class and id selectors should match + // case-insensitively, so just avoid inserting them into the filter. + Component::ID(ref id) if quirks_mode != QuirksMode::Quirks => id.precomputed_hash(), + Component::Class(ref class) if quirks_mode != QuirksMode::Quirks => { + class.precomputed_hash() + }, + Component::AttributeInNoNamespace { ref local_name, .. } + if Impl::should_collect_attr_hash(local_name) => + { + // AttributeInNoNamespace is only used when local_name == + // local_name_lower. + local_name.precomputed_hash() + }, + Component::AttributeInNoNamespaceExists { + ref local_name, + ref local_name_lower, + .. + } => { + // Only insert the local-name into the filter if it's all + // lowercase. Otherwise we would need to test both hashes, and + // our data structures aren't really set up for that. + if local_name != local_name_lower || !Impl::should_collect_attr_hash(local_name) { + continue; + } + local_name.precomputed_hash() + }, + Component::AttributeOther(ref selector) => { + if selector.local_name != selector.local_name_lower || + !Impl::should_collect_attr_hash(&selector.local_name) + { + continue; + } + selector.local_name.precomputed_hash() + }, + Component::Is(ref list) | Component::Where(ref list) => { + // :where and :is OR their selectors, so we can't put any hash + // in the filter if there's more than one selector, as that'd + // exclude elements that may match one of the other selectors. + let slice = list.slice(); + if slice.len() == 1 && + !collect_selector_hashes( + create_inner_iterator(&slice[0]), + quirks_mode, + hashes, + len, + create_inner_iterator, + ) + { + return false; + } + continue; + }, + _ => continue, + }; + + hashes[*len] = hash & BLOOM_HASH_MASK; + *len += 1; + if *len == hashes.len() { + return false; + } + } + true +} + +fn collect_ancestor_hashes( + iter: SelectorIter, + quirks_mode: QuirksMode, + hashes: &mut [u32; 4], + len: &mut usize, +) { + collect_selector_hashes(AncestorIter::new(iter), quirks_mode, hashes, len, |s| { + AncestorIter(s.iter()) + }); +} + +impl AncestorHashes { + pub fn new(selector: &Selector, quirks_mode: QuirksMode) -> Self { + // Compute ancestor hashes for the bloom filter. + let mut hashes = [0u32; 4]; + let mut len = 0; + collect_ancestor_hashes(selector.iter(), quirks_mode, &mut hashes, &mut len); + debug_assert!(len <= 4); + + // Now, pack the fourth hash (if it exists) into the upper byte of each of + // the other three hashes. + if len == 4 { + let fourth = hashes[3]; + hashes[0] |= (fourth & 0x000000ff) << 24; + hashes[1] |= (fourth & 0x0000ff00) << 16; + hashes[2] |= (fourth & 0x00ff0000) << 8; + } + + AncestorHashes { + packed_hashes: [hashes[0], hashes[1], hashes[2]], + } + } + + /// Returns the fourth hash, reassembled from parts. + pub fn fourth_hash(&self) -> u32 { + ((self.packed_hashes[0] & 0xff000000) >> 24) | + ((self.packed_hashes[1] & 0xff000000) >> 16) | + ((self.packed_hashes[2] & 0xff000000) >> 8) + } +} + +#[inline] +pub fn namespace_empty_string() -> Impl::NamespaceUrl { + // Rust type’s default, not default namespace + Impl::NamespaceUrl::default() +} + +type SelectorData = ThinArc>; + +/// A Selector stores a sequence of simple selectors and combinators. The +/// iterator classes allow callers to iterate at either the raw sequence level or +/// at the level of sequences of simple selectors separated by combinators. Most +/// callers want the higher-level iterator. +/// +/// We store compound selectors internally right-to-left (in matching order). +/// Additionally, we invert the order of top-level compound selectors so that +/// each one matches left-to-right. This is because matching namespace, local name, +/// id, and class are all relatively cheap, whereas matching pseudo-classes might +/// be expensive (depending on the pseudo-class). Since authors tend to put the +/// pseudo-classes on the right, it's faster to start matching on the left. +/// +/// This reordering doesn't change the semantics of selector matching, and we +/// handle it in to_css to make it invisible to serialization. +#[derive(Clone, Eq, PartialEq, ToShmem)] +#[shmem(no_bounds)] +#[repr(transparent)] +pub struct Selector(#[shmem(field_bound)] SelectorData); + +impl Selector { + /// See Arc::mark_as_intentionally_leaked + pub fn mark_as_intentionally_leaked(&self) { + self.0.mark_as_intentionally_leaked() + } + + fn ampersand() -> Self { + Self(ThinArc::from_header_and_iter( + SpecificityAndFlags { + specificity: 0, + flags: SelectorFlags::HAS_PARENT, + }, + std::iter::once(Component::ParentSelector), + )) + } + + #[inline] + pub fn specificity(&self) -> u32 { + self.0.header.specificity + } + + #[inline] + pub(crate) fn flags(&self) -> SelectorFlags { + self.0.header.flags + } + + #[inline] + pub fn has_pseudo_element(&self) -> bool { + self.flags().intersects(SelectorFlags::HAS_PSEUDO) + } + + #[inline] + pub fn has_parent_selector(&self) -> bool { + self.flags().intersects(SelectorFlags::HAS_PARENT) + } + + #[inline] + pub fn is_slotted(&self) -> bool { + self.flags().intersects(SelectorFlags::HAS_SLOTTED) + } + + #[inline] + pub fn is_part(&self) -> bool { + self.flags().intersects(SelectorFlags::HAS_PART) + } + + #[inline] + pub fn parts(&self) -> Option<&[Impl::Identifier]> { + if !self.is_part() { + return None; + } + + let mut iter = self.iter(); + if self.has_pseudo_element() { + // Skip the pseudo-element. + for _ in &mut iter {} + + let combinator = iter.next_sequence()?; + debug_assert_eq!(combinator, Combinator::PseudoElement); + } + + for component in iter { + if let Component::Part(ref part) = *component { + return Some(part); + } + } + + debug_assert!(false, "is_part() lied somehow?"); + None + } + + #[inline] + pub fn pseudo_element(&self) -> Option<&Impl::PseudoElement> { + if !self.has_pseudo_element() { + return None; + } + + for component in self.iter() { + if let Component::PseudoElement(ref pseudo) = *component { + return Some(pseudo); + } + } + + debug_assert!(false, "has_pseudo_element lied!"); + None + } + + /// Whether this selector (pseudo-element part excluded) matches every element. + /// + /// Used for "pre-computed" pseudo-elements in components/style/stylist.rs + #[inline] + pub fn is_universal(&self) -> bool { + self.iter_raw_match_order().all(|c| { + matches!( + *c, + Component::ExplicitUniversalType | + Component::ExplicitAnyNamespace | + Component::Combinator(Combinator::PseudoElement) | + Component::PseudoElement(..) + ) + }) + } + + /// Returns an iterator over this selector in matching order (right-to-left). + /// When a combinator is reached, the iterator will return None, and + /// next_sequence() may be called to continue to the next sequence. + #[inline] + pub fn iter(&self) -> SelectorIter { + SelectorIter { + iter: self.iter_raw_match_order(), + next_combinator: None, + } + } + + /// Same as `iter()`, but skips `RelativeSelectorAnchor` and its associated combinator. + #[inline] + pub fn iter_skip_relative_selector_anchor(&self) -> SelectorIter { + if cfg!(debug_assertions) { + let mut selector_iter = self.iter_raw_parse_order_from(0); + assert!( + matches!( + selector_iter.next().unwrap(), + Component::RelativeSelectorAnchor + ), + "Relative selector does not start with RelativeSelectorAnchor" + ); + assert!( + selector_iter.next().unwrap().is_combinator(), + "Relative combinator does not exist" + ); + } + + SelectorIter { + iter: self.0.slice()[..self.len() - 2].iter(), + next_combinator: None, + } + } + + /// Whether this selector is a featureless :host selector, with no combinators to the left, and + /// optionally has a pseudo-element to the right. + #[inline] + pub fn is_featureless_host_selector_or_pseudo_element(&self) -> bool { + let flags = self.flags(); + flags.intersects(SelectorFlags::HAS_HOST) && + !flags.intersects(SelectorFlags::HAS_NON_FEATURELESS_COMPONENT) + } + + /// Returns an iterator over this selector in matching order (right-to-left), + /// skipping the rightmost |offset| Components. + #[inline] + pub fn iter_from(&self, offset: usize) -> SelectorIter { + let iter = self.0.slice()[offset..].iter(); + SelectorIter { + iter, + next_combinator: None, + } + } + + /// Returns the combinator at index `index` (zero-indexed from the right), + /// or panics if the component is not a combinator. + #[inline] + pub fn combinator_at_match_order(&self, index: usize) -> Combinator { + match self.0.slice()[index] { + Component::Combinator(c) => c, + ref other => panic!( + "Not a combinator: {:?}, {:?}, index: {}", + other, self, index + ), + } + } + + /// Returns an iterator over the entire sequence of simple selectors and + /// combinators, in matching order (from right to left). + #[inline] + pub fn iter_raw_match_order(&self) -> slice::Iter> { + self.0.slice().iter() + } + + /// Returns the combinator at index `index` (zero-indexed from the left), + /// or panics if the component is not a combinator. + #[inline] + pub fn combinator_at_parse_order(&self, index: usize) -> Combinator { + match self.0.slice()[self.len() - index - 1] { + Component::Combinator(c) => c, + ref other => panic!( + "Not a combinator: {:?}, {:?}, index: {}", + other, self, index + ), + } + } + + /// Returns an iterator over the sequence of simple selectors and + /// combinators, in parse order (from left to right), starting from + /// `offset`. + #[inline] + pub fn iter_raw_parse_order_from(&self, offset: usize) -> Rev>> { + self.0.slice()[..self.len() - offset].iter().rev() + } + + /// Creates a Selector from a vec of Components, specified in parse order. Used in tests. + #[allow(dead_code)] + pub(crate) fn from_vec( + vec: Vec>, + specificity: u32, + flags: SelectorFlags, + ) -> Self { + let mut builder = SelectorBuilder::default(); + for component in vec.into_iter() { + if let Some(combinator) = component.as_combinator() { + builder.push_combinator(combinator); + } else { + builder.push_simple_selector(component); + } + } + let spec = SpecificityAndFlags { specificity, flags }; + Selector(builder.build_with_specificity_and_flags(spec)) + } + + #[inline] + fn into_data(self) -> SelectorData { + self.0 + } + + pub fn replace_parent_selector(&self, parent: &SelectorList) -> Self { + let parent_specificity_and_flags = + selector_list_specificity_and_flags(parent.slice().iter()); + + let mut specificity = Specificity::from(self.specificity()); + let mut flags = self.flags() - SelectorFlags::HAS_PARENT; + + fn replace_parent_on_selector_list( + orig: &[Selector], + parent: &SelectorList, + specificity: &mut Specificity, + flags: &mut SelectorFlags, + propagate_specificity: bool, + flags_to_propagate: SelectorFlags, + ) -> Option> { + if !orig.iter().any(|s| s.has_parent_selector()) { + return None; + } + + let result = SelectorList::from_iter(orig.iter().map(|s| { + if !s.has_parent_selector() { + return s.clone(); + } + s.replace_parent_selector(parent) + })); + + let result_specificity_and_flags = + selector_list_specificity_and_flags(result.slice().iter()); + if propagate_specificity { + *specificity += Specificity::from( + result_specificity_and_flags.specificity - + selector_list_specificity_and_flags(orig.iter()).specificity, + ); + } + flags.insert( + result_specificity_and_flags + .flags + .intersection(flags_to_propagate), + ); + Some(result) + } + + fn replace_parent_on_relative_selector_list( + orig: &[RelativeSelector], + parent: &SelectorList, + specificity: &mut Specificity, + flags: &mut SelectorFlags, + flags_to_propagate: SelectorFlags, + ) -> Vec> { + let mut any = false; + + let result = orig + .iter() + .map(|s| { + if !s.selector.has_parent_selector() { + return s.clone(); + } + any = true; + RelativeSelector { + match_hint: s.match_hint, + selector: s.selector.replace_parent_selector(parent), + } + }) + .collect(); + + if !any { + return result; + } + + let result_specificity_and_flags = + relative_selector_list_specificity_and_flags(&result); + flags.insert( + result_specificity_and_flags + .flags + .intersection(flags_to_propagate), + ); + *specificity += Specificity::from( + result_specificity_and_flags.specificity - + relative_selector_list_specificity_and_flags(orig).specificity, + ); + result + } + + fn replace_parent_on_selector( + orig: &Selector, + parent: &SelectorList, + specificity: &mut Specificity, + flags: &mut SelectorFlags, + flags_to_propagate: SelectorFlags, + ) -> Selector { + if !orig.has_parent_selector() { + return orig.clone(); + } + let new_selector = orig.replace_parent_selector(parent); + *specificity += Specificity::from(new_selector.specificity() - orig.specificity()); + flags.insert(new_selector.flags().intersection(flags_to_propagate)); + new_selector + } + + let mut items = if !self.has_parent_selector() { + // Implicit `&` plus descendant combinator. + let iter = self.iter_raw_match_order(); + let len = iter.len() + 2; + specificity += Specificity::from(parent_specificity_and_flags.specificity); + flags.insert( + parent_specificity_and_flags + .flags + .intersection(SelectorFlags::for_nesting()), + ); + let iter = iter + .cloned() + .chain(std::iter::once(Component::Combinator( + Combinator::Descendant, + ))) + .chain(std::iter::once(Component::Is(parent.clone()))); + UniqueArc::from_header_and_iter_with_size(Default::default(), iter, len) + } else { + let iter = self.iter_raw_match_order().map(|component| { + use self::Component::*; + match *component { + LocalName(..) | + ID(..) | + Class(..) | + AttributeInNoNamespaceExists { .. } | + AttributeInNoNamespace { .. } | + AttributeOther(..) | + ExplicitUniversalType | + ExplicitAnyNamespace | + ExplicitNoNamespace | + DefaultNamespace(..) | + Namespace(..) | + Root | + Empty | + Scope | + Nth(..) | + NonTSPseudoClass(..) | + PseudoElement(..) | + Combinator(..) | + Host(None) | + Part(..) | + Invalid(..) | + RelativeSelectorAnchor => component.clone(), + ParentSelector => { + specificity += Specificity::from(parent_specificity_and_flags.specificity); + flags.insert( + parent_specificity_and_flags + .flags + .intersection(SelectorFlags::for_nesting()), + ); + Is(parent.clone()) + }, + Negation(ref selectors) => { + Negation( + replace_parent_on_selector_list( + selectors.slice(), + parent, + &mut specificity, + &mut flags, + /* propagate_specificity = */ true, + SelectorFlags::for_nesting(), + ) + .unwrap_or_else(|| selectors.clone()), + ) + }, + Is(ref selectors) => { + Is(replace_parent_on_selector_list( + selectors.slice(), + parent, + &mut specificity, + &mut flags, + /* propagate_specificity = */ true, + SelectorFlags::for_nesting(), + ) + .unwrap_or_else(|| selectors.clone())) + }, + Where(ref selectors) => { + Where( + replace_parent_on_selector_list( + selectors.slice(), + parent, + &mut specificity, + &mut flags, + /* propagate_specificity = */ false, + SelectorFlags::for_nesting(), + ) + .unwrap_or_else(|| selectors.clone()), + ) + }, + Has(ref selectors) => Has(replace_parent_on_relative_selector_list( + selectors, + parent, + &mut specificity, + &mut flags, + SelectorFlags::for_nesting(), + ) + .into_boxed_slice()), + + Host(Some(ref selector)) => Host(Some(replace_parent_on_selector( + selector, + parent, + &mut specificity, + &mut flags, + SelectorFlags::for_nesting() - SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + ))), + NthOf(ref data) => { + let selectors = replace_parent_on_selector_list( + data.selectors(), + parent, + &mut specificity, + &mut flags, + /* propagate_specificity = */ true, + SelectorFlags::for_nesting(), + ); + NthOf(match selectors { + Some(s) => { + NthOfSelectorData::new(data.nth_data(), s.slice().iter().cloned()) + }, + None => data.clone(), + }) + }, + Slotted(ref selector) => Slotted(replace_parent_on_selector( + selector, + parent, + &mut specificity, + &mut flags, + SelectorFlags::for_nesting(), + )), + } + }); + UniqueArc::from_header_and_iter(Default::default(), iter) + }; + *items.header_mut() = SpecificityAndFlags { + specificity: specificity.into(), + flags, + }; + Selector(items.shareable()) + } + + /// Returns count of simple selectors and combinators in the Selector. + #[inline] + pub fn len(&self) -> usize { + self.0.len() + } + + /// Returns the address on the heap of the ThinArc for memory reporting. + pub fn thin_arc_heap_ptr(&self) -> *const ::std::os::raw::c_void { + self.0.heap_ptr() + } + + /// Traverse selector components inside `self`. + /// + /// Implementations of this method should call `SelectorVisitor` methods + /// or other impls of `Visit` as appropriate based on the fields of `Self`. + /// + /// A return value of `false` indicates terminating the traversal. + /// It should be propagated with an early return. + /// On the contrary, `true` indicates that all fields of `self` have been traversed: + /// + /// ```rust,ignore + /// if !visitor.visit_simple_selector(&self.some_simple_selector) { + /// return false; + /// } + /// if !self.some_component.visit(visitor) { + /// return false; + /// } + /// true + /// ``` + pub fn visit(&self, visitor: &mut V) -> bool + where + V: SelectorVisitor, + { + let mut current = self.iter(); + let mut combinator = None; + loop { + if !visitor.visit_complex_selector(combinator) { + return false; + } + + for selector in &mut current { + if !selector.visit(visitor) { + return false; + } + } + + combinator = current.next_sequence(); + if combinator.is_none() { + break; + } + } + + true + } + + /// Parse a selector, without any pseudo-element. + #[inline] + pub fn parse<'i, 't, P>( + parser: &P, + input: &mut CssParser<'i, 't>, + ) -> Result> + where + P: Parser<'i, Impl = Impl>, + { + parse_selector( + parser, + input, + SelectorParsingState::empty(), + ParseRelative::No, + ) + } + + pub fn new_invalid(s: &str) -> Self { + fn check_for_parent(input: &mut CssParser, has_parent: &mut bool) { + while let Ok(t) = input.next() { + match *t { + Token::Function(_) | + Token::ParenthesisBlock | + Token::CurlyBracketBlock | + Token::SquareBracketBlock => { + let _ = input.parse_nested_block( + |i| -> Result<(), ParseError<'_, BasicParseError>> { + check_for_parent(i, has_parent); + Ok(()) + }, + ); + }, + Token::Delim('&') => { + *has_parent = true; + }, + _ => {}, + } + if *has_parent { + break; + } + } + } + let mut has_parent = false; + { + let mut parser = cssparser::ParserInput::new(s); + let mut parser = CssParser::new(&mut parser); + check_for_parent(&mut parser, &mut has_parent); + } + Self(ThinArc::from_header_and_iter( + SpecificityAndFlags { + specificity: 0, + flags: if has_parent { + SelectorFlags::HAS_PARENT + } else { + SelectorFlags::empty() + }, + }, + std::iter::once(Component::Invalid(Arc::new(String::from(s.trim())))), + )) + } + + /// Is the compound starting at the offset the subject compound, or referring to its pseudo-element? + pub fn is_rightmost(&self, offset: usize) -> bool { + // There can really be only one pseudo-element, and it's not really valid for anything else to + // follow it. + offset == 0 || matches!(self.combinator_at_match_order(offset - 1), Combinator::PseudoElement) + } +} + +#[derive(Clone)] +pub struct SelectorIter<'a, Impl: 'a + SelectorImpl> { + iter: slice::Iter<'a, Component>, + next_combinator: Option, +} + +impl<'a, Impl: 'a + SelectorImpl> SelectorIter<'a, Impl> { + /// Prepares this iterator to point to the next sequence to the left, + /// returning the combinator if the sequence was found. + #[inline] + pub fn next_sequence(&mut self) -> Option { + self.next_combinator.take() + } + + /// Whether this selector is a featureless host selector, with no + /// combinators to the left. + #[inline] + pub(crate) fn is_featureless_host_selector(&mut self) -> bool { + self.selector_length() > 0 && + self.all(|component| component.matches_featureless_host()) && + self.next_sequence().is_none() + } + + #[inline] + pub(crate) fn matches_for_stateless_pseudo_element(&mut self) -> bool { + let first = match self.next() { + Some(c) => c, + // Note that this is the common path that we keep inline: the + // pseudo-element not having anything to its right. + None => return true, + }; + self.matches_for_stateless_pseudo_element_internal(first) + } + + #[inline(never)] + fn matches_for_stateless_pseudo_element_internal(&mut self, first: &Component) -> bool { + if !first.matches_for_stateless_pseudo_element() { + return false; + } + for component in self { + // The only other parser-allowed Components in this sequence are + // state pseudo-classes, or one of the other things that can contain + // them. + if !component.matches_for_stateless_pseudo_element() { + return false; + } + } + true + } + + /// Returns remaining count of the simple selectors and combinators in the Selector. + #[inline] + pub fn selector_length(&self) -> usize { + self.iter.len() + } +} + +impl<'a, Impl: SelectorImpl> Iterator for SelectorIter<'a, Impl> { + type Item = &'a Component; + + #[inline] + fn next(&mut self) -> Option { + debug_assert!( + self.next_combinator.is_none(), + "You should call next_sequence!" + ); + match *self.iter.next()? { + Component::Combinator(c) => { + self.next_combinator = Some(c); + None + }, + ref x => Some(x), + } + } +} + +impl<'a, Impl: SelectorImpl> fmt::Debug for SelectorIter<'a, Impl> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let iter = self.iter.clone().rev(); + for component in iter { + component.to_css(f)? + } + Ok(()) + } +} + +/// An iterator over all combinators in a selector. Does not traverse selectors within psuedoclasses. +struct CombinatorIter<'a, Impl: 'a + SelectorImpl>(SelectorIter<'a, Impl>); +impl<'a, Impl: 'a + SelectorImpl> CombinatorIter<'a, Impl> { + fn new(inner: SelectorIter<'a, Impl>) -> Self { + let mut result = CombinatorIter(inner); + result.consume_non_combinators(); + result + } + + fn consume_non_combinators(&mut self) { + while self.0.next().is_some() {} + } +} + +impl<'a, Impl: SelectorImpl> Iterator for CombinatorIter<'a, Impl> { + type Item = Combinator; + fn next(&mut self) -> Option { + let result = self.0.next_sequence(); + self.consume_non_combinators(); + result + } +} + +/// An iterator over all simple selectors belonging to ancestors. +struct AncestorIter<'a, Impl: 'a + SelectorImpl>(SelectorIter<'a, Impl>); +impl<'a, Impl: 'a + SelectorImpl> AncestorIter<'a, Impl> { + /// Creates an AncestorIter. The passed-in iterator is assumed to point to + /// the beginning of the child sequence, which will be skipped. + fn new(inner: SelectorIter<'a, Impl>) -> Self { + let mut result = AncestorIter(inner); + result.skip_until_ancestor(); + result + } + + /// Skips a sequence of simple selectors and all subsequent sequences until + /// a non-pseudo-element ancestor combinator is reached. + fn skip_until_ancestor(&mut self) { + loop { + while self.0.next().is_some() {} + // If this is ever changed to stop at the "pseudo-element" + // combinator, we will need to fix the way we compute hashes for + // revalidation selectors. + if self.0.next_sequence().map_or(true, |x| { + matches!(x, Combinator::Child | Combinator::Descendant) + }) { + break; + } + } + } +} + +impl<'a, Impl: SelectorImpl> Iterator for AncestorIter<'a, Impl> { + type Item = &'a Component; + fn next(&mut self) -> Option { + // Grab the next simple selector in the sequence if available. + let next = self.0.next(); + if next.is_some() { + return next; + } + + // See if there are more sequences. If so, skip any non-ancestor sequences. + if let Some(combinator) = self.0.next_sequence() { + if !matches!(combinator, Combinator::Child | Combinator::Descendant) { + self.skip_until_ancestor(); + } + } + + self.0.next() + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, ToShmem)] +pub enum Combinator { + Child, // > + Descendant, // space + NextSibling, // + + LaterSibling, // ~ + /// A dummy combinator we use to the left of pseudo-elements. + /// + /// It serializes as the empty string, and acts effectively as a child + /// combinator in most cases. If we ever actually start using a child + /// combinator for this, we will need to fix up the way hashes are computed + /// for revalidation selectors. + PseudoElement, + /// Another combinator used for ::slotted(), which represent the jump from + /// a node to its assigned slot. + SlotAssignment, + /// Another combinator used for `::part()`, which represents the jump from + /// the part to the containing shadow host. + Part, +} + +impl Combinator { + /// Returns true if this combinator is a child or descendant combinator. + #[inline] + pub fn is_ancestor(&self) -> bool { + matches!( + *self, + Combinator::Child | + Combinator::Descendant | + Combinator::PseudoElement | + Combinator::SlotAssignment + ) + } + + /// Returns true if this combinator is a pseudo-element combinator. + #[inline] + pub fn is_pseudo_element(&self) -> bool { + matches!(*self, Combinator::PseudoElement) + } + + /// Returns true if this combinator is a next- or later-sibling combinator. + #[inline] + pub fn is_sibling(&self) -> bool { + matches!(*self, Combinator::NextSibling | Combinator::LaterSibling) + } +} + +/// An enum for the different types of :nth- pseudoclasses +#[derive(Copy, Clone, Eq, PartialEq, ToShmem)] +#[shmem(no_bounds)] +pub enum NthType { + Child, + LastChild, + OnlyChild, + OfType, + LastOfType, + OnlyOfType, +} + +impl NthType { + pub fn is_only(self) -> bool { + self == Self::OnlyChild || self == Self::OnlyOfType + } + + pub fn is_of_type(self) -> bool { + self == Self::OfType || self == Self::LastOfType || self == Self::OnlyOfType + } + + pub fn is_from_end(self) -> bool { + self == Self::LastChild || self == Self::LastOfType + } +} + +/// The properties that comprise an :nth- pseudoclass as of Selectors 3 (e.g., +/// nth-child(An+B)). +/// https://www.w3.org/TR/selectors-3/#nth-child-pseudo +#[derive(Copy, Clone, Eq, PartialEq, ToShmem)] +#[shmem(no_bounds)] +pub struct NthSelectorData { + pub ty: NthType, + pub is_function: bool, + pub a: i32, + pub b: i32, +} + +impl NthSelectorData { + /// Returns selector data for :only-{child,of-type} + #[inline] + pub const fn only(of_type: bool) -> Self { + Self { + ty: if of_type { + NthType::OnlyOfType + } else { + NthType::OnlyChild + }, + is_function: false, + a: 0, + b: 1, + } + } + + /// Returns selector data for :first-{child,of-type} + #[inline] + pub const fn first(of_type: bool) -> Self { + Self { + ty: if of_type { + NthType::OfType + } else { + NthType::Child + }, + is_function: false, + a: 0, + b: 1, + } + } + + /// Returns selector data for :last-{child,of-type} + #[inline] + pub const fn last(of_type: bool) -> Self { + Self { + ty: if of_type { + NthType::LastOfType + } else { + NthType::LastChild + }, + is_function: false, + a: 0, + b: 1, + } + } + + /// Returns true if this is an edge selector that is not `:*-of-type`` + #[inline] + pub fn is_simple_edge(&self) -> bool { + self.a == 0 && self.b == 1 && !self.ty.is_of_type() + } + + /// Writes the beginning of the selector. + #[inline] + fn write_start(&self, dest: &mut W) -> fmt::Result { + dest.write_str(match self.ty { + NthType::Child if self.is_function => ":nth-child(", + NthType::Child => ":first-child", + NthType::LastChild if self.is_function => ":nth-last-child(", + NthType::LastChild => ":last-child", + NthType::OfType if self.is_function => ":nth-of-type(", + NthType::OfType => ":first-of-type", + NthType::LastOfType if self.is_function => ":nth-last-of-type(", + NthType::LastOfType => ":last-of-type", + NthType::OnlyChild => ":only-child", + NthType::OnlyOfType => ":only-of-type", + }) + } + + /// Serialize (part of the CSS Syntax spec, but currently only used here). + /// + #[inline] + fn write_affine(&self, dest: &mut W) -> fmt::Result { + match (self.a, self.b) { + (0, 0) => dest.write_char('0'), + + (1, 0) => dest.write_char('n'), + (-1, 0) => dest.write_str("-n"), + (_, 0) => write!(dest, "{}n", self.a), + + (0, _) => write!(dest, "{}", self.b), + (1, _) => write!(dest, "n{:+}", self.b), + (-1, _) => write!(dest, "-n{:+}", self.b), + (_, _) => write!(dest, "{}n{:+}", self.a, self.b), + } + } +} + +/// The properties that comprise an :nth- pseudoclass as of Selectors 4 (e.g., +/// nth-child(An+B [of S]?)). +/// https://www.w3.org/TR/selectors-4/#nth-child-pseudo +#[derive(Clone, Eq, PartialEq, ToShmem)] +#[shmem(no_bounds)] +pub struct NthOfSelectorData( + #[shmem(field_bound)] ThinArc>, +); + +impl NthOfSelectorData { + /// Returns selector data for :nth-{,last-}{child,of-type}(An+B [of S]) + #[inline] + pub fn new(nth_data: &NthSelectorData, selectors: I) -> Self + where + I: Iterator> + ExactSizeIterator, + { + Self(ThinArc::from_header_and_iter(*nth_data, selectors)) + } + + /// Returns the An+B part of the selector + #[inline] + pub fn nth_data(&self) -> &NthSelectorData { + &self.0.header + } + + /// Returns the selector list part of the selector + #[inline] + pub fn selectors(&self) -> &[Selector] { + self.0.slice() + } +} + +/// Flag indicating where a given relative selector's match would be contained. +#[derive(Clone, Copy, Eq, PartialEq, ToShmem)] +pub enum RelativeSelectorMatchHint { + /// Within this element's subtree. + InSubtree, + /// Within this element's direct children. + InChild, + /// This element's next sibling. + InNextSibling, + /// Within this element's next sibling's subtree. + InNextSiblingSubtree, + /// Within this element's subsequent siblings. + InSibling, + /// Across this element's subsequent siblings and their subtrees. + InSiblingSubtree, +} + +impl RelativeSelectorMatchHint { + /// Create a new relative selector match hint based on its composition. + pub fn new( + relative_combinator: Combinator, + has_child_or_descendants: bool, + has_adjacent_or_next_siblings: bool, + ) -> Self { + match relative_combinator { + Combinator::Descendant => RelativeSelectorMatchHint::InSubtree, + Combinator::Child => { + if !has_child_or_descendants { + RelativeSelectorMatchHint::InChild + } else { + // Technically, for any composition that consists of child combinators only, + // the search space is depth-constrained, but it's probably not worth optimizing for. + RelativeSelectorMatchHint::InSubtree + } + }, + Combinator::NextSibling => { + if !has_child_or_descendants && !has_adjacent_or_next_siblings { + RelativeSelectorMatchHint::InNextSibling + } else if !has_child_or_descendants && has_adjacent_or_next_siblings { + RelativeSelectorMatchHint::InSibling + } else if has_child_or_descendants && !has_adjacent_or_next_siblings { + // Match won't cross multiple siblings. + RelativeSelectorMatchHint::InNextSiblingSubtree + } else { + RelativeSelectorMatchHint::InSiblingSubtree + } + }, + Combinator::LaterSibling => { + if !has_child_or_descendants { + RelativeSelectorMatchHint::InSibling + } else { + // Even if the match may not cross multiple siblings, we have to look until + // we find a match anyway. + RelativeSelectorMatchHint::InSiblingSubtree + } + }, + Combinator::Part | Combinator::PseudoElement | Combinator::SlotAssignment => { + debug_assert!(false, "Unexpected relative combinator"); + RelativeSelectorMatchHint::InSubtree + }, + } + } + + /// Is the match traversal direction towards the descendant of this element (As opposed to siblings)? + pub fn is_descendant_direction(&self) -> bool { + matches!(*self, Self::InChild | Self::InSubtree) + } + + /// Is the match traversal terminated at the next sibling? + pub fn is_next_sibling(&self) -> bool { + matches!(*self, Self::InNextSibling | Self::InNextSiblingSubtree) + } + + /// Does the match involve matching the subtree? + pub fn is_subtree(&self) -> bool { + matches!( + *self, + Self::InSubtree | Self::InSiblingSubtree | Self::InNextSiblingSubtree + ) + } +} + +/// Count of combinators in a given relative selector, not traversing selectors of pseudoclasses. +#[derive(Clone, Copy)] +pub struct RelativeSelectorCombinatorCount { + relative_combinator: Combinator, + pub child_or_descendants: usize, + pub adjacent_or_next_siblings: usize, +} + +impl RelativeSelectorCombinatorCount { + /// Create a new relative selector combinator count from a given relative selector. + pub fn new(relative_selector: &RelativeSelector) -> Self { + let mut result = RelativeSelectorCombinatorCount { + relative_combinator: relative_selector.selector.combinator_at_parse_order(1), + child_or_descendants: 0, + adjacent_or_next_siblings: 0, + }; + + for combinator in CombinatorIter::new( + relative_selector + .selector + .iter_skip_relative_selector_anchor(), + ) { + match combinator { + Combinator::Descendant | Combinator::Child => { + result.child_or_descendants += 1; + }, + Combinator::NextSibling | Combinator::LaterSibling => { + result.adjacent_or_next_siblings += 1; + }, + Combinator::Part | Combinator::PseudoElement | Combinator::SlotAssignment => { + continue + }, + }; + } + result + } + + /// Get the match hint based on the current combinator count. + pub fn get_match_hint(&self) -> RelativeSelectorMatchHint { + RelativeSelectorMatchHint::new( + self.relative_combinator, + self.child_or_descendants != 0, + self.adjacent_or_next_siblings != 0, + ) + } +} + +/// Storage for a relative selector. +#[derive(Clone, Eq, PartialEq, ToShmem)] +#[shmem(no_bounds)] +pub struct RelativeSelector { + /// Match space constraining hint. + pub match_hint: RelativeSelectorMatchHint, + /// The selector. Guaranteed to contain `RelativeSelectorAnchor` and the relative combinator in parse order. + #[shmem(field_bound)] + pub selector: Selector, +} + +bitflags! { + /// Composition of combinators in a given selector, not traversing selectors of pseudoclasses. + #[derive(Clone, Debug, Eq, PartialEq)] + struct CombinatorComposition: u8 { + const DESCENDANTS = 1 << 0; + const SIBLINGS = 1 << 1; + } +} + +impl CombinatorComposition { + fn for_relative_selector(inner_selector: &Selector) -> Self { + let mut result = CombinatorComposition::empty(); + for combinator in CombinatorIter::new(inner_selector.iter_skip_relative_selector_anchor()) { + match combinator { + Combinator::Descendant | Combinator::Child => { + result.insert(Self::DESCENDANTS); + }, + Combinator::NextSibling | Combinator::LaterSibling => { + result.insert(Self::SIBLINGS); + }, + Combinator::Part | Combinator::PseudoElement | Combinator::SlotAssignment => { + continue + }, + }; + if result.is_all() { + break; + } + } + return result; + } +} + +impl RelativeSelector { + fn from_selector_list(selector_list: SelectorList) -> Box<[Self]> { + let vec: Vec = selector_list + .slice() + .iter() + .map(|selector| { + // It's more efficient to keep track of all this during the parse time, but that seems like a lot of special + // case handling for what it's worth. + if cfg!(debug_assertions) { + let relative_selector_anchor = selector.iter_raw_parse_order_from(0).next(); + debug_assert!( + relative_selector_anchor.is_some(), + "Relative selector is empty" + ); + debug_assert!( + matches!( + relative_selector_anchor.unwrap(), + Component::RelativeSelectorAnchor + ), + "Relative selector anchor is missing" + ); + } + // Leave a hint for narrowing down the search space when we're matching. + let composition = CombinatorComposition::for_relative_selector(&selector); + let match_hint = RelativeSelectorMatchHint::new( + selector.combinator_at_parse_order(1), + composition.intersects(CombinatorComposition::DESCENDANTS), + composition.intersects(CombinatorComposition::SIBLINGS), + ); + RelativeSelector { + match_hint, + selector: selector.clone(), + } + }) + .collect(); + vec.into_boxed_slice() + } +} + +/// A CSS simple selector or combinator. We store both in the same enum for +/// optimal packing and cache performance, see [1]. +/// +/// [1] https://bugzilla.mozilla.org/show_bug.cgi?id=1357973 +#[derive(Clone, Eq, PartialEq, ToShmem)] +#[shmem(no_bounds)] +pub enum Component { + LocalName(LocalName), + + ID(#[shmem(field_bound)] Impl::Identifier), + Class(#[shmem(field_bound)] Impl::Identifier), + + AttributeInNoNamespaceExists { + #[shmem(field_bound)] + local_name: Impl::LocalName, + local_name_lower: Impl::LocalName, + }, + // Used only when local_name is already lowercase. + AttributeInNoNamespace { + local_name: Impl::LocalName, + operator: AttrSelectorOperator, + #[shmem(field_bound)] + value: Impl::AttrValue, + case_sensitivity: ParsedCaseSensitivity, + }, + // Use a Box in the less common cases with more data to keep size_of::() small. + AttributeOther(Box>), + + ExplicitUniversalType, + ExplicitAnyNamespace, + + ExplicitNoNamespace, + DefaultNamespace(#[shmem(field_bound)] Impl::NamespaceUrl), + Namespace( + #[shmem(field_bound)] Impl::NamespacePrefix, + #[shmem(field_bound)] Impl::NamespaceUrl, + ), + + /// Pseudo-classes + Negation(SelectorList), + Root, + Empty, + Scope, + ParentSelector, + Nth(NthSelectorData), + NthOf(NthOfSelectorData), + NonTSPseudoClass(#[shmem(field_bound)] Impl::NonTSPseudoClass), + /// The ::slotted() pseudo-element: + /// + /// https://drafts.csswg.org/css-scoping/#slotted-pseudo + /// + /// The selector here is a compound selector, that is, no combinators. + /// + /// NOTE(emilio): This should support a list of selectors, but as of this + /// writing no other browser does, and that allows them to put ::slotted() + /// in the rule hash, so we do that too. + /// + /// See https://github.com/w3c/csswg-drafts/issues/2158 + Slotted(Selector), + /// The `::part` pseudo-element. + /// https://drafts.csswg.org/css-shadow-parts/#part + Part(#[shmem(field_bound)] Box<[Impl::Identifier]>), + /// The `:host` pseudo-class: + /// + /// https://drafts.csswg.org/css-scoping/#host-selector + /// + /// NOTE(emilio): This should support a list of selectors, but as of this + /// writing no other browser does, and that allows them to put :host() + /// in the rule hash, so we do that too. + /// + /// See https://github.com/w3c/csswg-drafts/issues/2158 + Host(Option>), + /// The `:where` pseudo-class. + /// + /// https://drafts.csswg.org/selectors/#zero-matches + /// + /// The inner argument is conceptually a SelectorList, but we move the + /// selectors to the heap to keep Component small. + Where(SelectorList), + /// The `:is` pseudo-class. + /// + /// https://drafts.csswg.org/selectors/#matches-pseudo + /// + /// Same comment as above re. the argument. + Is(SelectorList), + /// The `:has` pseudo-class. + /// + /// https://drafts.csswg.org/selectors/#has-pseudo + /// + /// Same comment as above re. the argument. + Has(Box<[RelativeSelector]>), + /// An invalid selector inside :is() / :where(). + Invalid(Arc), + /// An implementation-dependent pseudo-element selector. + PseudoElement(#[shmem(field_bound)] Impl::PseudoElement), + + Combinator(Combinator), + + /// Used only for relative selectors, which starts with a combinator + /// (With an implied descendant combinator if not specified). + /// + /// https://drafts.csswg.org/selectors-4/#typedef-relative-selector + RelativeSelectorAnchor, +} + +impl Component { + /// Returns true if this is a combinator. + #[inline] + pub fn is_combinator(&self) -> bool { + matches!(*self, Component::Combinator(_)) + } + + /// Returns true if this is a :host() selector. + #[inline] + pub fn is_host(&self) -> bool { + matches!(*self, Component::Host(..)) + } + + /// Returns true if this is a :host() selector. + #[inline] + pub fn matches_featureless_host(&self) -> bool { + match *self { + Component::Host(..) => true, + Component::Where(ref l) | Component::Is(ref l) => { + // TODO(emilio): For now we use .all() rather than .any(), because not doing so + // brings up a fair amount of extra complexity (we can't make the decision on + // whether to walk out statically). + l.slice() + .iter() + .all(|i| i.is_featureless_host_selector_or_pseudo_element()) + }, + _ => false, + } + } + + /// Returns the value as a combinator if applicable, None otherwise. + pub fn as_combinator(&self) -> Option { + match *self { + Component::Combinator(c) => Some(c), + _ => None, + } + } + + /// Whether this component is valid after a pseudo-element. Only intended + /// for sanity-checking. + pub fn maybe_allowed_after_pseudo_element(&self) -> bool { + match *self { + Component::NonTSPseudoClass(..) => true, + Component::Negation(ref selectors) | + Component::Is(ref selectors) | + Component::Where(ref selectors) => selectors.slice().iter().all(|selector| { + selector + .iter_raw_match_order() + .all(|c| c.maybe_allowed_after_pseudo_element()) + }), + _ => false, + } + } + + /// Whether a given selector should match for stateless pseudo-elements. + /// + /// This is a bit subtle: Only selectors that return true in + /// `maybe_allowed_after_pseudo_element` should end up here, and + /// `NonTSPseudoClass` never matches (as it is a stateless pseudo after + /// all). + fn matches_for_stateless_pseudo_element(&self) -> bool { + debug_assert!( + self.maybe_allowed_after_pseudo_element(), + "Someone messed up pseudo-element parsing: {:?}", + *self + ); + match *self { + Component::Negation(ref selectors) => !selectors.slice().iter().all(|selector| { + selector + .iter_raw_match_order() + .all(|c| c.matches_for_stateless_pseudo_element()) + }), + Component::Is(ref selectors) | Component::Where(ref selectors) => { + selectors.slice().iter().any(|selector| { + selector + .iter_raw_match_order() + .all(|c| c.matches_for_stateless_pseudo_element()) + }) + }, + _ => false, + } + } + + pub fn visit(&self, visitor: &mut V) -> bool + where + V: SelectorVisitor, + { + use self::Component::*; + if !visitor.visit_simple_selector(self) { + return false; + } + + match *self { + Slotted(ref selector) => { + if !selector.visit(visitor) { + return false; + } + }, + Host(Some(ref selector)) => { + if !selector.visit(visitor) { + return false; + } + }, + AttributeInNoNamespaceExists { + ref local_name, + ref local_name_lower, + } => { + if !visitor.visit_attribute_selector( + &NamespaceConstraint::Specific(&namespace_empty_string::()), + local_name, + local_name_lower, + ) { + return false; + } + }, + AttributeInNoNamespace { ref local_name, .. } => { + if !visitor.visit_attribute_selector( + &NamespaceConstraint::Specific(&namespace_empty_string::()), + local_name, + local_name, + ) { + return false; + } + }, + AttributeOther(ref attr_selector) => { + let empty_string; + let namespace = match attr_selector.namespace() { + Some(ns) => ns, + None => { + empty_string = crate::parser::namespace_empty_string::(); + NamespaceConstraint::Specific(&empty_string) + }, + }; + if !visitor.visit_attribute_selector( + &namespace, + &attr_selector.local_name, + &attr_selector.local_name_lower, + ) { + return false; + } + }, + + NonTSPseudoClass(ref pseudo_class) => { + if !pseudo_class.visit(visitor) { + return false; + } + }, + Negation(ref list) | Is(ref list) | Where(ref list) => { + let list_kind = SelectorListKind::from_component(self); + debug_assert!(!list_kind.is_empty()); + if !visitor.visit_selector_list(list_kind, list.slice()) { + return false; + } + }, + NthOf(ref nth_of_data) => { + if !visitor.visit_selector_list(SelectorListKind::NTH_OF, nth_of_data.selectors()) { + return false; + } + }, + Has(ref list) => { + if !visitor.visit_relative_selector_list(list) { + return false; + } + }, + _ => {}, + } + + true + } + + // Returns true if this has any selector that requires an index calculation. e.g. + // :nth-child, :first-child, etc. For nested selectors, return true only if the + // indexed selector is in its subject compound. + pub fn has_indexed_selector_in_subject(&self) -> bool { + match *self { + Component::NthOf(..) | Component::Nth(..) => return true, + Component::Is(ref selectors) | + Component::Where(ref selectors) | + Component::Negation(ref selectors) => { + // Check the subject compound. + for selector in selectors.slice() { + let mut iter = selector.iter(); + while let Some(c) = iter.next() { + if c.has_indexed_selector_in_subject() { + return true; + } + } + } + }, + _ => (), + }; + false + } +} + +#[derive(Clone, Eq, PartialEq, ToShmem)] +#[shmem(no_bounds)] +pub struct LocalName { + #[shmem(field_bound)] + pub name: Impl::LocalName, + pub lower_name: Impl::LocalName, +} + +impl Debug for Selector { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("Selector(")?; + self.to_css(f)?; + write!( + f, + ", specificity = {:#x}, flags = {:?})", + self.specificity(), + self.flags() + ) + } +} + +impl Debug for Component { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.to_css(f) + } +} +impl Debug for AttrSelectorWithOptionalNamespace { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.to_css(f) + } +} +impl Debug for LocalName { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.to_css(f) + } +} + +fn serialize_selector_list<'a, Impl, I, W>(iter: I, dest: &mut W) -> fmt::Result +where + Impl: SelectorImpl, + I: Iterator>, + W: fmt::Write, +{ + let mut first = true; + for selector in iter { + if !first { + dest.write_str(", ")?; + } + first = false; + selector.to_css(dest)?; + } + Ok(()) +} + +impl ToCss for SelectorList { + fn to_css(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + serialize_selector_list(self.slice().iter(), dest) + } +} + +impl ToCss for Selector { + fn to_css(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + // Compound selectors invert the order of their contents, so we need to + // undo that during serialization. + // + // This two-iterator strategy involves walking over the selector twice. + // We could do something more clever, but selector serialization probably + // isn't hot enough to justify it, and the stringification likely + // dominates anyway. + // + // NB: A parse-order iterator is a Rev<>, which doesn't expose as_slice(), + // which we need for |split|. So we split by combinators on a match-order + // sequence and then reverse. + + let mut combinators = self + .iter_raw_match_order() + .rev() + .filter_map(|x| x.as_combinator()); + let compound_selectors = self + .iter_raw_match_order() + .as_slice() + .split(|x| x.is_combinator()) + .rev(); + + let mut combinators_exhausted = false; + for compound in compound_selectors { + debug_assert!(!combinators_exhausted); + + // https://drafts.csswg.org/cssom/#serializing-selectors + if compound.is_empty() { + continue; + } + if let Component::RelativeSelectorAnchor = compound.first().unwrap() { + debug_assert!( + compound.len() == 1, + "RelativeLeft should only be a simple selector" + ); + combinators.next().unwrap().to_css_relative(dest)?; + continue; + } + + // 1. If there is only one simple selector in the compound selectors + // which is a universal selector, append the result of + // serializing the universal selector to s. + // + // Check if `!compound.empty()` first--this can happen if we have + // something like `... > ::before`, because we store `>` and `::` + // both as combinators internally. + // + // If we are in this case, after we have serialized the universal + // selector, we skip Step 2 and continue with the algorithm. + let (can_elide_namespace, first_non_namespace) = match compound[0] { + Component::ExplicitAnyNamespace | + Component::ExplicitNoNamespace | + Component::Namespace(..) => (false, 1), + Component::DefaultNamespace(..) => (true, 1), + _ => (true, 0), + }; + let mut perform_step_2 = true; + let next_combinator = combinators.next(); + if first_non_namespace == compound.len() - 1 { + match (next_combinator, &compound[first_non_namespace]) { + // We have to be careful here, because if there is a + // pseudo element "combinator" there isn't really just + // the one simple selector. Technically this compound + // selector contains the pseudo element selector as well + // -- Combinator::PseudoElement, just like + // Combinator::SlotAssignment, don't exist in the + // spec. + (Some(Combinator::PseudoElement), _) | + (Some(Combinator::SlotAssignment), _) => (), + (_, &Component::ExplicitUniversalType) => { + // Iterate over everything so we serialize the namespace + // too. + for simple in compound.iter() { + simple.to_css(dest)?; + } + // Skip step 2, which is an "otherwise". + perform_step_2 = false; + }, + _ => (), + } + } + + // 2. Otherwise, for each simple selector in the compound selectors + // that is not a universal selector of which the namespace prefix + // maps to a namespace that is not the default namespace + // serialize the simple selector and append the result to s. + // + // See https://github.com/w3c/csswg-drafts/issues/1606, which is + // proposing to change this to match up with the behavior asserted + // in cssom/serialize-namespaced-type-selectors.html, which the + // following code tries to match. + if perform_step_2 { + for simple in compound.iter() { + if let Component::ExplicitUniversalType = *simple { + // Can't have a namespace followed by a pseudo-element + // selector followed by a universal selector in the same + // compound selector, so we don't have to worry about the + // real namespace being in a different `compound`. + if can_elide_namespace { + continue; + } + } + simple.to_css(dest)?; + } + } + + // 3. If this is not the last part of the chain of the selector + // append a single SPACE (U+0020), followed by the combinator + // ">", "+", "~", ">>", "||", as appropriate, followed by another + // single SPACE (U+0020) if the combinator was not whitespace, to + // s. + match next_combinator { + Some(c) => c.to_css(dest)?, + None => combinators_exhausted = true, + }; + + // 4. If this is the last part of the chain of the selector and + // there is a pseudo-element, append "::" followed by the name of + // the pseudo-element, to s. + // + // (we handle this above) + } + + Ok(()) + } +} + +impl Combinator { + fn to_css_internal(&self, dest: &mut W, prefix_space: bool) -> fmt::Result + where + W: fmt::Write, + { + if matches!( + *self, + Combinator::PseudoElement | Combinator::Part | Combinator::SlotAssignment + ) { + return Ok(()); + } + if prefix_space { + dest.write_char(' ')?; + } + match *self { + Combinator::Child => dest.write_str("> "), + Combinator::Descendant => Ok(()), + Combinator::NextSibling => dest.write_str("+ "), + Combinator::LaterSibling => dest.write_str("~ "), + Combinator::PseudoElement | Combinator::Part | Combinator::SlotAssignment => unsafe { + debug_unreachable!("Already handled") + }, + } + } + + fn to_css_relative(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + self.to_css_internal(dest, false) + } +} + +impl ToCss for Combinator { + fn to_css(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + self.to_css_internal(dest, true) + } +} + +impl ToCss for Component { + fn to_css(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + use self::Component::*; + + match *self { + Combinator(ref c) => c.to_css(dest), + Slotted(ref selector) => { + dest.write_str("::slotted(")?; + selector.to_css(dest)?; + dest.write_char(')') + }, + Part(ref part_names) => { + dest.write_str("::part(")?; + for (i, name) in part_names.iter().enumerate() { + if i != 0 { + dest.write_char(' ')?; + } + name.to_css(dest)?; + } + dest.write_char(')') + }, + PseudoElement(ref p) => p.to_css(dest), + ID(ref s) => { + dest.write_char('#')?; + s.to_css(dest) + }, + Class(ref s) => { + dest.write_char('.')?; + s.to_css(dest) + }, + LocalName(ref s) => s.to_css(dest), + ExplicitUniversalType => dest.write_char('*'), + + DefaultNamespace(_) => Ok(()), + ExplicitNoNamespace => dest.write_char('|'), + ExplicitAnyNamespace => dest.write_str("*|"), + Namespace(ref prefix, _) => { + prefix.to_css(dest)?; + dest.write_char('|') + }, + + AttributeInNoNamespaceExists { ref local_name, .. } => { + dest.write_char('[')?; + local_name.to_css(dest)?; + dest.write_char(']') + }, + AttributeInNoNamespace { + ref local_name, + operator, + ref value, + case_sensitivity, + .. + } => { + dest.write_char('[')?; + local_name.to_css(dest)?; + operator.to_css(dest)?; + value.to_css(dest)?; + match case_sensitivity { + ParsedCaseSensitivity::CaseSensitive | + ParsedCaseSensitivity::AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument => {}, + ParsedCaseSensitivity::AsciiCaseInsensitive => dest.write_str(" i")?, + ParsedCaseSensitivity::ExplicitCaseSensitive => dest.write_str(" s")?, + } + dest.write_char(']') + }, + AttributeOther(ref attr_selector) => attr_selector.to_css(dest), + + // Pseudo-classes + Root => dest.write_str(":root"), + Empty => dest.write_str(":empty"), + Scope => dest.write_str(":scope"), + ParentSelector => dest.write_char('&'), + Host(ref selector) => { + dest.write_str(":host")?; + if let Some(ref selector) = *selector { + dest.write_char('(')?; + selector.to_css(dest)?; + dest.write_char(')')?; + } + Ok(()) + }, + Nth(ref nth_data) => { + nth_data.write_start(dest)?; + if nth_data.is_function { + nth_data.write_affine(dest)?; + dest.write_char(')')?; + } + Ok(()) + }, + NthOf(ref nth_of_data) => { + let nth_data = nth_of_data.nth_data(); + nth_data.write_start(dest)?; + debug_assert!( + nth_data.is_function, + "A selector must be a function to hold An+B notation" + ); + nth_data.write_affine(dest)?; + debug_assert!( + matches!(nth_data.ty, NthType::Child | NthType::LastChild), + "Only :nth-child or :nth-last-child can be of a selector list" + ); + debug_assert!( + !nth_of_data.selectors().is_empty(), + "The selector list should not be empty" + ); + dest.write_str(" of ")?; + serialize_selector_list(nth_of_data.selectors().iter(), dest)?; + dest.write_char(')') + }, + Is(ref list) | Where(ref list) | Negation(ref list) => { + match *self { + Where(..) => dest.write_str(":where(")?, + Is(..) => dest.write_str(":is(")?, + Negation(..) => dest.write_str(":not(")?, + _ => unreachable!(), + } + serialize_selector_list(list.slice().iter(), dest)?; + dest.write_str(")") + }, + Has(ref list) => { + dest.write_str(":has(")?; + let mut first = true; + for RelativeSelector { ref selector, .. } in list.iter() { + if !first { + dest.write_str(", ")?; + } + first = false; + selector.to_css(dest)?; + } + dest.write_str(")") + }, + NonTSPseudoClass(ref pseudo) => pseudo.to_css(dest), + Invalid(ref css) => dest.write_str(css), + RelativeSelectorAnchor => Ok(()), + } + } +} + +impl ToCss for AttrSelectorWithOptionalNamespace { + fn to_css(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + dest.write_char('[')?; + match self.namespace { + Some(NamespaceConstraint::Specific((ref prefix, _))) => { + prefix.to_css(dest)?; + dest.write_char('|')? + }, + Some(NamespaceConstraint::Any) => dest.write_str("*|")?, + None => {}, + } + self.local_name.to_css(dest)?; + match self.operation { + ParsedAttrSelectorOperation::Exists => {}, + ParsedAttrSelectorOperation::WithValue { + operator, + case_sensitivity, + ref value, + } => { + operator.to_css(dest)?; + value.to_css(dest)?; + match case_sensitivity { + ParsedCaseSensitivity::CaseSensitive | + ParsedCaseSensitivity::AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument => {}, + ParsedCaseSensitivity::AsciiCaseInsensitive => dest.write_str(" i")?, + ParsedCaseSensitivity::ExplicitCaseSensitive => dest.write_str(" s")?, + } + }, + } + dest.write_char(']') + } +} + +impl ToCss for LocalName { + fn to_css(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + self.name.to_css(dest) + } +} + +/// Build up a Selector. +/// selector : simple_selector_sequence [ combinator simple_selector_sequence ]* ; +/// +/// `Err` means invalid selector. +fn parse_selector<'i, 't, P, Impl>( + parser: &P, + input: &mut CssParser<'i, 't>, + mut state: SelectorParsingState, + parse_relative: ParseRelative, +) -> Result, ParseError<'i, P::Error>> +where + P: Parser<'i, Impl = Impl>, + Impl: SelectorImpl, +{ + let mut builder = SelectorBuilder::default(); + + // Helps rewind less, but also simplifies dealing with relative combinators below. + input.skip_whitespace(); + + if parse_relative != ParseRelative::No { + let combinator = try_parse_combinator::(input); + match parse_relative { + ParseRelative::ForHas => { + builder.push_simple_selector(Component::RelativeSelectorAnchor); + // Do we see a combinator? If so, push that. Otherwise, push a descendant + // combinator. + builder.push_combinator(combinator.unwrap_or(Combinator::Descendant)); + }, + ParseRelative::ForNesting => { + if let Ok(combinator) = combinator { + builder.push_simple_selector(Component::ParentSelector); + builder.push_combinator(combinator); + } + }, + ParseRelative::No => unreachable!(), + } + } + 'outer_loop: loop { + // Parse a sequence of simple selectors. + let empty = parse_compound_selector(parser, &mut state, input, &mut builder)?; + if empty { + return Err(input.new_custom_error(if builder.has_combinators() { + SelectorParseErrorKind::DanglingCombinator + } else { + SelectorParseErrorKind::EmptySelector + })); + } + + if state.intersects(SelectorParsingState::AFTER_PSEUDO) { + debug_assert!(state.intersects( + SelectorParsingState::AFTER_PSEUDO_ELEMENT | + SelectorParsingState::AFTER_SLOTTED | + SelectorParsingState::AFTER_PART + )); + break; + } + + let combinator = if let Ok(c) = try_parse_combinator::(input) { + c + } else { + break 'outer_loop; + }; + + if !state.allows_combinators() { + return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState)); + } + + builder.push_combinator(combinator); + } + return Ok(Selector(builder.build())); +} + +fn try_parse_combinator<'i, 't, P, Impl>(input: &mut CssParser<'i, 't>) -> Result { + let mut any_whitespace = false; + loop { + let before_this_token = input.state(); + match input.next_including_whitespace() { + Err(_e) => return Err(()), + Ok(&Token::WhiteSpace(_)) => any_whitespace = true, + Ok(&Token::Delim('>')) => { + return Ok(Combinator::Child); + }, + Ok(&Token::Delim('+')) => { + return Ok(Combinator::NextSibling); + }, + Ok(&Token::Delim('~')) => { + return Ok(Combinator::LaterSibling); + }, + Ok(_) => { + input.reset(&before_this_token); + if any_whitespace { + return Ok(Combinator::Descendant); + } else { + return Err(()); + } + }, + } + } +} + +/// * `Err(())`: Invalid selector, abort +/// * `Ok(false)`: Not a type selector, could be something else. `input` was not consumed. +/// * `Ok(true)`: Length 0 (`*|*`), 1 (`*|E` or `ns|*`) or 2 (`|E` or `ns|E`) +fn parse_type_selector<'i, 't, P, Impl, S>( + parser: &P, + input: &mut CssParser<'i, 't>, + state: SelectorParsingState, + sink: &mut S, +) -> Result> +where + P: Parser<'i, Impl = Impl>, + Impl: SelectorImpl, + S: Push>, +{ + match parse_qualified_name(parser, input, /* in_attr_selector = */ false) { + Err(ParseError { + kind: ParseErrorKind::Basic(BasicParseErrorKind::EndOfInput), + .. + }) | + Ok(OptionalQName::None(_)) => Ok(false), + Ok(OptionalQName::Some(namespace, local_name)) => { + if state.intersects(SelectorParsingState::AFTER_PSEUDO) { + return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState)); + } + match namespace { + QNamePrefix::ImplicitAnyNamespace => {}, + QNamePrefix::ImplicitDefaultNamespace(url) => { + sink.push(Component::DefaultNamespace(url)) + }, + QNamePrefix::ExplicitNamespace(prefix, url) => { + sink.push(match parser.default_namespace() { + Some(ref default_url) if url == *default_url => { + Component::DefaultNamespace(url) + }, + _ => Component::Namespace(prefix, url), + }) + }, + QNamePrefix::ExplicitNoNamespace => sink.push(Component::ExplicitNoNamespace), + QNamePrefix::ExplicitAnyNamespace => { + match parser.default_namespace() { + // Element type selectors that have no namespace + // component (no namespace separator) represent elements + // without regard to the element's namespace (equivalent + // to "*|") unless a default namespace has been declared + // for namespaced selectors (e.g. in CSS, in the style + // sheet). If a default namespace has been declared, + // such selectors will represent only elements in the + // default namespace. + // -- Selectors § 6.1.1 + // So we'll have this act the same as the + // QNamePrefix::ImplicitAnyNamespace case. + None => {}, + Some(_) => sink.push(Component::ExplicitAnyNamespace), + } + }, + QNamePrefix::ImplicitNoNamespace => { + unreachable!() // Not returned with in_attr_selector = false + }, + } + match local_name { + Some(name) => sink.push(Component::LocalName(LocalName { + lower_name: to_ascii_lowercase(&name).as_ref().into(), + name: name.as_ref().into(), + })), + None => sink.push(Component::ExplicitUniversalType), + } + Ok(true) + }, + Err(e) => Err(e), + } +} + +#[derive(Debug)] +enum SimpleSelectorParseResult { + SimpleSelector(Component), + PseudoElement(Impl::PseudoElement), + SlottedPseudo(Selector), + PartPseudo(Box<[Impl::Identifier]>), +} + +#[derive(Debug)] +enum QNamePrefix { + ImplicitNoNamespace, // `foo` in attr selectors + ImplicitAnyNamespace, // `foo` in type selectors, without a default ns + ImplicitDefaultNamespace(Impl::NamespaceUrl), // `foo` in type selectors, with a default ns + ExplicitNoNamespace, // `|foo` + ExplicitAnyNamespace, // `*|foo` + ExplicitNamespace(Impl::NamespacePrefix, Impl::NamespaceUrl), // `prefix|foo` +} + +enum OptionalQName<'i, Impl: SelectorImpl> { + Some(QNamePrefix, Option>), + None(Token<'i>), +} + +/// * `Err(())`: Invalid selector, abort +/// * `Ok(None(token))`: Not a simple selector, could be something else. `input` was not consumed, +/// but the token is still returned. +/// * `Ok(Some(namespace, local_name))`: `None` for the local name means a `*` universal selector +fn parse_qualified_name<'i, 't, P, Impl>( + parser: &P, + input: &mut CssParser<'i, 't>, + in_attr_selector: bool, +) -> Result, ParseError<'i, P::Error>> +where + P: Parser<'i, Impl = Impl>, + Impl: SelectorImpl, +{ + let default_namespace = |local_name| { + let namespace = match parser.default_namespace() { + Some(url) => QNamePrefix::ImplicitDefaultNamespace(url), + None => QNamePrefix::ImplicitAnyNamespace, + }; + Ok(OptionalQName::Some(namespace, local_name)) + }; + + let explicit_namespace = |input: &mut CssParser<'i, 't>, namespace| { + let location = input.current_source_location(); + match input.next_including_whitespace() { + Ok(&Token::Delim('*')) if !in_attr_selector => Ok(OptionalQName::Some(namespace, None)), + Ok(&Token::Ident(ref local_name)) => { + Ok(OptionalQName::Some(namespace, Some(local_name.clone()))) + }, + Ok(t) if in_attr_selector => { + let e = SelectorParseErrorKind::InvalidQualNameInAttr(t.clone()); + Err(location.new_custom_error(e)) + }, + Ok(t) => Err(location.new_custom_error( + SelectorParseErrorKind::ExplicitNamespaceUnexpectedToken(t.clone()), + )), + Err(e) => Err(e.into()), + } + }; + + let start = input.state(); + match input.next_including_whitespace() { + Ok(Token::Ident(value)) => { + let value = value.clone(); + let after_ident = input.state(); + match input.next_including_whitespace() { + Ok(&Token::Delim('|')) => { + let prefix = value.as_ref().into(); + let result = parser.namespace_for_prefix(&prefix); + let url = result.ok_or( + after_ident + .source_location() + .new_custom_error(SelectorParseErrorKind::ExpectedNamespace(value)), + )?; + explicit_namespace(input, QNamePrefix::ExplicitNamespace(prefix, url)) + }, + _ => { + input.reset(&after_ident); + if in_attr_selector { + Ok(OptionalQName::Some( + QNamePrefix::ImplicitNoNamespace, + Some(value), + )) + } else { + default_namespace(Some(value)) + } + }, + } + }, + Ok(Token::Delim('*')) => { + let after_star = input.state(); + match input.next_including_whitespace() { + Ok(&Token::Delim('|')) => { + explicit_namespace(input, QNamePrefix::ExplicitAnyNamespace) + }, + _ if !in_attr_selector => { + input.reset(&after_star); + default_namespace(None) + }, + result => { + let t = result?; + Err(after_star + .source_location() + .new_custom_error(SelectorParseErrorKind::ExpectedBarInAttr(t.clone()))) + }, + } + }, + Ok(Token::Delim('|')) => explicit_namespace(input, QNamePrefix::ExplicitNoNamespace), + Ok(t) => { + let t = t.clone(); + input.reset(&start); + Ok(OptionalQName::None(t)) + }, + Err(e) => { + input.reset(&start); + Err(e.into()) + }, + } +} + +fn parse_attribute_selector<'i, 't, P, Impl>( + parser: &P, + input: &mut CssParser<'i, 't>, +) -> Result, ParseError<'i, P::Error>> +where + P: Parser<'i, Impl = Impl>, + Impl: SelectorImpl, +{ + let namespace; + let local_name; + + input.skip_whitespace(); + + match parse_qualified_name(parser, input, /* in_attr_selector = */ true)? { + OptionalQName::None(t) => { + return Err(input.new_custom_error( + SelectorParseErrorKind::NoQualifiedNameInAttributeSelector(t), + )); + }, + OptionalQName::Some(_, None) => unreachable!(), + OptionalQName::Some(ns, Some(ln)) => { + local_name = ln; + namespace = match ns { + QNamePrefix::ImplicitNoNamespace | QNamePrefix::ExplicitNoNamespace => None, + QNamePrefix::ExplicitNamespace(prefix, url) => { + Some(NamespaceConstraint::Specific((prefix, url))) + }, + QNamePrefix::ExplicitAnyNamespace => Some(NamespaceConstraint::Any), + QNamePrefix::ImplicitAnyNamespace | QNamePrefix::ImplicitDefaultNamespace(_) => { + unreachable!() // Not returned with in_attr_selector = true + }, + } + }, + } + + let location = input.current_source_location(); + let operator = match input.next() { + // [foo] + Err(_) => { + let local_name_lower = to_ascii_lowercase(&local_name).as_ref().into(); + let local_name = local_name.as_ref().into(); + if let Some(namespace) = namespace { + return Ok(Component::AttributeOther(Box::new( + AttrSelectorWithOptionalNamespace { + namespace: Some(namespace), + local_name, + local_name_lower, + operation: ParsedAttrSelectorOperation::Exists, + }, + ))); + } else { + return Ok(Component::AttributeInNoNamespaceExists { + local_name, + local_name_lower, + }); + } + }, + + // [foo=bar] + Ok(&Token::Delim('=')) => AttrSelectorOperator::Equal, + // [foo~=bar] + Ok(&Token::IncludeMatch) => AttrSelectorOperator::Includes, + // [foo|=bar] + Ok(&Token::DashMatch) => AttrSelectorOperator::DashMatch, + // [foo^=bar] + Ok(&Token::PrefixMatch) => AttrSelectorOperator::Prefix, + // [foo*=bar] + Ok(&Token::SubstringMatch) => AttrSelectorOperator::Substring, + // [foo$=bar] + Ok(&Token::SuffixMatch) => AttrSelectorOperator::Suffix, + Ok(t) => { + return Err(location.new_custom_error( + SelectorParseErrorKind::UnexpectedTokenInAttributeSelector(t.clone()), + )); + }, + }; + + let value = match input.expect_ident_or_string() { + Ok(t) => t.clone(), + Err(BasicParseError { + kind: BasicParseErrorKind::UnexpectedToken(t), + location, + }) => return Err(location.new_custom_error(SelectorParseErrorKind::BadValueInAttr(t))), + Err(e) => return Err(e.into()), + }; + + let attribute_flags = parse_attribute_flags(input)?; + let value = value.as_ref().into(); + let local_name_lower; + let local_name_is_ascii_lowercase; + let case_sensitivity; + { + let local_name_lower_cow = to_ascii_lowercase(&local_name); + case_sensitivity = + attribute_flags.to_case_sensitivity(local_name_lower_cow.as_ref(), namespace.is_some()); + local_name_lower = local_name_lower_cow.as_ref().into(); + local_name_is_ascii_lowercase = matches!(local_name_lower_cow, Cow::Borrowed(..)); + } + let local_name = local_name.as_ref().into(); + if namespace.is_some() || !local_name_is_ascii_lowercase { + Ok(Component::AttributeOther(Box::new( + AttrSelectorWithOptionalNamespace { + namespace, + local_name, + local_name_lower, + operation: ParsedAttrSelectorOperation::WithValue { + operator, + case_sensitivity, + value, + }, + }, + ))) + } else { + Ok(Component::AttributeInNoNamespace { + local_name, + operator, + value, + case_sensitivity, + }) + } +} + +/// An attribute selector can have 's' or 'i' as flags, or no flags at all. +enum AttributeFlags { + // Matching should be case-sensitive ('s' flag). + CaseSensitive, + // Matching should be case-insensitive ('i' flag). + AsciiCaseInsensitive, + // No flags. Matching behavior depends on the name of the attribute. + CaseSensitivityDependsOnName, +} + +impl AttributeFlags { + fn to_case_sensitivity(self, local_name: &str, have_namespace: bool) -> ParsedCaseSensitivity { + match self { + AttributeFlags::CaseSensitive => ParsedCaseSensitivity::ExplicitCaseSensitive, + AttributeFlags::AsciiCaseInsensitive => ParsedCaseSensitivity::AsciiCaseInsensitive, + AttributeFlags::CaseSensitivityDependsOnName => { + if !have_namespace && + include!(concat!( + env!("OUT_DIR"), + "/ascii_case_insensitive_html_attributes.rs" + )) + .contains(local_name) + { + ParsedCaseSensitivity::AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument + } else { + ParsedCaseSensitivity::CaseSensitive + } + }, + } + } +} + +fn parse_attribute_flags<'i, 't>( + input: &mut CssParser<'i, 't>, +) -> Result> { + let location = input.current_source_location(); + let token = match input.next() { + Ok(t) => t, + Err(..) => { + // Selectors spec says language-defined; HTML says it depends on the + // exact attribute name. + return Ok(AttributeFlags::CaseSensitivityDependsOnName); + }, + }; + + let ident = match *token { + Token::Ident(ref i) => i, + ref other => return Err(location.new_basic_unexpected_token_error(other.clone())), + }; + + Ok(match_ignore_ascii_case! { + ident, + "i" => AttributeFlags::AsciiCaseInsensitive, + "s" => AttributeFlags::CaseSensitive, + _ => return Err(location.new_basic_unexpected_token_error(token.clone())), + }) +} + +/// Level 3: Parse **one** simple_selector. (Though we might insert a second +/// implied "|*" type selector.) +fn parse_negation<'i, 't, P, Impl>( + parser: &P, + input: &mut CssParser<'i, 't>, + state: SelectorParsingState, +) -> Result, ParseError<'i, P::Error>> +where + P: Parser<'i, Impl = Impl>, + Impl: SelectorImpl, +{ + let list = SelectorList::parse_with_state( + parser, + input, + state | + SelectorParsingState::SKIP_DEFAULT_NAMESPACE | + SelectorParsingState::DISALLOW_PSEUDOS, + ForgivingParsing::No, + ParseRelative::No, + )?; + + Ok(Component::Negation(list)) +} + +/// simple_selector_sequence +/// : [ type_selector | universal ] [ HASH | class | attrib | pseudo | negation ]* +/// | [ HASH | class | attrib | pseudo | negation ]+ +/// +/// `Err(())` means invalid selector. +/// `Ok(true)` is an empty selector +fn parse_compound_selector<'i, 't, P, Impl>( + parser: &P, + state: &mut SelectorParsingState, + input: &mut CssParser<'i, 't>, + builder: &mut SelectorBuilder, +) -> Result> +where + P: Parser<'i, Impl = Impl>, + Impl: SelectorImpl, +{ + input.skip_whitespace(); + + let mut empty = true; + if parse_type_selector(parser, input, *state, builder)? { + empty = false; + } + + loop { + let result = match parse_one_simple_selector(parser, input, *state)? { + None => break, + Some(result) => result, + }; + + if empty { + if let Some(url) = parser.default_namespace() { + // If there was no explicit type selector, but there is a + // default namespace, there is an implicit "|*" type + // selector. Except for :host() or :not() / :is() / :where(), + // where we ignore it. + // + // https://drafts.csswg.org/css-scoping/#host-element-in-tree: + // + // When considered within its own shadow trees, the shadow + // host is featureless. Only the :host, :host(), and + // :host-context() pseudo-classes are allowed to match it. + // + // https://drafts.csswg.org/selectors-4/#featureless: + // + // A featureless element does not match any selector at all, + // except those it is explicitly defined to match. If a + // given selector is allowed to match a featureless element, + // it must do so while ignoring the default namespace. + // + // https://drafts.csswg.org/selectors-4/#matches + // + // Default namespace declarations do not affect the compound + // selector representing the subject of any selector within + // a :is() pseudo-class, unless that compound selector + // contains an explicit universal selector or type selector. + // + // (Similar quotes for :where() / :not()) + // + let ignore_default_ns = state + .intersects(SelectorParsingState::SKIP_DEFAULT_NAMESPACE) || + matches!( + result, + SimpleSelectorParseResult::SimpleSelector(Component::Host(..)) + ); + if !ignore_default_ns { + builder.push_simple_selector(Component::DefaultNamespace(url)); + } + } + } + + empty = false; + + match result { + SimpleSelectorParseResult::SimpleSelector(s) => { + builder.push_simple_selector(s); + }, + SimpleSelectorParseResult::PartPseudo(part_names) => { + state.insert(SelectorParsingState::AFTER_PART); + builder.push_combinator(Combinator::Part); + builder.push_simple_selector(Component::Part(part_names)); + }, + SimpleSelectorParseResult::SlottedPseudo(selector) => { + state.insert(SelectorParsingState::AFTER_SLOTTED); + builder.push_combinator(Combinator::SlotAssignment); + builder.push_simple_selector(Component::Slotted(selector)); + }, + SimpleSelectorParseResult::PseudoElement(p) => { + state.insert(SelectorParsingState::AFTER_PSEUDO_ELEMENT); + if !p.accepts_state_pseudo_classes() { + state.insert(SelectorParsingState::AFTER_NON_STATEFUL_PSEUDO_ELEMENT); + } + builder.push_combinator(Combinator::PseudoElement); + builder.push_simple_selector(Component::PseudoElement(p)); + }, + } + } + Ok(empty) +} + +fn parse_is_where<'i, 't, P, Impl>( + parser: &P, + input: &mut CssParser<'i, 't>, + state: SelectorParsingState, + component: impl FnOnce(SelectorList) -> Component, +) -> Result, ParseError<'i, P::Error>> +where + P: Parser<'i, Impl = Impl>, + Impl: SelectorImpl, +{ + debug_assert!(parser.parse_is_and_where()); + // https://drafts.csswg.org/selectors/#matches-pseudo: + // + // Pseudo-elements cannot be represented by the matches-any + // pseudo-class; they are not valid within :is(). + // + let inner = SelectorList::parse_with_state( + parser, + input, + state | + SelectorParsingState::SKIP_DEFAULT_NAMESPACE | + SelectorParsingState::DISALLOW_PSEUDOS, + ForgivingParsing::Yes, + ParseRelative::No, + )?; + Ok(component(inner)) +} + +fn parse_has<'i, 't, P, Impl>( + parser: &P, + input: &mut CssParser<'i, 't>, + state: SelectorParsingState, +) -> Result, ParseError<'i, P::Error>> +where + P: Parser<'i, Impl = Impl>, + Impl: SelectorImpl, +{ + debug_assert!(parser.parse_has()); + if state.intersects(SelectorParsingState::DISALLOW_RELATIVE_SELECTOR) { + return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState)); + } + // Nested `:has()` is disallowed, mark it as such. + // Note: The spec defines ":has-allowed pseudo-element," but there's no + // pseudo-element defined as such at the moment. + // https://w3c.github.io/csswg-drafts/selectors-4/#has-allowed-pseudo-element + let inner = SelectorList::parse_with_state( + parser, + input, + state | + SelectorParsingState::SKIP_DEFAULT_NAMESPACE | + SelectorParsingState::DISALLOW_PSEUDOS | + SelectorParsingState::DISALLOW_RELATIVE_SELECTOR, + ForgivingParsing::No, + ParseRelative::ForHas, + )?; + Ok(Component::Has(RelativeSelector::from_selector_list(inner))) +} + +fn parse_functional_pseudo_class<'i, 't, P, Impl>( + parser: &P, + input: &mut CssParser<'i, 't>, + name: CowRcStr<'i>, + state: SelectorParsingState, +) -> Result, ParseError<'i, P::Error>> +where + P: Parser<'i, Impl = Impl>, + Impl: SelectorImpl, +{ + match_ignore_ascii_case! { &name, + "nth-child" => return parse_nth_pseudo_class(parser, input, state, NthType::Child), + "nth-of-type" => return parse_nth_pseudo_class(parser, input, state, NthType::OfType), + "nth-last-child" => return parse_nth_pseudo_class(parser, input, state, NthType::LastChild), + "nth-last-of-type" => return parse_nth_pseudo_class(parser, input, state, NthType::LastOfType), + "is" if parser.parse_is_and_where() => return parse_is_where(parser, input, state, Component::Is), + "where" if parser.parse_is_and_where() => return parse_is_where(parser, input, state, Component::Where), + "has" if parser.parse_has() => return parse_has(parser, input, state), + "host" => { + if !state.allows_tree_structural_pseudo_classes() { + return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState)); + } + return Ok(Component::Host(Some(parse_inner_compound_selector(parser, input, state)?))); + }, + "not" => { + return parse_negation(parser, input, state) + }, + _ => {} + } + + if parser.parse_is_and_where() && parser.is_is_alias(&name) { + return parse_is_where(parser, input, state, Component::Is); + } + + if state.intersects(SelectorParsingState::AFTER_PSEUDO_ELEMENT | SelectorParsingState::AFTER_SLOTTED) { + return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState)); + } + + let after_part = state.intersects(SelectorParsingState::AFTER_PART); + P::parse_non_ts_functional_pseudo_class(parser, name, input, after_part).map(Component::NonTSPseudoClass) +} + +fn parse_nth_pseudo_class<'i, 't, P, Impl>( + parser: &P, + input: &mut CssParser<'i, 't>, + state: SelectorParsingState, + ty: NthType, +) -> Result, ParseError<'i, P::Error>> +where + P: Parser<'i, Impl = Impl>, + Impl: SelectorImpl, +{ + if !state.allows_tree_structural_pseudo_classes() { + return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState)); + } + let (a, b) = parse_nth(input)?; + let nth_data = NthSelectorData { + ty, + is_function: true, + a, + b, + }; + if !parser.parse_nth_child_of() || ty.is_of_type() { + return Ok(Component::Nth(nth_data)); + } + + // Try to parse "of ". + if input.try_parse(|i| i.expect_ident_matching("of")).is_err() { + return Ok(Component::Nth(nth_data)); + } + // Whitespace between "of" and the selector list is optional + // https://github.com/w3c/csswg-drafts/issues/8285 + let selectors = SelectorList::parse_with_state( + parser, + input, + state | + SelectorParsingState::SKIP_DEFAULT_NAMESPACE | + SelectorParsingState::DISALLOW_PSEUDOS, + ForgivingParsing::No, + ParseRelative::No, + )?; + Ok(Component::NthOf(NthOfSelectorData::new( + &nth_data, + selectors.slice().iter().cloned(), + ))) +} + +/// Returns whether the name corresponds to a CSS2 pseudo-element that +/// can be specified with the single colon syntax (in addition to the +/// double-colon syntax, which can be used for all pseudo-elements). +fn is_css2_pseudo_element(name: &str) -> bool { + // ** Do not add to this list! ** + match_ignore_ascii_case! { name, + "before" | "after" | "first-line" | "first-letter" => true, + _ => false, + } +} + +/// Parse a simple selector other than a type selector. +/// +/// * `Err(())`: Invalid selector, abort +/// * `Ok(None)`: Not a simple selector, could be something else. `input` was not consumed. +/// * `Ok(Some(_))`: Parsed a simple selector or pseudo-element +fn parse_one_simple_selector<'i, 't, P, Impl>( + parser: &P, + input: &mut CssParser<'i, 't>, + state: SelectorParsingState, +) -> Result>, ParseError<'i, P::Error>> +where + P: Parser<'i, Impl = Impl>, + Impl: SelectorImpl, +{ + let start = input.state(); + let token = match input.next_including_whitespace().map(|t| t.clone()) { + Ok(t) => t, + Err(..) => { + input.reset(&start); + return Ok(None); + }, + }; + + Ok(Some(match token { + Token::IDHash(id) => { + if state.intersects(SelectorParsingState::AFTER_PSEUDO) { + return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState)); + } + let id = Component::ID(id.as_ref().into()); + SimpleSelectorParseResult::SimpleSelector(id) + }, + Token::Delim(delim) if delim == '.' || (delim == '&' && parser.parse_parent_selector()) => { + if state.intersects(SelectorParsingState::AFTER_PSEUDO) { + return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState)); + } + let location = input.current_source_location(); + SimpleSelectorParseResult::SimpleSelector(if delim == '&' { + Component::ParentSelector + } else { + let class = match *input.next_including_whitespace()? { + Token::Ident(ref class) => class, + ref t => { + let e = SelectorParseErrorKind::ClassNeedsIdent(t.clone()); + return Err(location.new_custom_error(e)); + }, + }; + Component::Class(class.as_ref().into()) + }) + }, + Token::SquareBracketBlock => { + if state.intersects(SelectorParsingState::AFTER_PSEUDO) { + return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState)); + } + let attr = input.parse_nested_block(|input| parse_attribute_selector(parser, input))?; + SimpleSelectorParseResult::SimpleSelector(attr) + }, + Token::Colon => { + let location = input.current_source_location(); + let (is_single_colon, next_token) = match input.next_including_whitespace()?.clone() { + Token::Colon => (false, input.next_including_whitespace()?.clone()), + t => (true, t), + }; + let (name, is_functional) = match next_token { + Token::Ident(name) => (name, false), + Token::Function(name) => (name, true), + t => { + let e = SelectorParseErrorKind::PseudoElementExpectedIdent(t); + return Err(input.new_custom_error(e)); + }, + }; + let is_pseudo_element = !is_single_colon || is_css2_pseudo_element(&name); + if is_pseudo_element { + if !state.allows_pseudos() { + return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState)); + } + let pseudo_element = if is_functional { + if P::parse_part(parser) && name.eq_ignore_ascii_case("part") { + if !state.allows_part() { + return Err( + input.new_custom_error(SelectorParseErrorKind::InvalidState) + ); + } + let names = input.parse_nested_block(|input| { + let mut result = Vec::with_capacity(1); + result.push(input.expect_ident()?.as_ref().into()); + while !input.is_exhausted() { + result.push(input.expect_ident()?.as_ref().into()); + } + Ok(result.into_boxed_slice()) + })?; + return Ok(Some(SimpleSelectorParseResult::PartPseudo(names))); + } + if P::parse_slotted(parser) && name.eq_ignore_ascii_case("slotted") { + if !state.allows_slotted() { + return Err( + input.new_custom_error(SelectorParseErrorKind::InvalidState) + ); + } + let selector = input.parse_nested_block(|input| { + parse_inner_compound_selector(parser, input, state) + })?; + return Ok(Some(SimpleSelectorParseResult::SlottedPseudo(selector))); + } + input.parse_nested_block(|input| { + P::parse_functional_pseudo_element(parser, name, input) + })? + } else { + P::parse_pseudo_element(parser, location, name)? + }; + + if state.intersects(SelectorParsingState::AFTER_SLOTTED) && + !pseudo_element.valid_after_slotted() + { + return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState)); + } + SimpleSelectorParseResult::PseudoElement(pseudo_element) + } else { + let pseudo_class = if is_functional { + input.parse_nested_block(|input| { + parse_functional_pseudo_class(parser, input, name, state) + })? + } else { + parse_simple_pseudo_class(parser, location, name, state)? + }; + SimpleSelectorParseResult::SimpleSelector(pseudo_class) + } + }, + _ => { + input.reset(&start); + return Ok(None); + }, + })) +} + +fn parse_simple_pseudo_class<'i, P, Impl>( + parser: &P, + location: SourceLocation, + name: CowRcStr<'i>, + state: SelectorParsingState, +) -> Result, ParseError<'i, P::Error>> +where + P: Parser<'i, Impl = Impl>, + Impl: SelectorImpl, +{ + if !state.allows_non_functional_pseudo_classes() { + return Err(location.new_custom_error(SelectorParseErrorKind::InvalidState)); + } + + if state.allows_tree_structural_pseudo_classes() { + match_ignore_ascii_case! { &name, + "first-child" => return Ok(Component::Nth(NthSelectorData::first(/* of_type = */ false))), + "last-child" => return Ok(Component::Nth(NthSelectorData::last(/* of_type = */ false))), + "only-child" => return Ok(Component::Nth(NthSelectorData::only(/* of_type = */ false))), + "root" => return Ok(Component::Root), + "empty" => return Ok(Component::Empty), + "scope" => return Ok(Component::Scope), + "host" if P::parse_host(parser) => return Ok(Component::Host(None)), + "first-of-type" => return Ok(Component::Nth(NthSelectorData::first(/* of_type = */ true))), + "last-of-type" => return Ok(Component::Nth(NthSelectorData::last(/* of_type = */ true))), + "only-of-type" => return Ok(Component::Nth(NthSelectorData::only(/* of_type = */ true))), + _ => {}, + } + } + + let pseudo_class = P::parse_non_ts_pseudo_class(parser, location, name)?; + if state.intersects(SelectorParsingState::AFTER_PSEUDO_ELEMENT) && + !pseudo_class.is_user_action_state() + { + return Err(location.new_custom_error(SelectorParseErrorKind::InvalidState)); + } + Ok(Component::NonTSPseudoClass(pseudo_class)) +} + +// NB: pub module in order to access the DummyParser +#[cfg(test)] +pub mod tests { + use super::*; + use crate::builder::SelectorFlags; + use crate::parser; + use cssparser::{serialize_identifier, Parser as CssParser, ParserInput, ToCss}; + use std::collections::HashMap; + use std::fmt; + + #[derive(Clone, Debug, Eq, PartialEq)] + pub enum PseudoClass { + Hover, + Active, + Lang(String), + } + + #[derive(Clone, Debug, Eq, PartialEq)] + pub enum PseudoElement { + Before, + After, + Highlight(String), + } + + impl parser::PseudoElement for PseudoElement { + type Impl = DummySelectorImpl; + + fn accepts_state_pseudo_classes(&self) -> bool { + true + } + + fn valid_after_slotted(&self) -> bool { + true + } + } + + impl parser::NonTSPseudoClass for PseudoClass { + type Impl = DummySelectorImpl; + + #[inline] + fn is_active_or_hover(&self) -> bool { + matches!(*self, PseudoClass::Active | PseudoClass::Hover) + } + + #[inline] + fn is_user_action_state(&self) -> bool { + self.is_active_or_hover() + } + } + + impl ToCss for PseudoClass { + fn to_css(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + match *self { + PseudoClass::Hover => dest.write_str(":hover"), + PseudoClass::Active => dest.write_str(":active"), + PseudoClass::Lang(ref lang) => { + dest.write_str(":lang(")?; + serialize_identifier(lang, dest)?; + dest.write_char(')') + }, + } + } + } + + impl ToCss for PseudoElement { + fn to_css(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + match *self { + PseudoElement::Before => dest.write_str("::before"), + PseudoElement::After => dest.write_str("::after"), + PseudoElement::Highlight(ref name) => { + dest.write_str("::highlight(")?; + serialize_identifier(&name, dest)?; + dest.write_char(')') + }, + } + } + } + + #[derive(Clone, Debug, PartialEq)] + pub struct DummySelectorImpl; + + #[derive(Default)] + pub struct DummyParser { + default_ns: Option, + ns_prefixes: HashMap, + } + + impl DummyParser { + fn default_with_namespace(default_ns: DummyAtom) -> DummyParser { + DummyParser { + default_ns: Some(default_ns), + ns_prefixes: Default::default(), + } + } + } + + impl SelectorImpl for DummySelectorImpl { + type ExtraMatchingData<'a> = std::marker::PhantomData<&'a ()>; + type AttrValue = DummyAttrValue; + type Identifier = DummyAtom; + type LocalName = DummyAtom; + type NamespaceUrl = DummyAtom; + type NamespacePrefix = DummyAtom; + type BorrowedLocalName = DummyAtom; + type BorrowedNamespaceUrl = DummyAtom; + type NonTSPseudoClass = PseudoClass; + type PseudoElement = PseudoElement; + } + + #[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] + pub struct DummyAttrValue(String); + + impl ToCss for DummyAttrValue { + fn to_css(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + use std::fmt::Write; + + dest.write_char('"')?; + write!(cssparser::CssStringWriter::new(dest), "{}", &self.0)?; + dest.write_char('"') + } + } + + impl<'a> From<&'a str> for DummyAttrValue { + fn from(string: &'a str) -> Self { + Self(string.into()) + } + } + + #[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] + pub struct DummyAtom(String); + + impl ToCss for DummyAtom { + fn to_css(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + serialize_identifier(&self.0, dest) + } + } + + impl From for DummyAtom { + fn from(string: String) -> Self { + DummyAtom(string) + } + } + + impl<'a> From<&'a str> for DummyAtom { + fn from(string: &'a str) -> Self { + DummyAtom(string.into()) + } + } + + impl PrecomputedHash for DummyAtom { + fn precomputed_hash(&self) -> u32 { + self.0.as_ptr() as u32 + } + } + + impl<'i> Parser<'i> for DummyParser { + type Impl = DummySelectorImpl; + type Error = SelectorParseErrorKind<'i>; + + fn parse_slotted(&self) -> bool { + true + } + + fn parse_nth_child_of(&self) -> bool { + true + } + + fn parse_is_and_where(&self) -> bool { + true + } + + fn parse_has(&self) -> bool { + true + } + + fn parse_parent_selector(&self) -> bool { + true + } + + fn parse_part(&self) -> bool { + true + } + + fn parse_non_ts_pseudo_class( + &self, + location: SourceLocation, + name: CowRcStr<'i>, + ) -> Result> { + match_ignore_ascii_case! { &name, + "hover" => return Ok(PseudoClass::Hover), + "active" => return Ok(PseudoClass::Active), + _ => {} + } + Err( + location.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement( + name, + )), + ) + } + + fn parse_non_ts_functional_pseudo_class<'t>( + &self, + name: CowRcStr<'i>, + parser: &mut CssParser<'i, 't>, + after_part: bool, + ) -> Result> { + match_ignore_ascii_case! { &name, + "lang" if !after_part => { + let lang = parser.expect_ident_or_string()?.as_ref().to_owned(); + return Ok(PseudoClass::Lang(lang)); + }, + _ => {} + } + Err( + parser.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement( + name, + )), + ) + } + + fn parse_pseudo_element( + &self, + location: SourceLocation, + name: CowRcStr<'i>, + ) -> Result> { + match_ignore_ascii_case! { &name, + "before" => return Ok(PseudoElement::Before), + "after" => return Ok(PseudoElement::After), + _ => {} + } + Err( + location.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement( + name, + )), + ) + } + + fn parse_functional_pseudo_element<'t>( + &self, + name: CowRcStr<'i>, + parser: &mut CssParser<'i, 't>, + ) -> Result> { + match_ignore_ascii_case! {&name, + "highlight" => return Ok(PseudoElement::Highlight(parser.expect_ident()?.as_ref().to_owned())), + _ => {} + } + Err( + parser.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement( + name, + )), + ) + } + + fn default_namespace(&self) -> Option { + self.default_ns.clone() + } + + fn namespace_for_prefix(&self, prefix: &DummyAtom) -> Option { + self.ns_prefixes.get(prefix).cloned() + } + } + + fn parse<'i>( + input: &'i str, + ) -> Result, SelectorParseError<'i>> { + parse_relative(input, ParseRelative::No) + } + + fn parse_relative<'i>( + input: &'i str, + parse_relative: ParseRelative, + ) -> Result, SelectorParseError<'i>> { + parse_ns_relative(input, &DummyParser::default(), parse_relative) + } + + fn parse_expected<'i, 'a>( + input: &'i str, + expected: Option<&'a str>, + ) -> Result, SelectorParseError<'i>> { + parse_ns_expected(input, &DummyParser::default(), expected) + } + + fn parse_relative_expected<'i, 'a>( + input: &'i str, + parse_relative: ParseRelative, + expected: Option<&'a str>, + ) -> Result, SelectorParseError<'i>> { + parse_ns_relative_expected(input, &DummyParser::default(), parse_relative, expected) + } + + fn parse_ns<'i>( + input: &'i str, + parser: &DummyParser, + ) -> Result, SelectorParseError<'i>> { + parse_ns_relative(input, parser, ParseRelative::No) + } + + fn parse_ns_relative<'i>( + input: &'i str, + parser: &DummyParser, + parse_relative: ParseRelative, + ) -> Result, SelectorParseError<'i>> { + parse_ns_relative_expected(input, parser, parse_relative, None) + } + + fn parse_ns_expected<'i, 'a>( + input: &'i str, + parser: &DummyParser, + expected: Option<&'a str>, + ) -> Result, SelectorParseError<'i>> { + parse_ns_relative_expected(input, parser, ParseRelative::No, expected) + } + + fn parse_ns_relative_expected<'i, 'a>( + input: &'i str, + parser: &DummyParser, + parse_relative: ParseRelative, + expected: Option<&'a str>, + ) -> Result, SelectorParseError<'i>> { + let mut parser_input = ParserInput::new(input); + let result = SelectorList::parse( + parser, + &mut CssParser::new(&mut parser_input), + parse_relative, + ); + if let Ok(ref selectors) = result { + // We can't assume that the serialized parsed selector will equal + // the input; for example, if there is no default namespace, '*|foo' + // should serialize to 'foo'. + assert_eq!( + selectors.to_css_string(), + match expected { + Some(x) => x, + None => input, + } + ); + } + result + } + + fn specificity(a: u32, b: u32, c: u32) -> u32 { + a << 20 | b << 10 | c + } + + #[test] + fn test_empty() { + let mut input = ParserInput::new(":empty"); + let list = SelectorList::parse( + &DummyParser::default(), + &mut CssParser::new(&mut input), + ParseRelative::No, + ); + assert!(list.is_ok()); + } + + const MATHML: &str = "http://www.w3.org/1998/Math/MathML"; + const SVG: &str = "http://www.w3.org/2000/svg"; + + #[test] + fn test_parsing() { + assert!(parse("").is_err()); + assert!(parse(":lang(4)").is_err()); + assert!(parse(":lang(en US)").is_err()); + assert_eq!( + parse("EeÉ"), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![Component::LocalName(LocalName { + name: DummyAtom::from("EeÉ"), + lower_name: DummyAtom::from("eeÉ"), + })], + specificity(0, 0, 1), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + assert_eq!( + parse("|e"), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::ExplicitNoNamespace, + Component::LocalName(LocalName { + name: DummyAtom::from("e"), + lower_name: DummyAtom::from("e"), + }), + ], + specificity(0, 0, 1), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + // When the default namespace is not set, *| should be elided. + // https://github.com/servo/servo/pull/17537 + assert_eq!( + parse_expected("*|e", Some("e")), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![Component::LocalName(LocalName { + name: DummyAtom::from("e"), + lower_name: DummyAtom::from("e"), + })], + specificity(0, 0, 1), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + // When the default namespace is set, *| should _not_ be elided (as foo + // is no longer equivalent to *|foo--the former is only for foo in the + // default namespace). + // https://github.com/servo/servo/issues/16020 + assert_eq!( + parse_ns( + "*|e", + &DummyParser::default_with_namespace(DummyAtom::from("https://mozilla.org")) + ), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::ExplicitAnyNamespace, + Component::LocalName(LocalName { + name: DummyAtom::from("e"), + lower_name: DummyAtom::from("e"), + }), + ], + specificity(0, 0, 1), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + assert_eq!( + parse("*"), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![Component::ExplicitUniversalType], + specificity(0, 0, 0), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + assert_eq!( + parse("|*"), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::ExplicitNoNamespace, + Component::ExplicitUniversalType, + ], + specificity(0, 0, 0), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + assert_eq!( + parse_expected("*|*", Some("*")), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![Component::ExplicitUniversalType], + specificity(0, 0, 0), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + assert_eq!( + parse_ns( + "*|*", + &DummyParser::default_with_namespace(DummyAtom::from("https://mozilla.org")) + ), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::ExplicitAnyNamespace, + Component::ExplicitUniversalType, + ], + specificity(0, 0, 0), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + assert_eq!( + parse(".foo:lang(en-US)"), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::Class(DummyAtom::from("foo")), + Component::NonTSPseudoClass(PseudoClass::Lang("en-US".to_owned())), + ], + specificity(0, 2, 0), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + assert_eq!( + parse("#bar"), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![Component::ID(DummyAtom::from("bar"))], + specificity(1, 0, 0), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + assert_eq!( + parse("e.foo#bar"), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::LocalName(LocalName { + name: DummyAtom::from("e"), + lower_name: DummyAtom::from("e"), + }), + Component::Class(DummyAtom::from("foo")), + Component::ID(DummyAtom::from("bar")), + ], + specificity(1, 1, 1), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + assert_eq!( + parse("e.foo #bar"), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::LocalName(LocalName { + name: DummyAtom::from("e"), + lower_name: DummyAtom::from("e"), + }), + Component::Class(DummyAtom::from("foo")), + Component::Combinator(Combinator::Descendant), + Component::ID(DummyAtom::from("bar")), + ], + specificity(1, 1, 1), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + // Default namespace does not apply to attribute selectors + // https://github.com/mozilla/servo/pull/1652 + let mut parser = DummyParser::default(); + assert_eq!( + parse_ns("[Foo]", &parser), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![Component::AttributeInNoNamespaceExists { + local_name: DummyAtom::from("Foo"), + local_name_lower: DummyAtom::from("foo"), + }], + specificity(0, 1, 0), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + assert!(parse_ns("svg|circle", &parser).is_err()); + parser + .ns_prefixes + .insert(DummyAtom("svg".into()), DummyAtom(SVG.into())); + assert_eq!( + parse_ns("svg|circle", &parser), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::Namespace(DummyAtom("svg".into()), SVG.into()), + Component::LocalName(LocalName { + name: DummyAtom::from("circle"), + lower_name: DummyAtom::from("circle"), + }), + ], + specificity(0, 0, 1), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + assert_eq!( + parse_ns("svg|*", &parser), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::Namespace(DummyAtom("svg".into()), SVG.into()), + Component::ExplicitUniversalType, + ], + specificity(0, 0, 0), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + // Default namespace does not apply to attribute selectors + // https://github.com/mozilla/servo/pull/1652 + // but it does apply to implicit type selectors + // https://github.com/servo/rust-selectors/pull/82 + parser.default_ns = Some(MATHML.into()); + assert_eq!( + parse_ns("[Foo]", &parser), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::DefaultNamespace(MATHML.into()), + Component::AttributeInNoNamespaceExists { + local_name: DummyAtom::from("Foo"), + local_name_lower: DummyAtom::from("foo"), + }, + ], + specificity(0, 1, 0), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + // Default namespace does apply to type selectors + assert_eq!( + parse_ns("e", &parser), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::DefaultNamespace(MATHML.into()), + Component::LocalName(LocalName { + name: DummyAtom::from("e"), + lower_name: DummyAtom::from("e"), + }), + ], + specificity(0, 0, 1), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + assert_eq!( + parse_ns("*", &parser), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::DefaultNamespace(MATHML.into()), + Component::ExplicitUniversalType, + ], + specificity(0, 0, 0), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + assert_eq!( + parse_ns("*|*", &parser), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::ExplicitAnyNamespace, + Component::ExplicitUniversalType, + ], + specificity(0, 0, 0), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + // Default namespace applies to universal and type selectors inside :not and :matches, + // but not otherwise. + assert_eq!( + parse_ns(":not(.cl)", &parser), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::DefaultNamespace(MATHML.into()), + Component::Negation(SelectorList::from_vec(vec![Selector::from_vec( + vec![Component::Class(DummyAtom::from("cl"))], + specificity(0, 1, 0), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])), + ], + specificity(0, 1, 0), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + assert_eq!( + parse_ns(":not(*)", &parser), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::DefaultNamespace(MATHML.into()), + Component::Negation(SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::DefaultNamespace(MATHML.into()), + Component::ExplicitUniversalType, + ], + specificity(0, 0, 0), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )]),), + ], + specificity(0, 0, 0), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + assert_eq!( + parse_ns(":not(e)", &parser), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::DefaultNamespace(MATHML.into()), + Component::Negation(SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::DefaultNamespace(MATHML.into()), + Component::LocalName(LocalName { + name: DummyAtom::from("e"), + lower_name: DummyAtom::from("e"), + }), + ], + specificity(0, 0, 1), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])), + ], + specificity(0, 0, 1), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + assert_eq!( + parse("[attr|=\"foo\"]"), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![Component::AttributeInNoNamespace { + local_name: DummyAtom::from("attr"), + operator: AttrSelectorOperator::DashMatch, + value: DummyAttrValue::from("foo"), + case_sensitivity: ParsedCaseSensitivity::CaseSensitive, + }], + specificity(0, 1, 0), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + // https://github.com/mozilla/servo/issues/1723 + assert_eq!( + parse("::before"), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::Combinator(Combinator::PseudoElement), + Component::PseudoElement(PseudoElement::Before), + ], + specificity(0, 0, 1), + SelectorFlags::HAS_PSEUDO, + )])) + ); + assert_eq!( + parse("::before:hover"), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::Combinator(Combinator::PseudoElement), + Component::PseudoElement(PseudoElement::Before), + Component::NonTSPseudoClass(PseudoClass::Hover), + ], + specificity(0, 1, 1), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT | SelectorFlags::HAS_PSEUDO, + )])) + ); + assert_eq!( + parse("::before:hover:hover"), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::Combinator(Combinator::PseudoElement), + Component::PseudoElement(PseudoElement::Before), + Component::NonTSPseudoClass(PseudoClass::Hover), + Component::NonTSPseudoClass(PseudoClass::Hover), + ], + specificity(0, 2, 1), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT | SelectorFlags::HAS_PSEUDO, + )])) + ); + assert!(parse("::before:hover:lang(foo)").is_err()); + assert!(parse("::before:hover .foo").is_err()); + assert!(parse("::before .foo").is_err()); + assert!(parse("::before ~ bar").is_err()); + assert!(parse("::before:active").is_ok()); + + // https://github.com/servo/servo/issues/15335 + assert!(parse(":: before").is_err()); + assert_eq!( + parse("div ::after"), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::LocalName(LocalName { + name: DummyAtom::from("div"), + lower_name: DummyAtom::from("div"), + }), + Component::Combinator(Combinator::Descendant), + Component::Combinator(Combinator::PseudoElement), + Component::PseudoElement(PseudoElement::After), + ], + specificity(0, 0, 2), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT | SelectorFlags::HAS_PSEUDO, + )])) + ); + assert_eq!( + parse("#d1 > .ok"), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::ID(DummyAtom::from("d1")), + Component::Combinator(Combinator::Child), + Component::Class(DummyAtom::from("ok")), + ], + (1 << 20) + (1 << 10) + (0 << 0), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + parser.default_ns = None; + assert!(parse(":not(#provel.old)").is_ok()); + assert!(parse(":not(#provel > old)").is_ok()); + assert!(parse("table[rules]:not([rules=\"none\"]):not([rules=\"\"])").is_ok()); + // https://github.com/servo/servo/issues/16017 + assert_eq!( + parse_ns(":not(*)", &parser), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![Component::Negation(SelectorList::from_vec(vec![ + Selector::from_vec( + vec![Component::ExplicitUniversalType], + specificity(0, 0, 0), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + ) + ]))], + specificity(0, 0, 0), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + assert_eq!( + parse_ns(":not(|*)", &parser), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![Component::Negation(SelectorList::from_vec(vec![ + Selector::from_vec( + vec![ + Component::ExplicitNoNamespace, + Component::ExplicitUniversalType, + ], + specificity(0, 0, 0), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + ) + ]))], + specificity(0, 0, 0), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + // *| should be elided if there is no default namespace. + // https://github.com/servo/servo/pull/17537 + assert_eq!( + parse_ns_expected(":not(*|*)", &parser, Some(":not(*)")), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![Component::Negation(SelectorList::from_vec(vec![ + Selector::from_vec( + vec![Component::ExplicitUniversalType], + specificity(0, 0, 0), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + ) + ]))], + specificity(0, 0, 0), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + + assert!(parse("::highlight(foo)").is_ok()); + + assert!(parse("::slotted()").is_err()); + assert!(parse("::slotted(div)").is_ok()); + assert!(parse("::slotted(div).foo").is_err()); + assert!(parse("::slotted(div + bar)").is_err()); + assert!(parse("::slotted(div) + foo").is_err()); + + assert!(parse("::part()").is_err()); + assert!(parse("::part(42)").is_err()); + assert!(parse("::part(foo bar)").is_ok()); + assert!(parse("::part(foo):hover").is_ok()); + assert!(parse("::part(foo) + bar").is_err()); + + assert!(parse("div ::slotted(div)").is_ok()); + assert!(parse("div + slot::slotted(div)").is_ok()); + assert!(parse("div + slot::slotted(div.foo)").is_ok()); + assert!(parse("slot::slotted(div,foo)::first-line").is_err()); + assert!(parse("::slotted(div)::before").is_ok()); + assert!(parse("slot::slotted(div,foo)").is_err()); + + assert!(parse("foo:where()").is_ok()); + assert!(parse("foo:where(div, foo, .bar baz)").is_ok()); + assert!(parse("foo:where(::before)").is_ok()); + } + + #[test] + fn parent_selector() { + assert!(parse("foo &").is_ok()); + assert_eq!( + parse("#foo &.bar"), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::ID(DummyAtom::from("foo")), + Component::Combinator(Combinator::Descendant), + Component::ParentSelector, + Component::Class(DummyAtom::from("bar")), + ], + (1 << 20) + (1 << 10) + (0 << 0), + SelectorFlags::HAS_PARENT | SelectorFlags::HAS_NON_FEATURELESS_COMPONENT + )])) + ); + + let parent = parse(".bar, div .baz").unwrap(); + let child = parse("#foo &.bar").unwrap(); + assert_eq!( + child.replace_parent_selector(&parent), + parse("#foo :is(.bar, div .baz).bar").unwrap() + ); + + let has_child = parse("#foo:has(&.bar)").unwrap(); + assert_eq!( + has_child.replace_parent_selector(&parent), + parse("#foo:has(:is(.bar, div .baz).bar)").unwrap() + ); + + let child = parse("#foo").unwrap(); + assert_eq!( + child.replace_parent_selector(&parent), + parse(":is(.bar, div .baz) #foo").unwrap() + ); + + let child = + parse_relative_expected("+ #foo", ParseRelative::ForNesting, Some("& + #foo")).unwrap(); + assert_eq!(child, parse("& + #foo").unwrap()); + } + + #[test] + fn test_pseudo_iter() { + let list = parse("q::before").unwrap(); + let selector = &list.slice()[0]; + assert!(!selector.is_universal()); + let mut iter = selector.iter(); + assert_eq!( + iter.next(), + Some(&Component::PseudoElement(PseudoElement::Before)) + ); + assert_eq!(iter.next(), None); + let combinator = iter.next_sequence(); + assert_eq!(combinator, Some(Combinator::PseudoElement)); + assert!(matches!(iter.next(), Some(&Component::LocalName(..)))); + assert_eq!(iter.next(), None); + assert_eq!(iter.next_sequence(), None); + } + + #[test] + fn test_universal() { + let list = parse_ns( + "*|*::before", + &DummyParser::default_with_namespace(DummyAtom::from("https://mozilla.org")), + ) + .unwrap(); + let selector = &list.slice()[0]; + assert!(selector.is_universal()); + } + + #[test] + fn test_empty_pseudo_iter() { + let list = parse("::before").unwrap(); + let selector = &list.slice()[0]; + assert!(selector.is_universal()); + let mut iter = selector.iter(); + assert_eq!( + iter.next(), + Some(&Component::PseudoElement(PseudoElement::Before)) + ); + assert_eq!(iter.next(), None); + assert_eq!(iter.next_sequence(), Some(Combinator::PseudoElement)); + assert_eq!(iter.next(), None); + assert_eq!(iter.next_sequence(), None); + } + + struct TestVisitor { + seen: Vec, + } + + impl SelectorVisitor for TestVisitor { + type Impl = DummySelectorImpl; + + fn visit_simple_selector(&mut self, s: &Component) -> bool { + let mut dest = String::new(); + s.to_css(&mut dest).unwrap(); + self.seen.push(dest); + true + } + } + + #[test] + fn visitor() { + let mut test_visitor = TestVisitor { seen: vec![] }; + parse(":not(:hover) ~ label").unwrap().slice()[0].visit(&mut test_visitor); + assert!(test_visitor.seen.contains(&":hover".into())); + + let mut test_visitor = TestVisitor { seen: vec![] }; + parse("::before:hover").unwrap().slice()[0].visit(&mut test_visitor); + assert!(test_visitor.seen.contains(&":hover".into())); + } +} diff --git a/servo/components/selectors/relative_selector/cache.rs b/servo/components/selectors/relative_selector/cache.rs new file mode 100644 index 0000000000..d7681aa3a4 --- /dev/null +++ b/servo/components/selectors/relative_selector/cache.rs @@ -0,0 +1,81 @@ +/* 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 fxhash::FxHashMap; +/// Relative selector cache. This is useful for following cases. +/// First case is non-subject relative selector: Imagine `.anchor:has(<..>) ~ .foo`, with DOM +/// `.anchor + .foo + .. + .foo`. Each match on `.foo` triggers `:has()` traversal that +/// yields the same result. This is simple enough, since we just need to store +/// the exact match on that anchor pass/fail. +/// Second case is `querySelectorAll`: Imagine `querySelectorAll(':has(.a)')`, with DOM +/// `div > .. > div > .a`. When the we perform the traversal at the top div, +/// we basically end up evaluating `:has(.a)` for all anchors, which could be reused. +/// Also consider the sibling version: `querySelectorAll(':has(~ .a)')` with DOM +/// `div + .. + div + .a`. +/// TODO(dshin): Second case is not yet handled. That is tracked in Bug 1845291. +use std::hash::Hash; + +use crate::parser::{RelativeSelector, SelectorKey}; +use crate::{tree::OpaqueElement, SelectorImpl}; + +/// Match data for a given element and a selector. +#[derive(Clone, Copy)] +pub enum RelativeSelectorCachedMatch { + /// This selector matches this element. + Matched, + /// This selector does not match this element. + NotMatched, +} + +impl RelativeSelectorCachedMatch { + /// Is the cached result a match? + pub fn matched(&self) -> bool { + matches!(*self, Self::Matched) + } +} + +#[derive(Clone, Copy, Hash, Eq, PartialEq)] +struct Key { + element: OpaqueElement, + selector: SelectorKey, +} + +impl Key { + pub fn new( + element: OpaqueElement, + selector: &RelativeSelector, + ) -> Self { + Key { + element, + selector: SelectorKey::new(&selector.selector), + } + } +} + +/// Cache to speed up matching of relative selectors. +#[derive(Default)] +pub struct RelativeSelectorCache { + cache: FxHashMap, +} + +impl RelativeSelectorCache { + /// Add a relative selector match into the cache. + pub fn add( + &mut self, + anchor: OpaqueElement, + selector: &RelativeSelector, + matched: RelativeSelectorCachedMatch, + ) { + self.cache.insert(Key::new(anchor, selector), matched); + } + + /// Check if we have a cache entry for the element. + pub fn lookup( + &mut self, + element: OpaqueElement, + selector: &RelativeSelector, + ) -> Option { + self.cache.get(&Key::new(element, selector)).copied() + } +} diff --git a/servo/components/selectors/relative_selector/filter.rs b/servo/components/selectors/relative_selector/filter.rs new file mode 100644 index 0000000000..3c3eb7d2fa --- /dev/null +++ b/servo/components/selectors/relative_selector/filter.rs @@ -0,0 +1,159 @@ +/* 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/. */ + +/// Bloom filter for relative selectors. +use fxhash::FxHashMap; + +use crate::bloom::BloomFilter; +use crate::context::QuirksMode; +use crate::parser::{collect_selector_hashes, RelativeSelector, RelativeSelectorMatchHint}; +use crate::tree::{Element, OpaqueElement}; +use crate::SelectorImpl; + +enum Entry { + /// Filter lookup happened once. Construction of the filter is expensive, + /// so this is set when the element for subtree traversal is encountered. + Lookup, + /// Filter lookup happened more than once, and the filter for this element's + /// subtree traversal is constructed. Could use special handlings for pseudo-classes + /// such as `:hover` and `:focus`, see Bug 1845503. + HasFilter(Box), +} + +#[derive(Clone, Copy, Hash, Eq, PartialEq)] +enum TraversalKind { + Children, + Descendants, +} + +fn add_to_filter(element: &E, filter: &mut BloomFilter, kind: TraversalKind) -> bool { + let mut child = element.first_element_child(); + while let Some(e) = child { + if !e.add_element_unique_hashes(filter) { + return false; + } + if kind == TraversalKind::Descendants { + if !add_to_filter(&e, filter, kind) { + return false; + } + } + child = e.next_sibling_element(); + } + true +} + +#[derive(Clone, Copy, Hash, Eq, PartialEq)] +struct Key(OpaqueElement, TraversalKind); + +/// Map of bloom filters for fast-rejecting relative selectors. +#[derive(Default)] +pub struct RelativeSelectorFilterMap { + map: FxHashMap, +} + +fn fast_reject( + selector: &RelativeSelector, + quirks_mode: QuirksMode, + filter: &BloomFilter, +) -> bool { + let mut hashes = [0u32; 4]; + let mut len = 0; + // For inner selectors, we only collect from the single rightmost compound. + // This is because inner selectors can cause breakouts: e.g. `.anchor:has(:is(.a .b) .c)` + // can match when `.a` is the ancestor of `.anchor`. Including `.a` would possibly fast + // reject the subtree for not having `.a`, even if the selector would match. + // Technically, if the selector's traversal is non-sibling subtree, we can traverse + // inner selectors up to the point where a descendant/child combinator is encountered + // (e.g. In `.anchor:has(:is(.a ~ .b) .c)`, `.a` is guaranteed to be the a descendant + // of `.anchor`). While that can be separately handled, well, this is simpler. + collect_selector_hashes( + selector.selector.iter(), + quirks_mode, + &mut hashes, + &mut len, + |s| s.iter(), + ); + for i in 0..len { + if !filter.might_contain_hash(hashes[i]) { + // Definitely rejected. + return true; + } + } + false +} + +impl RelativeSelectorFilterMap { + fn get_filter(&mut self, element: &E, kind: TraversalKind) -> Option<&BloomFilter> { + // Insert flag to indicate that we looked up the filter once, and + // create the filter if and only if that flag is there. + let key = Key(element.opaque(), kind); + let entry = self + .map + .entry(key) + .and_modify(|entry| { + if !matches!(entry, Entry::Lookup) { + return; + } + let mut filter = BloomFilter::new(); + // Go through all children/descendants of this element and add their hashes. + if add_to_filter(element, &mut filter, kind) { + *entry = Entry::HasFilter(Box::new(filter)); + } + }) + .or_insert(Entry::Lookup); + match entry { + Entry::Lookup => None, + Entry::HasFilter(ref filter) => Some(filter.as_ref()), + } + } + + /// Potentially reject the given selector for this element. + /// This may seem redundant in presence of the cache, but the cache keys into the + /// selector-element pair specifically, while this filter keys to the element + /// and the traversal kind, so it is useful for handling multiple selectors + /// that effectively end up looking at the same(-ish, for siblings) subtree. + pub fn fast_reject( + &mut self, + element: &E, + selector: &RelativeSelector, + quirks_mode: QuirksMode, + ) -> bool { + if matches!( + selector.match_hint, + RelativeSelectorMatchHint::InNextSibling + ) { + // Don't bother. + return false; + } + let is_sibling = matches!( + selector.match_hint, + RelativeSelectorMatchHint::InSibling | + RelativeSelectorMatchHint::InNextSiblingSubtree | + RelativeSelectorMatchHint::InSiblingSubtree + ); + let is_subtree = matches!( + selector.match_hint, + RelativeSelectorMatchHint::InSubtree | + RelativeSelectorMatchHint::InNextSiblingSubtree | + RelativeSelectorMatchHint::InSiblingSubtree + ); + let kind = if is_subtree { + TraversalKind::Descendants + } else { + TraversalKind::Children + }; + if is_sibling { + // Contain the entirety of the parent's children/subtree in the filter, and use that. + // This is less likely to reject, especially for sibling subtree matches; however, it's less + // expensive memory-wise, compared to storing filters for each sibling. + element.parent_element().map_or(false, |parent| { + self.get_filter(&parent, kind) + .map_or(false, |filter| fast_reject(selector, quirks_mode, filter)) + }) + } else { + self.get_filter(element, kind) + .map_or(false, |filter| fast_reject(selector, quirks_mode, filter)) + } + } +} diff --git a/servo/components/selectors/relative_selector/mod.rs b/servo/components/selectors/relative_selector/mod.rs new file mode 100644 index 0000000000..6dd39f7327 --- /dev/null +++ b/servo/components/selectors/relative_selector/mod.rs @@ -0,0 +1,6 @@ +/* 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/. */ + +pub mod cache; +pub mod filter; diff --git a/servo/components/selectors/sink.rs b/servo/components/selectors/sink.rs new file mode 100644 index 0000000000..dcdd7ff259 --- /dev/null +++ b/servo/components/selectors/sink.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/. */ + +//! Small helpers to abstract over different containers. +#![deny(missing_docs)] + +use smallvec::{Array, SmallVec}; + +/// A trait to abstract over a `push` method that may be implemented for +/// different kind of types. +/// +/// Used to abstract over `Array`, `SmallVec` and `Vec`, and also to implement a +/// type which `push` method does only tweak a byte when we only need to check +/// for the presence of something. +pub trait Push { + /// Push a value into self. + fn push(&mut self, value: T); +} + +impl Push for Vec { + fn push(&mut self, value: T) { + Vec::push(self, value); + } +} + +impl Push for SmallVec { + fn push(&mut self, value: A::Item) { + SmallVec::push(self, value); + } +} diff --git a/servo/components/selectors/tree.rs b/servo/components/selectors/tree.rs new file mode 100644 index 0000000000..c1ea8ff5ae --- /dev/null +++ b/servo/components/selectors/tree.rs @@ -0,0 +1,168 @@ +/* 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/. */ + +//! Traits that nodes must implement. Breaks the otherwise-cyclic dependency +//! between layout and style. + +use crate::attr::{AttrSelectorOperation, CaseSensitivity, NamespaceConstraint}; +use crate::bloom::BloomFilter; +use crate::matching::{ElementSelectorFlags, MatchingContext}; +use crate::parser::SelectorImpl; +use std::fmt::Debug; +use std::ptr::NonNull; + +/// Opaque representation of an Element, for identity comparisons. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub struct OpaqueElement(NonNull<()>); + +unsafe impl Send for OpaqueElement {} + +impl OpaqueElement { + /// Creates a new OpaqueElement from an arbitrarily-typed pointer. + pub fn new(ptr: &T) -> Self { + unsafe { + OpaqueElement(NonNull::new_unchecked( + ptr as *const T as *const () as *mut (), + )) + } + } +} + +pub trait Element: Sized + Clone + Debug { + type Impl: SelectorImpl; + + /// Converts self into an opaque representation. + fn opaque(&self) -> OpaqueElement; + + fn parent_element(&self) -> Option; + + /// Whether the parent node of this element is a shadow root. + fn parent_node_is_shadow_root(&self) -> bool; + + /// The host of the containing shadow root, if any. + fn containing_shadow_host(&self) -> Option; + + /// The parent of a given pseudo-element, after matching a pseudo-element + /// selector. + /// + /// This is guaranteed to be called in a pseudo-element. + fn pseudo_element_originating_element(&self) -> Option { + debug_assert!(self.is_pseudo_element()); + self.parent_element() + } + + /// Whether we're matching on a pseudo-element. + fn is_pseudo_element(&self) -> bool; + + /// Skips non-element nodes + fn prev_sibling_element(&self) -> Option; + + /// Skips non-element nodes + fn next_sibling_element(&self) -> Option; + + /// Skips non-element nodes + fn first_element_child(&self) -> Option; + + fn is_html_element_in_html_document(&self) -> bool; + + fn has_local_name(&self, local_name: &::BorrowedLocalName) -> bool; + + /// Empty string for no namespace + fn has_namespace(&self, ns: &::BorrowedNamespaceUrl) -> bool; + + /// Whether this element and the `other` element have the same local name and namespace. + fn is_same_type(&self, other: &Self) -> bool; + + fn attr_matches( + &self, + ns: &NamespaceConstraint<&::NamespaceUrl>, + local_name: &::LocalName, + operation: &AttrSelectorOperation<&::AttrValue>, + ) -> bool; + + fn has_attr_in_no_namespace( + &self, + local_name: &::LocalName, + ) -> bool { + self.attr_matches( + &NamespaceConstraint::Specific(&crate::parser::namespace_empty_string::()), + local_name, + &AttrSelectorOperation::Exists, + ) + } + + fn match_non_ts_pseudo_class( + &self, + pc: &::NonTSPseudoClass, + context: &mut MatchingContext, + ) -> bool; + + fn match_pseudo_element( + &self, + pe: &::PseudoElement, + context: &mut MatchingContext, + ) -> bool; + + /// Sets selector flags on the elemnt itself or the parent, depending on the + /// flags, which indicate what kind of work may need to be performed when + /// DOM state changes. + fn apply_selector_flags(&self, flags: ElementSelectorFlags); + + /// Whether this element is a `link`. + fn is_link(&self) -> bool; + + /// Returns whether the element is an HTML element. + fn is_html_slot_element(&self) -> bool; + + /// Returns the assigned element this element is assigned to. + /// + /// Necessary for the `::slotted` pseudo-class. + fn assigned_slot(&self) -> Option { + None + } + + fn has_id( + &self, + id: &::Identifier, + case_sensitivity: CaseSensitivity, + ) -> bool; + + fn has_class( + &self, + name: &::Identifier, + case_sensitivity: CaseSensitivity, + ) -> bool; + + /// Returns the mapping from the `exportparts` attribute in the reverse + /// direction, that is, in an outer-tree -> inner-tree direction. + fn imported_part( + &self, + name: &::Identifier, + ) -> Option<::Identifier>; + + fn is_part(&self, name: &::Identifier) -> bool; + + /// Returns whether this element matches `:empty`. + /// + /// That is, whether it does not contain any child element or any non-zero-length text node. + /// See http://dev.w3.org/csswg/selectors-3/#empty-pseudo + fn is_empty(&self) -> bool; + + /// Returns whether this element matches `:root`, + /// i.e. whether it is the root element of a document. + /// + /// Note: this can be false even if `.parent_element()` is `None` + /// if the parent node is a `DocumentFragment`. + fn is_root(&self) -> bool; + + /// Returns whether this element should ignore matching nth child + /// selector. + fn ignores_nth_child_selectors(&self) -> bool { + false + } + + /// Add hashes unique to this element to the given filter, returning true + /// if any got added. + fn add_element_unique_hashes(&self, filter: &mut BloomFilter) -> bool; +} diff --git a/servo/components/selectors/visitor.rs b/servo/components/selectors/visitor.rs new file mode 100644 index 0000000000..d5befbc68b --- /dev/null +++ b/servo/components/selectors/visitor.rs @@ -0,0 +1,136 @@ +/* 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/. */ + +//! Visitor traits for selectors. + +#![deny(missing_docs)] + +use crate::attr::NamespaceConstraint; +use crate::parser::{Combinator, Component, RelativeSelector, Selector, SelectorImpl}; + +/// A trait to visit selector properties. +/// +/// All the `visit_foo` methods return a boolean indicating whether the +/// traversal should continue or not. +pub trait SelectorVisitor: Sized { + /// The selector implementation this visitor wants to visit. + type Impl: SelectorImpl; + + /// Visit an attribute selector that may match (there are other selectors + /// that may never match, like those containing whitespace or the empty + /// string). + fn visit_attribute_selector( + &mut self, + _namespace: &NamespaceConstraint<&::NamespaceUrl>, + _local_name: &::LocalName, + _local_name_lower: &::LocalName, + ) -> bool { + true + } + + /// Visit a simple selector. + fn visit_simple_selector(&mut self, _: &Component) -> bool { + true + } + + /// Visit a nested relative selector list. The caller is responsible to call visit + /// into the internal selectors if / as needed. + /// + /// The default implementation skips it altogether. + fn visit_relative_selector_list(&mut self, _list: &[RelativeSelector]) -> bool { + true + } + + /// Visit a nested selector list. The caller is responsible to call visit + /// into the internal selectors if / as needed. + /// + /// The default implementation does this. + fn visit_selector_list( + &mut self, + _list_kind: SelectorListKind, + list: &[Selector], + ) -> bool { + for nested in list { + if !nested.visit(self) { + return false; + } + } + true + } + + /// Visits a complex selector. + /// + /// Gets the combinator to the right of the selector, or `None` if the + /// selector is the rightmost one. + fn visit_complex_selector(&mut self, _combinator_to_right: Option) -> bool { + true + } +} + +bitflags! { + /// The kinds of components the visitor is visiting the selector list of, if any + #[derive(Clone, Copy, Default)] + pub struct SelectorListKind: u8 { + /// The visitor is inside :not(..) + const NEGATION = 1 << 0; + /// The visitor is inside :is(..) + const IS = 1 << 1; + /// The visitor is inside :where(..) + const WHERE = 1 << 2; + /// The visitor is inside :nth-child(.. of ) or + /// :nth-last-child(.. of ) + const NTH_OF = 1 << 3; + /// The visitor is inside :has(..) + const HAS = 1 << 4; + } +} + +impl SelectorListKind { + /// Construct a SelectorListKind for the corresponding component. + pub fn from_component(component: &Component) -> Self { + match component { + Component::Negation(_) => SelectorListKind::NEGATION, + Component::Is(_) => SelectorListKind::IS, + Component::Where(_) => SelectorListKind::WHERE, + Component::NthOf(_) => SelectorListKind::NTH_OF, + _ => SelectorListKind::empty(), + } + } + + /// Whether the visitor is inside :not(..) + pub fn in_negation(&self) -> bool { + self.intersects(SelectorListKind::NEGATION) + } + + /// Whether the visitor is inside :is(..) + pub fn in_is(&self) -> bool { + self.intersects(SelectorListKind::IS) + } + + /// Whether the visitor is inside :where(..) + pub fn in_where(&self) -> bool { + self.intersects(SelectorListKind::WHERE) + } + + /// Whether the visitor is inside :nth-child(.. of ) or + /// :nth-last-child(.. of ) + pub fn in_nth_of(&self) -> bool { + self.intersects(SelectorListKind::NTH_OF) + } + + /// Whether the visitor is inside :has(..) + pub fn in_has(&self) -> bool { + self.intersects(SelectorListKind::HAS) + } + + /// Whether this nested selector is relevant for nth-of dependencies. + pub fn relevant_to_nth_of_dependencies(&self) -> bool { + // Order of nesting for `:has` and `:nth-child(.. of ..)` doesn't matter, because: + // * `:has(:nth-child(.. of ..))`: The location of the anchoring element is + // independent from where `:nth-child(.. of ..)` is applied. + // * `:nth-child(.. of :has(..))`: Invalidations inside `:has` must first use the + // `:has` machinary to find the anchor, then carry out the remaining invalidation. + self.in_nth_of() && !self.in_has() + } +} diff --git a/servo/components/servo_arc/Cargo.toml b/servo/components/servo_arc/Cargo.toml new file mode 100644 index 0000000000..03b1004bac --- /dev/null +++ b/servo/components/servo_arc/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "servo_arc" +version = "0.1.1" +authors = ["The Servo Project Developers"] +license = "MIT/Apache-2.0" +repository = "https://github.com/servo/servo" +description = "A fork of std::sync::Arc with some extra functionality and without weak references" + +[lib] +name = "servo_arc" +path = "lib.rs" + +[features] +gecko_refcount_logging = [] +servo = ["serde"] + +[dependencies] +serde = { version = "1.0", optional = true } +stable_deref_trait = "1.0.0" diff --git a/servo/components/servo_arc/lib.rs b/servo/components/servo_arc/lib.rs new file mode 100644 index 0000000000..1438ccebfd --- /dev/null +++ b/servo/components/servo_arc/lib.rs @@ -0,0 +1,1195 @@ +// Copyright 2012-2014 The Rust Project Developers. See the COPYRIGHT +// file at the top-level directory of this distribution and at +// http://rust-lang.org/COPYRIGHT. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +//! Fork of Arc for Servo. This has the following advantages over std::sync::Arc: +//! +//! * We don't waste storage on the weak reference count. +//! * We don't do extra RMU operations to handle the possibility of weak references. +//! * We can experiment with arena allocation (todo). +//! * We can add methods to support our custom use cases [1]. +//! * We have support for dynamically-sized types (see from_header_and_iter). +//! * We have support for thin arcs to unsized types (see ThinArc). +//! * We have support for references to static data, which don't do any +//! refcounting. +//! +//! [1]: https://bugzilla.mozilla.org/show_bug.cgi?id=1360883 + +// The semantics of `Arc` are already documented in the Rust docs, so we don't +// duplicate those here. +#![allow(missing_docs)] + +#[cfg(feature = "servo")] +extern crate serde; +extern crate stable_deref_trait; + +#[cfg(feature = "servo")] +use serde::{Deserialize, Serialize}; +use stable_deref_trait::{CloneStableDeref, StableDeref}; +use std::alloc::{self, Layout}; +use std::borrow; +use std::cmp::Ordering; +use std::convert::From; +use std::fmt; +use std::hash::{Hash, Hasher}; +use std::iter::{ExactSizeIterator, Iterator}; +use std::marker::PhantomData; +use std::mem::{self, align_of, size_of}; +use std::ops::{Deref, DerefMut}; +use std::os::raw::c_void; +use std::process; +use std::ptr; +use std::sync::atomic; +use std::sync::atomic::Ordering::{Acquire, Relaxed, Release}; +use std::{isize, usize}; + +/// A soft limit on the amount of references that may be made to an `Arc`. +/// +/// Going above this limit will abort your program (although not +/// necessarily) at _exactly_ `MAX_REFCOUNT + 1` references. +const MAX_REFCOUNT: usize = (isize::MAX) as usize; + +/// Special refcount value that means the data is not reference counted, +/// and that the `Arc` is really acting as a read-only static reference. +const STATIC_REFCOUNT: usize = usize::MAX; + +/// An atomically reference counted shared pointer +/// +/// See the documentation for [`Arc`] in the standard library. Unlike the +/// standard library `Arc`, this `Arc` does not support weak reference counting. +/// +/// See the discussion in https://github.com/rust-lang/rust/pull/60594 for the +/// usage of PhantomData. +/// +/// [`Arc`]: https://doc.rust-lang.org/stable/std/sync/struct.Arc.html +/// +/// cbindgen:derive-eq=false +/// cbindgen:derive-neq=false +#[repr(C)] +pub struct Arc { + p: ptr::NonNull>, + phantom: PhantomData, +} + +/// An `Arc` that is known to be uniquely owned +/// +/// When `Arc`s are constructed, they are known to be +/// uniquely owned. In such a case it is safe to mutate +/// the contents of the `Arc`. Normally, one would just handle +/// this by mutating the data on the stack before allocating the +/// `Arc`, however it's possible the data is large or unsized +/// and you need to heap-allocate it earlier in such a way +/// that it can be freely converted into a regular `Arc` once you're +/// done. +/// +/// `UniqueArc` exists for this purpose, when constructed it performs +/// the same allocations necessary for an `Arc`, however it allows mutable access. +/// Once the mutation is finished, you can call `.shareable()` and get a regular `Arc` +/// out of it. +/// +/// Ignore the doctest below there's no way to skip building with refcount +/// logging during doc tests (see rust-lang/rust#45599). +/// +/// ```rust,ignore +/// # use servo_arc::UniqueArc; +/// let data = [1, 2, 3, 4, 5]; +/// let mut x = UniqueArc::new(data); +/// x[4] = 7; // mutate! +/// let y = x.shareable(); // y is an Arc +/// ``` +pub struct UniqueArc(Arc); + +impl UniqueArc { + #[inline] + /// Construct a new UniqueArc + pub fn new(data: T) -> Self { + UniqueArc(Arc::new(data)) + } + + /// Construct an uninitialized arc + #[inline] + pub fn new_uninit() -> UniqueArc> { + unsafe { + let layout = Layout::new::>>(); + let ptr = alloc::alloc(layout); + let mut p = ptr::NonNull::new(ptr) + .unwrap_or_else(|| alloc::handle_alloc_error(layout)) + .cast::>>(); + ptr::write(&mut p.as_mut().count, atomic::AtomicUsize::new(1)); + + #[cfg(feature = "gecko_refcount_logging")] + { + NS_LogCtor(p.as_ptr() as *mut _, b"ServoArc\0".as_ptr() as *const _, 8) + } + + UniqueArc(Arc { + p, + phantom: PhantomData, + }) + } + } + + #[inline] + /// Convert to a shareable Arc once we're done mutating it + pub fn shareable(self) -> Arc { + self.0 + } +} + +impl UniqueArc> { + /// Convert to an initialized Arc. + #[inline] + pub unsafe fn assume_init(this: Self) -> UniqueArc { + UniqueArc(Arc { + p: mem::ManuallyDrop::new(this).0.p.cast(), + phantom: PhantomData, + }) + } +} + +impl Deref for UniqueArc { + type Target = T; + fn deref(&self) -> &T { + &*self.0 + } +} + +impl DerefMut for UniqueArc { + fn deref_mut(&mut self) -> &mut T { + // We know this to be uniquely owned + unsafe { &mut (*self.0.ptr()).data } + } +} + +unsafe impl Send for Arc {} +unsafe impl Sync for Arc {} + +/// The object allocated by an Arc +#[repr(C)] +struct ArcInner { + count: atomic::AtomicUsize, + data: T, +} + +unsafe impl Send for ArcInner {} +unsafe impl Sync for ArcInner {} + +/// Computes the offset of the data field within ArcInner. +fn data_offset() -> usize { + let size = size_of::>(); + let align = align_of::(); + // https://github.com/rust-lang/rust/blob/1.36.0/src/libcore/alloc.rs#L187-L207 + size.wrapping_add(align).wrapping_sub(1) & !align.wrapping_sub(1) +} + +impl Arc { + /// Construct an `Arc` + #[inline] + pub fn new(data: T) -> Self { + let ptr = Box::into_raw(Box::new(ArcInner { + count: atomic::AtomicUsize::new(1), + data, + })); + + #[cfg(feature = "gecko_refcount_logging")] + unsafe { + // FIXME(emilio): Would be so amazing to have + // std::intrinsics::type_name() around, so that we could also report + // a real size. + NS_LogCtor(ptr as *mut _, b"ServoArc\0".as_ptr() as *const _, 8); + } + + unsafe { + Arc { + p: ptr::NonNull::new_unchecked(ptr), + phantom: PhantomData, + } + } + } + + /// Construct an intentionally-leaked arc. + #[inline] + pub fn new_leaked(data: T) -> Self { + let arc = Self::new(data); + arc.mark_as_intentionally_leaked(); + arc + } + + /// Convert the Arc to a raw pointer, suitable for use across FFI + /// + /// Note: This returns a pointer to the data T, which is offset in the allocation. + #[inline] + pub fn into_raw(this: Self) -> *const T { + let ptr = unsafe { &((*this.ptr()).data) as *const _ }; + mem::forget(this); + ptr + } + + /// Reconstruct the Arc from a raw pointer obtained from into_raw() + /// + /// Note: This raw pointer will be offset in the allocation and must be preceded + /// by the atomic count. + #[inline] + pub unsafe fn from_raw(ptr: *const T) -> Self { + // To find the corresponding pointer to the `ArcInner` we need + // to subtract the offset of the `data` field from the pointer. + let ptr = (ptr as *const u8).sub(data_offset::()); + Arc { + p: ptr::NonNull::new_unchecked(ptr as *mut ArcInner), + phantom: PhantomData, + } + } + + /// Like from_raw, but returns an addrefed arc instead. + #[inline] + pub unsafe fn from_raw_addrefed(ptr: *const T) -> Self { + let arc = Self::from_raw(ptr); + mem::forget(arc.clone()); + arc + } + + /// Create a new static Arc (one that won't reference count the object) + /// and place it in the allocation provided by the specified `alloc` + /// function. + /// + /// `alloc` must return a pointer into a static allocation suitable for + /// storing data with the `Layout` passed into it. The pointer returned by + /// `alloc` will not be freed. + #[inline] + pub unsafe fn new_static(alloc: F, data: T) -> Arc + where + F: FnOnce(Layout) -> *mut u8, + { + let ptr = alloc(Layout::new::>()) as *mut ArcInner; + + let x = ArcInner { + count: atomic::AtomicUsize::new(STATIC_REFCOUNT), + data, + }; + + ptr::write(ptr, x); + + Arc { + p: ptr::NonNull::new_unchecked(ptr), + phantom: PhantomData, + } + } + + /// Produce a pointer to the data that can be converted back + /// to an Arc. This is basically an `&Arc`, without the extra indirection. + /// It has the benefits of an `&T` but also knows about the underlying refcount + /// and can be converted into more `Arc`s if necessary. + #[inline] + pub fn borrow_arc<'a>(&'a self) -> ArcBorrow<'a, T> { + ArcBorrow(&**self) + } + + /// Returns the address on the heap of the Arc itself -- not the T within it -- for memory + /// reporting. + /// + /// If this is a static reference, this returns null. + pub fn heap_ptr(&self) -> *const c_void { + if self.inner().count.load(Relaxed) == STATIC_REFCOUNT { + ptr::null() + } else { + self.p.as_ptr() as *const ArcInner as *const c_void + } + } +} + +impl Arc { + #[inline] + fn inner(&self) -> &ArcInner { + // This unsafety is ok because while this arc is alive we're guaranteed + // that the inner pointer is valid. Furthermore, we know that the + // `ArcInner` structure itself is `Sync` because the inner data is + // `Sync` as well, so we're ok loaning out an immutable pointer to these + // contents. + unsafe { &*self.ptr() } + } + + #[inline(always)] + fn record_drop(&self) { + #[cfg(feature = "gecko_refcount_logging")] + unsafe { + NS_LogDtor(self.ptr() as *mut _, b"ServoArc\0".as_ptr() as *const _, 8); + } + } + + /// Marks this `Arc` as intentionally leaked for the purposes of refcount + /// logging. + /// + /// It's a logic error to call this more than once, but it's not unsafe, as + /// it'd just report negative leaks. + #[inline(always)] + pub fn mark_as_intentionally_leaked(&self) { + self.record_drop(); + } + + // Non-inlined part of `drop`. Just invokes the destructor and calls the + // refcount logging machinery if enabled. + #[inline(never)] + unsafe fn drop_slow(&mut self) { + self.record_drop(); + let _ = Box::from_raw(self.ptr()); + } + + /// Test pointer equality between the two Arcs, i.e. they must be the _same_ + /// allocation + #[inline] + pub fn ptr_eq(this: &Self, other: &Self) -> bool { + this.ptr() as *const () == other.ptr() as *const () + } + + fn ptr(&self) -> *mut ArcInner { + self.p.as_ptr() + } + + /// Returns a raw ptr to the underlying allocation. + pub fn raw_ptr(&self) -> *const c_void { + self.p.as_ptr() as *const _ + } +} + +#[cfg(feature = "gecko_refcount_logging")] +extern "C" { + fn NS_LogCtor( + aPtr: *mut std::os::raw::c_void, + aTypeName: *const std::os::raw::c_char, + aSize: u32, + ); + fn NS_LogDtor( + aPtr: *mut std::os::raw::c_void, + aTypeName: *const std::os::raw::c_char, + aSize: u32, + ); +} + +impl Clone for Arc { + #[inline] + fn clone(&self) -> Self { + // NOTE(emilio): If you change anything here, make sure that the + // implementation in layout/style/ServoStyleConstsInlines.h matches! + // + // Using a relaxed ordering to check for STATIC_REFCOUNT is safe, since + // `count` never changes between STATIC_REFCOUNT and other values. + if self.inner().count.load(Relaxed) != STATIC_REFCOUNT { + // Using a relaxed ordering is alright here, as knowledge of the + // original reference prevents other threads from erroneously deleting + // the object. + // + // As explained in the [Boost documentation][1], Increasing the + // reference counter can always be done with memory_order_relaxed: New + // references to an object can only be formed from an existing + // reference, and passing an existing reference from one thread to + // another must already provide any required synchronization. + // + // [1]: (www.boost.org/doc/libs/1_55_0/doc/html/atomic/usage_examples.html) + let old_size = self.inner().count.fetch_add(1, Relaxed); + + // However we need to guard against massive refcounts in case someone + // is `mem::forget`ing Arcs. If we don't do this the count can overflow + // and users will use-after free. We racily saturate to `isize::MAX` on + // the assumption that there aren't ~2 billion threads incrementing + // the reference count at once. This branch will never be taken in + // any realistic program. + // + // We abort because such a program is incredibly degenerate, and we + // don't care to support it. + if old_size > MAX_REFCOUNT { + process::abort(); + } + } + + unsafe { + Arc { + p: ptr::NonNull::new_unchecked(self.ptr()), + phantom: PhantomData, + } + } + } +} + +impl Deref for Arc { + type Target = T; + + #[inline] + fn deref(&self) -> &T { + &self.inner().data + } +} + +impl Arc { + /// Makes a mutable reference to the `Arc`, cloning if necessary + /// + /// This is functionally equivalent to [`Arc::make_mut`][mm] from the standard library. + /// + /// If this `Arc` is uniquely owned, `make_mut()` will provide a mutable + /// reference to the contents. If not, `make_mut()` will create a _new_ `Arc` + /// with a copy of the contents, update `this` to point to it, and provide + /// a mutable reference to its contents. + /// + /// This is useful for implementing copy-on-write schemes where you wish to + /// avoid copying things if your `Arc` is not shared. + /// + /// [mm]: https://doc.rust-lang.org/stable/std/sync/struct.Arc.html#method.make_mut + #[inline] + pub fn make_mut(this: &mut Self) -> &mut T { + if !this.is_unique() { + // Another pointer exists; clone + *this = Arc::new((**this).clone()); + } + + unsafe { + // This unsafety is ok because we're guaranteed that the pointer + // returned is the *only* pointer that will ever be returned to T. Our + // reference count is guaranteed to be 1 at this point, and we required + // the Arc itself to be `mut`, so we're returning the only possible + // reference to the inner data. + &mut (*this.ptr()).data + } + } +} + +impl Arc { + /// Provides mutable access to the contents _if_ the `Arc` is uniquely owned. + #[inline] + pub fn get_mut(this: &mut Self) -> Option<&mut T> { + if this.is_unique() { + unsafe { + // See make_mut() for documentation of the threadsafety here. + Some(&mut (*this.ptr()).data) + } + } else { + None + } + } + + /// Whether or not the `Arc` is a static reference. + #[inline] + pub fn is_static(&self) -> bool { + // Using a relaxed ordering to check for STATIC_REFCOUNT is safe, since + // `count` never changes between STATIC_REFCOUNT and other values. + self.inner().count.load(Relaxed) == STATIC_REFCOUNT + } + + /// Whether or not the `Arc` is uniquely owned (is the refcount 1?) and not + /// a static reference. + #[inline] + pub fn is_unique(&self) -> bool { + // See the extensive discussion in [1] for why this needs to be Acquire. + // + // [1] https://github.com/servo/servo/issues/21186 + self.inner().count.load(Acquire) == 1 + } +} + +impl Drop for Arc { + #[inline] + fn drop(&mut self) { + // NOTE(emilio): If you change anything here, make sure that the + // implementation in layout/style/ServoStyleConstsInlines.h matches! + if self.is_static() { + return; + } + + // Because `fetch_sub` is already atomic, we do not need to synchronize + // with other threads unless we are going to delete the object. + if self.inner().count.fetch_sub(1, Release) != 1 { + return; + } + + // FIXME(bholley): Use the updated comment when [2] is merged. + // + // This load is needed to prevent reordering of use of the data and + // deletion of the data. Because it is marked `Release`, the decreasing + // of the reference count synchronizes with this `Acquire` load. This + // means that use of the data happens before decreasing the reference + // count, which happens before this load, which happens before the + // deletion of the data. + // + // As explained in the [Boost documentation][1], + // + // > It is important to enforce any possible access to the object in one + // > thread (through an existing reference) to *happen before* deleting + // > the object in a different thread. This is achieved by a "release" + // > operation after dropping a reference (any access to the object + // > through this reference must obviously happened before), and an + // > "acquire" operation before deleting the object. + // + // [1]: (www.boost.org/doc/libs/1_55_0/doc/html/atomic/usage_examples.html) + // [2]: https://github.com/rust-lang/rust/pull/41714 + self.inner().count.load(Acquire); + + unsafe { + self.drop_slow(); + } + } +} + +impl PartialEq for Arc { + fn eq(&self, other: &Arc) -> bool { + Self::ptr_eq(self, other) || *(*self) == *(*other) + } + + fn ne(&self, other: &Arc) -> bool { + !Self::ptr_eq(self, other) && *(*self) != *(*other) + } +} + +impl PartialOrd for Arc { + fn partial_cmp(&self, other: &Arc) -> Option { + (**self).partial_cmp(&**other) + } + + fn lt(&self, other: &Arc) -> bool { + *(*self) < *(*other) + } + + fn le(&self, other: &Arc) -> bool { + *(*self) <= *(*other) + } + + fn gt(&self, other: &Arc) -> bool { + *(*self) > *(*other) + } + + fn ge(&self, other: &Arc) -> bool { + *(*self) >= *(*other) + } +} +impl Ord for Arc { + fn cmp(&self, other: &Arc) -> Ordering { + (**self).cmp(&**other) + } +} +impl Eq for Arc {} + +impl fmt::Display for Arc { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Display::fmt(&**self, f) + } +} + +impl fmt::Debug for Arc { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Debug::fmt(&**self, f) + } +} + +impl fmt::Pointer for Arc { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Pointer::fmt(&self.ptr(), f) + } +} + +impl Default for Arc { + fn default() -> Arc { + Arc::new(Default::default()) + } +} + +impl Hash for Arc { + fn hash(&self, state: &mut H) { + (**self).hash(state) + } +} + +impl From for Arc { + #[inline] + fn from(t: T) -> Self { + Arc::new(t) + } +} + +impl borrow::Borrow for Arc { + #[inline] + fn borrow(&self) -> &T { + &**self + } +} + +impl AsRef for Arc { + #[inline] + fn as_ref(&self) -> &T { + &**self + } +} + +unsafe impl StableDeref for Arc {} +unsafe impl CloneStableDeref for Arc {} + +#[cfg(feature = "servo")] +impl<'de, T: Deserialize<'de>> Deserialize<'de> for Arc { + fn deserialize(deserializer: D) -> Result, D::Error> + where + D: ::serde::de::Deserializer<'de>, + { + T::deserialize(deserializer).map(Arc::new) + } +} + +#[cfg(feature = "servo")] +impl Serialize for Arc { + fn serialize(&self, serializer: S) -> Result + where + S: ::serde::ser::Serializer, + { + (**self).serialize(serializer) + } +} + +/// Structure to allow Arc-managing some fixed-sized data and a variably-sized +/// slice in a single allocation. +/// +/// cbindgen:derive-eq=false +/// cbindgen:derive-neq=false +#[derive(Debug, Eq)] +#[repr(C)] +pub struct HeaderSlice { + /// The fixed-sized data. + pub header: H, + + /// The length of the slice at our end. + len: usize, + + /// The dynamically-sized data. + data: [T; 0], +} + +impl PartialEq for HeaderSlice { + fn eq(&self, other: &Self) -> bool { + self.header == other.header && self.slice() == other.slice() + } +} + +impl Drop for HeaderSlice { + fn drop(&mut self) { + unsafe { + let mut ptr = self.data_mut(); + for _ in 0..self.len { + std::ptr::drop_in_place(ptr); + ptr = ptr.offset(1); + } + } + } +} + +impl HeaderSlice { + /// Returns the dynamically sized slice in this HeaderSlice. + #[inline(always)] + pub fn slice(&self) -> &[T] { + unsafe { std::slice::from_raw_parts(self.data(), self.len) } + } + + #[inline(always)] + fn data(&self) -> *const T { + std::ptr::addr_of!(self.data) as *const _ + } + + #[inline(always)] + fn data_mut(&mut self) -> *mut T { + std::ptr::addr_of_mut!(self.data) as *mut _ + } + + /// Returns the dynamically sized slice in this HeaderSlice. + #[inline(always)] + pub fn slice_mut(&mut self) -> &mut [T] { + unsafe { std::slice::from_raw_parts_mut(self.data_mut(), self.len) } + } + + /// Returns the len of the slice. + #[inline(always)] + pub fn len(&self) -> usize { + self.len + } +} + +#[inline(always)] +fn divide_rounding_up(dividend: usize, divisor: usize) -> usize { + (dividend + divisor - 1) / divisor +} + +impl Arc> { + /// Creates an Arc for a HeaderSlice using the given header struct and + /// iterator to generate the slice. + /// + /// `is_static` indicates whether to create a static Arc. + /// + /// `alloc` is used to get a pointer to the memory into which the + /// dynamically sized ArcInner> value will be + /// written. If `is_static` is true, then `alloc` must return a + /// pointer into some static memory allocation. If it is false, + /// then `alloc` must return an allocation that can be dellocated + /// by calling Box::from_raw::>> on it. + #[inline] + pub fn from_header_and_iter_alloc( + alloc: F, + header: H, + mut items: I, + num_items: usize, + is_static: bool, + ) -> Self + where + F: FnOnce(Layout) -> *mut u8, + I: Iterator, + { + assert_ne!(size_of::(), 0, "Need to think about ZST"); + + let size = size_of::>>() + size_of::() * num_items; + let inner_align = align_of::>>(); + debug_assert!(inner_align >= align_of::()); + + let ptr: *mut ArcInner>; + unsafe { + // Allocate the buffer. + let layout = if inner_align <= align_of::() { + Layout::from_size_align_unchecked(size, align_of::()) + } else if inner_align <= align_of::() { + // On 32-bit platforms may have 8 byte alignment while usize + // has 4 byte aligment. Use u64 to avoid over-alignment. + // This branch will compile away in optimized builds. + Layout::from_size_align_unchecked(size, align_of::()) + } else { + panic!("Over-aligned type not handled"); + }; + + let buffer = alloc(layout); + ptr = buffer as *mut ArcInner>; + + // Write the data. + // + // Note that any panics here (i.e. from the iterator) are safe, since + // we'll just leak the uninitialized memory. + let count = if is_static { + atomic::AtomicUsize::new(STATIC_REFCOUNT) + } else { + atomic::AtomicUsize::new(1) + }; + ptr::write(&mut ((*ptr).count), count); + ptr::write(&mut ((*ptr).data.header), header); + ptr::write(&mut ((*ptr).data.len), num_items); + if num_items != 0 { + let mut current = std::ptr::addr_of_mut!((*ptr).data.data) as *mut T; + for _ in 0..num_items { + ptr::write( + current, + items + .next() + .expect("ExactSizeIterator over-reported length"), + ); + current = current.offset(1); + } + // We should have consumed the buffer exactly, maybe accounting + // for some padding from the alignment. + debug_assert!( + (buffer.add(size) as usize - current as *mut u8 as usize) < inner_align + ); + } + assert!( + items.next().is_none(), + "ExactSizeIterator under-reported length" + ); + } + #[cfg(feature = "gecko_refcount_logging")] + unsafe { + if !is_static { + // FIXME(emilio): Would be so amazing to have + // std::intrinsics::type_name() around. + NS_LogCtor(ptr as *mut _, b"ServoArc\0".as_ptr() as *const _, 8) + } + } + + // Return the fat Arc. + assert_eq!( + size_of::(), + size_of::(), + "The Arc should be thin" + ); + unsafe { + Arc { + p: ptr::NonNull::new_unchecked(ptr), + phantom: PhantomData, + } + } + } + + /// Creates an Arc for a HeaderSlice using the given header struct and iterator to generate the + /// slice. Panics if num_items doesn't match the number of items. + #[inline] + pub fn from_header_and_iter_with_size(header: H, items: I, num_items: usize) -> Self + where + I: Iterator, + { + Arc::from_header_and_iter_alloc( + |layout| { + // align will only ever be align_of::() or align_of::() + let align = layout.align(); + unsafe { + if align == mem::align_of::() { + Self::allocate_buffer::(layout.size()) + } else { + assert_eq!(align, mem::align_of::()); + Self::allocate_buffer::(layout.size()) + } + } + }, + header, + items, + num_items, + /* is_static = */ false, + ) + } + + /// Creates an Arc for a HeaderSlice using the given header struct and + /// iterator to generate the slice. The resulting Arc will be fat. + #[inline] + pub fn from_header_and_iter(header: H, items: I) -> Self + where + I: Iterator + ExactSizeIterator, + { + let len = items.len(); + Self::from_header_and_iter_with_size(header, items, len) + } + + #[inline] + unsafe fn allocate_buffer(size: usize) -> *mut u8 { + // We use Vec because the underlying allocation machinery isn't + // available in stable Rust. To avoid alignment issues, we allocate + // words rather than bytes, rounding up to the nearest word size. + let words_to_allocate = divide_rounding_up(size, mem::size_of::()); + let mut vec = Vec::::with_capacity(words_to_allocate); + vec.set_len(words_to_allocate); + Box::into_raw(vec.into_boxed_slice()) as *mut W as *mut u8 + } +} + +/// This is functionally equivalent to Arc<(H, [T])> +/// +/// When you create an `Arc` containing a dynamically sized type like a slice, the `Arc` is +/// represented on the stack as a "fat pointer", where the length of the slice is stored alongside +/// the `Arc`'s pointer. In some situations you may wish to have a thin pointer instead, perhaps +/// for FFI compatibility or space efficiency. `ThinArc` solves this by storing the length in the +/// allocation itself, via `HeaderSlice`. +pub type ThinArc = Arc>; + +/// See `ArcUnion`. This is a version that works for `ThinArc`s. +pub type ThinArcUnion = ArcUnion, HeaderSlice>; + +impl UniqueArc> { + #[inline] + pub fn from_header_and_iter(header: H, items: I) -> Self + where + I: Iterator + ExactSizeIterator, + { + Self(Arc::from_header_and_iter(header, items)) + } + + #[inline] + pub fn from_header_and_iter_with_size(header: H, items: I, num_items: usize) -> Self + where + I: Iterator, + { + Self(Arc::from_header_and_iter_with_size( + header, items, num_items, + )) + } + + /// Returns a mutable reference to the header. + pub fn header_mut(&mut self) -> &mut H { + // We know this to be uniquely owned + unsafe { &mut (*self.0.ptr()).data.header } + } + + /// Returns a mutable reference to the slice. + pub fn data_mut(&mut self) -> &mut [T] { + // We know this to be uniquely owned + unsafe { (*self.0.ptr()).data.slice_mut() } + } +} + +/// A "borrowed `Arc`". This is a pointer to +/// a T that is known to have been allocated within an +/// `Arc`. +/// +/// This is equivalent in guarantees to `&Arc`, however it is +/// a bit more flexible. To obtain an `&Arc` you must have +/// an `Arc` instance somewhere pinned down until we're done with it. +/// It's also a direct pointer to `T`, so using this involves less pointer-chasing +/// +/// However, C++ code may hand us refcounted things as pointers to T directly, +/// so we have to conjure up a temporary `Arc` on the stack each time. +/// +/// `ArcBorrow` lets us deal with borrows of known-refcounted objects +/// without needing to worry about where the `Arc` is. +#[derive(Debug, Eq, PartialEq)] +pub struct ArcBorrow<'a, T: 'a>(&'a T); + +impl<'a, T> Copy for ArcBorrow<'a, T> {} +impl<'a, T> Clone for ArcBorrow<'a, T> { + #[inline] + fn clone(&self) -> Self { + *self + } +} + +impl<'a, T> ArcBorrow<'a, T> { + /// Clone this as an `Arc`. This bumps the refcount. + #[inline] + pub fn clone_arc(&self) -> Arc { + let arc = unsafe { Arc::from_raw(self.0) }; + // addref it! + mem::forget(arc.clone()); + arc + } + + /// For constructing from a reference known to be Arc-backed, + /// e.g. if we obtain such a reference over FFI + #[inline] + pub unsafe fn from_ref(r: &'a T) -> Self { + ArcBorrow(r) + } + + /// Compare two `ArcBorrow`s via pointer equality. Will only return + /// true if they come from the same allocation + pub fn ptr_eq(this: &Self, other: &Self) -> bool { + this.0 as *const T == other.0 as *const T + } + + /// Temporarily converts |self| into a bonafide Arc and exposes it to the + /// provided callback. The refcount is not modified. + #[inline] + pub fn with_arc(&self, f: F) -> U + where + F: FnOnce(&Arc) -> U, + T: 'static, + { + // Synthesize transient Arc, which never touches the refcount. + let transient = unsafe { mem::ManuallyDrop::new(Arc::from_raw(self.0)) }; + + // Expose the transient Arc to the callback, which may clone it if it wants. + let result = f(&transient); + + // Forward the result. + result + } + + /// Similar to deref, but uses the lifetime |a| rather than the lifetime of + /// self, which is incompatible with the signature of the Deref trait. + #[inline] + pub fn get(&self) -> &'a T { + self.0 + } +} + +impl<'a, T> Deref for ArcBorrow<'a, T> { + type Target = T; + + #[inline] + fn deref(&self) -> &T { + self.0 + } +} + +/// A tagged union that can represent `Arc` or `Arc` while only consuming a +/// single word. The type is also `NonNull`, and thus can be stored in an Option +/// without increasing size. +/// +/// This is functionally equivalent to +/// `enum ArcUnion { First(Arc), Second(Arc)` but only takes up +/// up a single word of stack space. +/// +/// This could probably be extended to support four types if necessary. +pub struct ArcUnion { + p: ptr::NonNull<()>, + phantom_a: PhantomData, + phantom_b: PhantomData, +} + +unsafe impl Send for ArcUnion {} +unsafe impl Sync for ArcUnion {} + +impl PartialEq for ArcUnion { + fn eq(&self, other: &Self) -> bool { + use crate::ArcUnionBorrow::*; + match (self.borrow(), other.borrow()) { + (First(x), First(y)) => x == y, + (Second(x), Second(y)) => x == y, + (_, _) => false, + } + } +} + +impl Eq for ArcUnion {} + +/// This represents a borrow of an `ArcUnion`. +#[derive(Debug)] +pub enum ArcUnionBorrow<'a, A: 'a, B: 'a> { + First(ArcBorrow<'a, A>), + Second(ArcBorrow<'a, B>), +} + +impl ArcUnion { + unsafe fn new(ptr: *mut ()) -> Self { + ArcUnion { + p: ptr::NonNull::new_unchecked(ptr), + phantom_a: PhantomData, + phantom_b: PhantomData, + } + } + + /// Returns true if the two values are pointer-equal. + #[inline] + pub fn ptr_eq(this: &Self, other: &Self) -> bool { + this.p == other.p + } + + #[inline] + pub fn ptr(&self) -> ptr::NonNull<()> { + self.p + } + + /// Returns an enum representing a borrow of either A or B. + #[inline] + pub fn borrow(&self) -> ArcUnionBorrow { + if self.is_first() { + let ptr = self.p.as_ptr() as *const ArcInner; + let borrow = unsafe { ArcBorrow::from_ref(&(*ptr).data) }; + ArcUnionBorrow::First(borrow) + } else { + let ptr = ((self.p.as_ptr() as usize) & !0x1) as *const ArcInner; + let borrow = unsafe { ArcBorrow::from_ref(&(*ptr).data) }; + ArcUnionBorrow::Second(borrow) + } + } + + /// Creates an `ArcUnion` from an instance of the first type. + pub fn from_first(other: Arc) -> Self { + let union = unsafe { Self::new(other.ptr() as *mut _) }; + mem::forget(other); + union + } + + /// Creates an `ArcUnion` from an instance of the second type. + pub fn from_second(other: Arc) -> Self { + let union = unsafe { Self::new(((other.ptr() as usize) | 0x1) as *mut _) }; + mem::forget(other); + union + } + + /// Returns true if this `ArcUnion` contains the first type. + pub fn is_first(&self) -> bool { + self.p.as_ptr() as usize & 0x1 == 0 + } + + /// Returns true if this `ArcUnion` contains the second type. + pub fn is_second(&self) -> bool { + !self.is_first() + } + + /// Returns a borrow of the first type if applicable, otherwise `None`. + pub fn as_first(&self) -> Option> { + match self.borrow() { + ArcUnionBorrow::First(x) => Some(x), + ArcUnionBorrow::Second(_) => None, + } + } + + /// Returns a borrow of the second type if applicable, otherwise None. + pub fn as_second(&self) -> Option> { + match self.borrow() { + ArcUnionBorrow::First(_) => None, + ArcUnionBorrow::Second(x) => Some(x), + } + } +} + +impl Clone for ArcUnion { + fn clone(&self) -> Self { + match self.borrow() { + ArcUnionBorrow::First(x) => ArcUnion::from_first(x.clone_arc()), + ArcUnionBorrow::Second(x) => ArcUnion::from_second(x.clone_arc()), + } + } +} + +impl Drop for ArcUnion { + fn drop(&mut self) { + match self.borrow() { + ArcUnionBorrow::First(x) => unsafe { + let _ = Arc::from_raw(&*x); + }, + ArcUnionBorrow::Second(x) => unsafe { + let _ = Arc::from_raw(&*x); + }, + } + } +} + +impl fmt::Debug for ArcUnion { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Debug::fmt(&self.borrow(), f) + } +} + +#[cfg(test)] +mod tests { + use super::{Arc, ThinArc}; + use std::clone::Clone; + use std::ops::Drop; + use std::sync::atomic; + use std::sync::atomic::Ordering::{Acquire, SeqCst}; + + #[derive(PartialEq)] + struct Canary(*mut atomic::AtomicUsize); + + impl Drop for Canary { + fn drop(&mut self) { + unsafe { + (*self.0).fetch_add(1, SeqCst); + } + } + } + + #[test] + fn empty_thin() { + let x = Arc::from_header_and_iter(100u32, std::iter::empty::()); + assert_eq!(x.header, 100); + assert!(x.slice().is_empty()); + } + + #[test] + fn thin_assert_padding() { + #[derive(Clone, Default)] + #[repr(C)] + struct Padded { + i: u16, + } + + // The header will have more alignment than `Padded` + let items = vec![Padded { i: 0xdead }, Padded { i: 0xbeef }]; + let a = ThinArc::from_header_and_iter(0i32, items.into_iter()); + assert_eq!(a.len(), 2); + assert_eq!(a.slice()[0].i, 0xdead); + assert_eq!(a.slice()[1].i, 0xbeef); + } + + #[test] + fn slices_and_thin() { + let mut canary = atomic::AtomicUsize::new(0); + let c = Canary(&mut canary as *mut atomic::AtomicUsize); + let v = vec![5, 6]; + { + let x = Arc::from_header_and_iter(c, v.into_iter()); + let _ = x.clone(); + let _ = x == x; + } + assert_eq!(canary.load(Acquire), 1); + } +} diff --git a/servo/components/style/Cargo.toml b/servo/components/style/Cargo.toml new file mode 100644 index 0000000000..acf1bcf6fe --- /dev/null +++ b/servo/components/style/Cargo.toml @@ -0,0 +1,91 @@ +[package] +name = "style" +version = "0.0.1" +authors = ["The Servo Project Developers"] +license = "MPL-2.0" +publish = false + +build = "build.rs" +edition = "2018" + +# https://github.com/rust-lang/cargo/issues/3544 +links = "servo_style_crate" + +[lib] +name = "style" +path = "lib.rs" +doctest = false + +[features] +gecko = ["nsstring", "serde", "style_traits/gecko", "bindgen", "regex", "toml", "mozbuild"] +servo = ["serde", "style_traits/servo", "servo_atoms", "servo_config", "html5ever", + "cssparser/serde", "encoding_rs", "malloc_size_of/servo", "arrayvec/use_union", + "servo_url", "string_cache", "to_shmem/servo", "servo_arc/servo"] +servo-layout-2013 = [] +servo-layout-2020 = [] +gecko_debug = [] +gecko_refcount_logging = [] + +[dependencies] +app_units = "0.7" +arrayvec = "0.7" +atomic_refcell = "0.1" +bitflags = "2" +byteorder = "1.0" +cssparser = "0.33" +derive_more = { version = "0.99", default-features = false, features = ["add", "add_assign", "deref", "deref_mut", "from"] } +dom = { path = "../../../dom/base/rust" } +new_debug_unreachable = "1.0" +encoding_rs = {version = "0.8", optional = true} +euclid = "0.22" +fxhash = "0.2" +html5ever = {version = "0.24", optional = true} +icu_segmenter = { version = "1.4", default-features = false, features = ["auto", "compiled_data"] } +indexmap = {version = "1.0", features = ["std"]} +itertools = "0.10" +itoa = "1.0" +lazy_static = "1" +log = "0.4" +malloc_size_of = { path = "../malloc_size_of" } +malloc_size_of_derive = { path = "../../../xpcom/rust/malloc_size_of_derive" } +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.4" +owning_ref = "0.4" +parking_lot = "0.12" +precomputed-hash = "0.1.1" +rayon = "1" +rayon-core = "1" +selectors = { path = "../selectors" } +serde = {version = "1.0", optional = true, features = ["derive"]} +servo_arc = { path = "../servo_arc" } +servo_atoms = {path = "../atoms", optional = true} +servo_config = {path = "../config", optional = true} +smallbitvec = "2.3.0" +smallvec = "1.0" +static_assertions = "1.1" +static_prefs = { path = "../../../modules/libpref/init/static_prefs" } +string_cache = { version = "0.8", optional = true } +style_derive = {path = "../style_derive"} +style_traits = {path = "../style_traits"} +servo_url = {path = "../url", optional = true} +to_shmem = {path = "../to_shmem"} +to_shmem_derive = {path = "../to_shmem_derive"} +time = "0.1" +thin-vec = { version = "0.2.1", features = ["gecko-ffi"] } +uluru = "3.0" +unicode-bidi = { version = "0.3", default-features = false } +void = "1.0.2" +gecko-profiler = { path = "../../../tools/profiler/rust-api" } + +[build-dependencies] +lazy_static = "1" +log = { version = "0.4", features = ["std"] } +bindgen = {version = "0.69", optional = true, default-features = false} +regex = {version = "1.0", optional = true, default-features = false, features = ["perf", "std"]} +walkdir = "2.1.4" +toml = {version = "0.5", optional = true, default-features = false} +mozbuild = {version = "0.1", optional = true} diff --git a/servo/components/style/README.md b/servo/components/style/README.md new file mode 100644 index 0000000000..96457e1b30 --- /dev/null +++ b/servo/components/style/README.md @@ -0,0 +1,6 @@ +servo-style +=========== + +Style system for Servo, using [rust-cssparser](https://github.com/servo/rust-cssparser) for parsing. + + * [Documentation](https://github.com/servo/servo/blob/master/docs/components/style.md). diff --git a/servo/components/style/animation.rs b/servo/components/style/animation.rs new file mode 100644 index 0000000000..b865120aba --- /dev/null +++ b/servo/components/style/animation.rs @@ -0,0 +1,1415 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! CSS transitions and animations. + +// NOTE(emilio): This code isn't really executed in Gecko, but we don't want to +// compile it out so that people remember it exists. + +use crate::context::{CascadeInputs, SharedStyleContext}; +use crate::dom::{OpaqueNode, TDocument, TElement, TNode}; +use crate::properties::animated_properties::{AnimationValue, AnimationValueMap}; +use crate::properties::longhands::animation_direction::computed_value::single_value::T as AnimationDirection; +use crate::properties::longhands::animation_fill_mode::computed_value::single_value::T as AnimationFillMode; +use crate::properties::longhands::animation_play_state::computed_value::single_value::T as AnimationPlayState; +use crate::properties::AnimationDeclarations; +use crate::properties::{ + ComputedValues, Importance, LonghandId, PropertyDeclarationBlock, PropertyDeclarationId, + PropertyDeclarationIdSet, +}; +use crate::rule_tree::CascadeLevel; +use crate::selector_parser::PseudoElement; +use crate::shared_lock::{Locked, SharedRwLock}; +use crate::style_resolver::StyleResolverForElement; +use crate::stylesheets::keyframes_rule::{KeyframesAnimation, KeyframesStep, KeyframesStepValue}; +use crate::stylesheets::layer_rule::LayerOrder; +use crate::values::animated::{Animate, Procedure}; +use crate::values::computed::{Time, TimingFunction}; +use crate::values::generics::easing::BeforeFlag; +use crate::Atom; +use fxhash::FxHashMap; +use parking_lot::RwLock; +use servo_arc::Arc; +use std::fmt; + +/// Represents an animation for a given property. +#[derive(Clone, Debug, MallocSizeOf)] +pub struct PropertyAnimation { + /// The value we are animating from. + from: AnimationValue, + + /// The value we are animating to. + to: AnimationValue, + + /// The timing function of this `PropertyAnimation`. + timing_function: TimingFunction, + + /// The duration of this `PropertyAnimation` in seconds. + pub duration: f64, +} + +impl PropertyAnimation { + /// Returns the given property longhand id. + pub fn property_id(&self) -> PropertyDeclarationId { + debug_assert_eq!(self.from.id(), self.to.id()); + self.from.id() + } + + fn from_property_declaration( + property_declaration: &PropertyDeclarationId, + 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 property_declaration = property_declaration.to_physical(new_style.writing_mode); + let from = AnimationValue::from_computed_values(property_declaration, old_style)?; + let to = AnimationValue::from_computed_values(property_declaration, new_style)?; + let duration = duration.seconds() as f64; + + if from == to || duration == 0.0 { + return None; + } + + Some(PropertyAnimation { + from, + to, + timing_function, + duration, + }) + } + + /// The output of the timing function given the progress ration of this animation. + fn timing_function_output(&self, progress: f64) -> f64 { + let epsilon = 1. / (200. * self.duration); + // FIXME: Need to set the before flag correctly. + // In order to get the before flag, we have to know the current animation phase + // and whether the iteration is reversed. For now, we skip this calculation + // by treating as if the flag is unset at all times. + // https://drafts.csswg.org/css-easing/#step-timing-function-algo + self.timing_function + .calculate_output(progress, BeforeFlag::Unset, epsilon) + } + + /// Update the given animation at a given point of progress. + fn calculate_value(&self, progress: f64) -> Result { + let procedure = Procedure::Interpolate { + progress: self.timing_function_output(progress), + }; + self.from.animate(&self.to, procedure) + } +} + +/// This structure represents the state of an animation. +#[derive(Clone, Debug, MallocSizeOf, PartialEq)] +pub enum AnimationState { + /// The animation has been created, but is not running yet. This state + /// is also used when an animation is still in the first delay phase. + Pending, + /// This animation is currently running. + Running, + /// This animation is paused. The inner field is the percentage of progress + /// when it was paused, from 0 to 1. + Paused(f64), + /// This animation has finished. + Finished, + /// This animation has been canceled. + Canceled, +} + +impl AnimationState { + /// Whether or not this state requires its owning animation to be ticked. + fn needs_to_be_ticked(&self) -> bool { + *self == AnimationState::Running || *self == AnimationState::Pending + } +} + +/// This structure represents a keyframes animation current iteration state. +/// +/// If the iteration count is infinite, there's no other state, otherwise we +/// have to keep track the current iteration and the max iteration count. +#[derive(Clone, Debug, MallocSizeOf)] +pub enum KeyframesIterationState { + /// Infinite iterations with the current iteration count. + Infinite(f64), + /// Current and max iterations. + Finite(f64, f64), +} + +/// A temporary data structure used when calculating ComputedKeyframes for an +/// animation. This data structure is used to collapse information for steps +/// which may be spread across multiple keyframe declarations into a single +/// instance per `start_percentage`. +struct IntermediateComputedKeyframe { + declarations: PropertyDeclarationBlock, + timing_function: Option, + start_percentage: f32, +} + +impl IntermediateComputedKeyframe { + fn new(start_percentage: f32) -> Self { + IntermediateComputedKeyframe { + declarations: PropertyDeclarationBlock::new(), + timing_function: None, + start_percentage, + } + } + + /// Walk through all keyframe declarations and combine all declarations with the + /// same `start_percentage` into individual `IntermediateComputedKeyframe`s. + fn generate_for_keyframes( + animation: &KeyframesAnimation, + context: &SharedStyleContext, + base_style: &ComputedValues, + ) -> Vec { + let mut intermediate_steps: Vec = Vec::with_capacity(animation.steps.len()); + let mut current_step = IntermediateComputedKeyframe::new(0.); + for step in animation.steps.iter() { + let start_percentage = step.start_percentage.0; + if start_percentage != current_step.start_percentage { + let new_step = IntermediateComputedKeyframe::new(start_percentage); + intermediate_steps.push(std::mem::replace(&mut current_step, new_step)); + } + + current_step.update_from_step(step, context, base_style); + } + intermediate_steps.push(current_step); + + // We should always have a first and a last step, even if these are just + // generated by KeyframesStepValue::ComputedValues. + debug_assert!(intermediate_steps.first().unwrap().start_percentage == 0.); + debug_assert!(intermediate_steps.last().unwrap().start_percentage == 1.); + + intermediate_steps + } + + fn update_from_step( + &mut self, + step: &KeyframesStep, + context: &SharedStyleContext, + base_style: &ComputedValues, + ) { + // Each keyframe declaration may optionally specify a timing function, falling + // back to the one defined global for the animation. + let guard = &context.guards.author; + if let Some(timing_function) = step.get_animation_timing_function(&guard) { + self.timing_function = Some(timing_function.to_computed_value_without_context()); + } + + let block = match step.value { + KeyframesStepValue::ComputedValues => return, + KeyframesStepValue::Declarations { ref block } => block, + }; + + // Filter out !important, non-animatable properties, and the + // 'display' property (which is only animatable from SMIL). + let guard = block.read_with(&guard); + for declaration in guard.normal_declaration_iter() { + if let PropertyDeclarationId::Longhand(id) = declaration.id() { + if id == LonghandId::Display { + continue; + } + + if !id.is_animatable() { + continue; + } + } + + self.declarations.push( + declaration.to_physical(base_style.writing_mode), + Importance::Normal, + ); + } + } + + fn resolve_style( + self, + element: E, + context: &SharedStyleContext, + base_style: &Arc, + resolver: &mut StyleResolverForElement, + ) -> Arc + where + E: TElement, + { + if !self.declarations.any_normal() { + return base_style.clone(); + } + + let document = element.as_node().owner_doc(); + let locked_block = Arc::new(document.shared_lock().wrap(self.declarations)); + let mut important_rules_changed = false; + let rule_node = base_style.rules().clone(); + let new_node = context.stylist.rule_tree().update_rule_at_level( + CascadeLevel::Animations, + LayerOrder::root(), + Some(locked_block.borrow_arc()), + &rule_node, + &context.guards, + &mut important_rules_changed, + ); + + if new_node.is_none() { + return base_style.clone(); + } + + let inputs = CascadeInputs { + rules: new_node, + visited_rules: base_style.visited_rules().cloned(), + flags: base_style.flags.for_cascade_inputs(), + }; + resolver + .cascade_style_and_visited_with_default_parents(inputs) + .0 + } +} + +/// A single computed keyframe for a CSS Animation. +#[derive(Clone, MallocSizeOf)] +struct ComputedKeyframe { + /// The timing function to use for transitions between this step + /// and the next one. + timing_function: TimingFunction, + + /// The starting percentage (a number between 0 and 1) which represents + /// at what point in an animation iteration this step is. + start_percentage: f32, + + /// The animation values to transition to and from when processing this + /// keyframe animation step. + values: Vec, +} + +impl ComputedKeyframe { + fn generate_for_keyframes( + element: E, + animation: &KeyframesAnimation, + context: &SharedStyleContext, + base_style: &Arc, + default_timing_function: TimingFunction, + resolver: &mut StyleResolverForElement, + ) -> Vec + where + E: TElement, + { + let mut animating_properties = PropertyDeclarationIdSet::default(); + for property in animation.properties_changed.iter() { + debug_assert!(property.is_animatable()); + animating_properties.insert(property.to_physical(base_style.writing_mode)); + } + + let animation_values_from_style: Vec = animating_properties + .iter() + .map(|property| { + AnimationValue::from_computed_values(property, &**base_style) + .expect("Unexpected non-animatable property.") + }) + .collect(); + + let intermediate_steps = + IntermediateComputedKeyframe::generate_for_keyframes(animation, context, base_style); + + let mut computed_steps: Vec = Vec::with_capacity(intermediate_steps.len()); + for (step_index, step) in intermediate_steps.into_iter().enumerate() { + let start_percentage = step.start_percentage; + let properties_changed_in_step = step.declarations.property_ids().clone(); + let step_timing_function = step.timing_function.clone(); + let step_style = step.resolve_style(element, context, base_style, resolver); + let timing_function = + step_timing_function.unwrap_or_else(|| default_timing_function.clone()); + + let values = { + // If a value is not set in a property declaration we use the value from + // the style for the first and last keyframe. For intermediate ones, we + // use the value from the previous keyframe. + // + // TODO(mrobinson): According to the spec, we should use an interpolated + // value for properties missing from keyframe declarations. + let default_values = if start_percentage == 0. || start_percentage == 1.0 { + &animation_values_from_style + } else { + debug_assert!(step_index != 0); + &computed_steps[step_index - 1].values + }; + + // For each property that is animating, pull the value from the resolved + // style for this step if it's in one of the declarations. Otherwise, we + // use the default value from the set we calculated above. + animating_properties + .iter() + .zip(default_values.iter()) + .map(|(property_declaration, default_value)| { + if properties_changed_in_step.contains(property_declaration) { + AnimationValue::from_computed_values(property_declaration, &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: PropertyDeclarationIdSet, + + /// The computed style for each keyframe of this animation. + computed_steps: Vec, + + /// The time this animation started at, which is the current value of the animation + /// timeline when this animation was created plus any animation delay. + pub started_at: f64, + + /// The duration of this animation. + pub duration: f64, + + /// The delay of the animation. + pub delay: f64, + + /// The `animation-fill-mode` property of this animation. + pub fill_mode: AnimationFillMode, + + /// The current iteration state for the animation. + pub iteration_state: KeyframesIterationState, + + /// Whether this animation is paused. + pub state: AnimationState, + + /// The declared animation direction of this animation. + pub direction: AnimationDirection, + + /// The current animation direction. This can only be `normal` or `reverse`. + pub current_direction: AnimationDirection, + + /// The original cascade style, needed to compute the generated keyframes of + /// the animation. + #[ignore_malloc_size_of = "ComputedValues"] + pub cascade_style: Arc, + + /// Whether or not this animation is new and or has already been tracked + /// by the script thread. + pub is_new: bool, +} + +impl Animation { + /// Whether or not this animation is cancelled by changes from a new style. + fn is_cancelled_in_new_style(&self, new_style: &Arc) -> bool { + let new_ui = new_style.get_ui(); + let index = new_ui + .animation_name_iter() + .position(|animation_name| Some(&self.name) == animation_name.as_atom()); + let index = match index { + Some(index) => index, + None => return true, + }; + + new_ui.animation_duration_mod(index).seconds() == 0. + } + + /// Given the current time, advances this animation to the next iteration, + /// updates times, and then toggles the direction if appropriate. Otherwise + /// does nothing. Returns true if this animation has iterated. + pub fn iterate_if_necessary(&mut self, time: f64) -> bool { + if !self.iteration_over(time) { + return false; + } + + // Only iterate animations that are currently running. + if self.state != AnimationState::Running { + return false; + } + + if self.on_last_iteration() { + return false; + } + + self.iterate(); + true + } + + fn iterate(&mut self) { + debug_assert!(!self.on_last_iteration()); + + if let KeyframesIterationState::Finite(ref mut current, max) = self.iteration_state { + *current = (*current + 1.).min(max); + } + + if let AnimationState::Paused(ref mut progress) = self.state { + debug_assert!(*progress > 1.); + *progress -= 1.; + } + + // Update the next iteration direction if applicable. + self.started_at += self.duration; + match self.direction { + AnimationDirection::Alternate | AnimationDirection::AlternateReverse => { + self.current_direction = match self.current_direction { + AnimationDirection::Normal => AnimationDirection::Reverse, + AnimationDirection::Reverse => AnimationDirection::Normal, + _ => unreachable!(), + }; + }, + _ => {}, + } + } + + /// A number (> 0 and <= 1) which represents the fraction of a full iteration + /// that the current iteration of the animation lasts. This will be less than 1 + /// if the current iteration is the fractional remainder of a non-integral + /// iteration count. + pub fn current_iteration_end_progress(&self) -> f64 { + match self.iteration_state { + KeyframesIterationState::Finite(current, max) => (max - current).min(1.), + KeyframesIterationState::Infinite(_) => 1., + } + } + + /// The duration of the current iteration of this animation which may be less + /// than the animation duration if it has a non-integral iteration count. + pub fn current_iteration_duration(&self) -> f64 { + self.current_iteration_end_progress() * self.duration + } + + /// Whether or not the current iteration is over. Note that this method assumes that + /// the animation is still running. + fn iteration_over(&self, time: f64) -> bool { + time > (self.started_at + self.current_iteration_duration()) + } + + /// Assuming this animation is running, whether or not it is on the last iteration. + fn on_last_iteration(&self) -> bool { + match self.iteration_state { + KeyframesIterationState::Finite(current, max) => current >= (max - 1.), + KeyframesIterationState::Infinite(_) => false, + } + } + + /// Whether or not this animation has finished at the provided time. This does + /// not take into account canceling i.e. when an animation or transition is + /// canceled due to changes in the style. + pub fn has_ended(&self, time: f64) -> bool { + if !self.on_last_iteration() { + return false; + } + + let progress = match self.state { + AnimationState::Finished => return true, + AnimationState::Paused(progress) => progress, + AnimationState::Running => (time - self.started_at) / self.duration, + AnimationState::Pending | AnimationState::Canceled => return false, + }; + + progress >= self.current_iteration_end_progress() + } + + /// Updates the appropiate state from other animation. + /// + /// This happens when an animation is re-submitted to layout, presumably + /// because of an state change. + /// + /// There are some bits of state we can't just replace, over all taking in + /// account times, so here's that logic. + pub fn update_from_other(&mut self, other: &Self, now: f64) { + use self::AnimationState::*; + + debug!( + "KeyframesAnimationState::update_from_other({:?}, {:?})", + self, other + ); + + // NB: We shall not touch the started_at field, since we don't want to + // restart the animation. + let old_started_at = self.started_at; + let old_duration = self.duration; + let old_direction = self.current_direction; + let old_state = self.state.clone(); + let old_iteration_state = self.iteration_state.clone(); + + *self = other.clone(); + + self.started_at = old_started_at; + self.current_direction = old_direction; + + // Don't update the iteration count, just the iteration limit. + // TODO: see how changing the limit affects rendering in other browsers. + // We might need to keep the iteration count even when it's infinite. + match (&mut self.iteration_state, old_iteration_state) { + ( + &mut KeyframesIterationState::Finite(ref mut iters, _), + KeyframesIterationState::Finite(old_iters, _), + ) => *iters = old_iters, + _ => {}, + } + + // Don't pause or restart animations that should remain finished. + // We call mem::replace because `has_ended(...)` looks at `Animation::state`. + let new_state = std::mem::replace(&mut self.state, Running); + if old_state == Finished && self.has_ended(now) { + self.state = Finished; + } else { + self.state = new_state; + } + + // If we're unpausing the animation, fake the start time so we seem to + // restore it. + // + // If the animation keeps paused, keep the old value. + // + // If we're pausing the animation, compute the progress value. + match (&mut self.state, &old_state) { + (&mut Pending, &Paused(progress)) => { + self.started_at = now - (self.duration * progress); + }, + (&mut Paused(ref mut new), &Paused(old)) => *new = old, + (&mut Paused(ref mut progress), &Running) => { + *progress = (now - old_started_at) / old_duration + }, + _ => {}, + } + + // Try to detect when we should skip straight to the running phase to + // avoid sending multiple animationstart events. + if self.state == Pending && self.started_at <= now && old_state != Pending { + self.state = Running; + } + } + + /// Fill in an `AnimationValueMap` with values calculated from this animation at + /// the given time value. + fn get_property_declaration_at_time(&self, now: f64, map: &mut AnimationValueMap) { + debug_assert!(!self.computed_steps.is_empty()); + + let total_progress = match self.state { + AnimationState::Running | AnimationState::Pending | AnimationState::Finished => { + (now - self.started_at) / self.duration + }, + AnimationState::Paused(progress) => progress, + AnimationState::Canceled => return, + }; + + if total_progress < 0. && + self.fill_mode != AnimationFillMode::Backwards && + self.fill_mode != AnimationFillMode::Both + { + return; + } + if self.has_ended(now) && + self.fill_mode != AnimationFillMode::Forwards && + self.fill_mode != AnimationFillMode::Both + { + return; + } + let total_progress = total_progress + .min(self.current_iteration_end_progress()) + .max(0.0); + + // Get the indices of the previous (from) keyframe and the next (to) keyframe. + let next_keyframe_index; + let prev_keyframe_index; + let num_steps = self.computed_steps.len(); + match self.current_direction { + AnimationDirection::Normal => { + next_keyframe_index = self + .computed_steps + .iter() + .position(|step| total_progress as f32 <= step.start_percentage); + prev_keyframe_index = next_keyframe_index + .and_then(|pos| if pos != 0 { Some(pos - 1) } else { None }) + .unwrap_or(0); + }, + AnimationDirection::Reverse => { + next_keyframe_index = self + .computed_steps + .iter() + .rev() + .position(|step| total_progress as f32 <= 1. - step.start_percentage) + .map(|pos| num_steps - pos - 1); + prev_keyframe_index = next_keyframe_index + .and_then(|pos| { + if pos != num_steps - 1 { + Some(pos + 1) + } else { + None + } + }) + .unwrap_or(num_steps - 1) + }, + _ => unreachable!(), + } + + debug!( + "Animation::get_property_declaration_at_time: keyframe from {:?} to {:?}", + prev_keyframe_index, next_keyframe_index + ); + + let prev_keyframe = &self.computed_steps[prev_keyframe_index]; + let next_keyframe = match next_keyframe_index { + Some(index) => &self.computed_steps[index], + None => return, + }; + + // If we only need to take into account one keyframe, then exit early + // in order to avoid doing more work. + let mut add_declarations_to_map = |keyframe: &ComputedKeyframe| { + for value in keyframe.values.iter() { + map.insert(value.id().to_owned(), value.clone()); + } + }; + if total_progress <= 0.0 { + add_declarations_to_map(&prev_keyframe); + return; + } + if total_progress >= 1.0 { + add_declarations_to_map(&next_keyframe); + return; + } + + let percentage_between_keyframes = + (next_keyframe.start_percentage - prev_keyframe.start_percentage).abs() as f64; + let duration_between_keyframes = percentage_between_keyframes * self.duration; + let direction_aware_prev_keyframe_start_percentage = match self.current_direction { + AnimationDirection::Normal => prev_keyframe.start_percentage as f64, + AnimationDirection::Reverse => 1. - prev_keyframe.start_percentage as f64, + _ => unreachable!(), + }; + let progress_between_keyframes = (total_progress - + direction_aware_prev_keyframe_start_percentage) / + percentage_between_keyframes; + + for (from, to) in prev_keyframe.values.iter().zip(next_keyframe.values.iter()) { + let animation = PropertyAnimation { + from: from.clone(), + to: to.clone(), + timing_function: prev_keyframe.timing_function.clone(), + duration: duration_between_keyframes as f64, + }; + + if let Ok(value) = animation.calculate_value(progress_between_keyframes) { + map.insert(value.id().to_owned(), 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, + property_declaration_id: &PropertyDeclarationId, + index: usize, + old_style: &ComputedValues, + new_style: &Arc, + ) { + let style = new_style.get_ui(); + let timing_function = style.transition_timing_function_mod(index); + let duration = style.transition_duration_mod(index); + let delay = style.transition_delay_mod(index).seconds() as f64; + let now = context.current_time_for_animations; + + // Only start a new transition if the style actually changes between + // the old style and the new style. + let property_animation = match PropertyAnimation::from_property_declaration( + property_declaration_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() == *property_declaration_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().to_owned(), 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, +) -> PropertyDeclarationIdSet { + let mut properties_that_transition = PropertyDeclarationIdSet::default(); + for transition in new_style.transition_properties() { + let physical_property = PropertyDeclarationId::Longhand( + transition.longhand_id.to_physical(new_style.writing_mode), + ); + if properties_that_transition.contains(physical_property) { + continue; + } + + properties_that_transition.insert(physical_property); + animation_state.start_transition_if_applicable( + context, + &physical_property, + transition.index, + old_style, + new_style, + ); + } + + properties_that_transition +} + +/// Triggers animations for a given node looking at the animation property +/// values. +pub fn maybe_start_animations( + element: E, + context: &SharedStyleContext, + new_style: &Arc, + animation_state: &mut ElementAnimationSet, + resolver: &mut StyleResolverForElement, +) where + E: TElement, +{ + let style = new_style.get_ui(); + for (i, name) in style.animation_name_iter().enumerate() { + let name = match name.as_atom() { + Some(atom) => atom, + None => continue, + }; + + debug!("maybe_start_animations: name={}", name); + let duration = style.animation_duration_mod(i).seconds() as f64; + if duration == 0. { + continue; + } + + let keyframe_animation = match context.stylist.get_animation(name, element) { + Some(animation) => animation, + None => continue, + }; + + debug!("maybe_start_animations: animation {} found", name); + + // If this animation doesn't have any keyframe, we can just continue + // without submitting it to the compositor, since both the first and + // the second keyframes would be synthetised from the computed + // values. + if keyframe_animation.steps.is_empty() { + continue; + } + + // NB: This delay may be negative, meaning that the animation may be created + // in a state where we have advanced one or more iterations or even that the + // animation begins in a finished state. + let delay = style.animation_delay_mod(i).seconds(); + + let iteration_count = style.animation_iteration_count_mod(i); + let iteration_state = if iteration_count.0.is_infinite() { + KeyframesIterationState::Infinite(0.0) + } else { + KeyframesIterationState::Finite(0.0, iteration_count.0 as f64) + }; + + let animation_direction = style.animation_direction_mod(i); + + let initial_direction = match animation_direction { + AnimationDirection::Normal | AnimationDirection::Alternate => { + AnimationDirection::Normal + }, + AnimationDirection::Reverse | AnimationDirection::AlternateReverse => { + AnimationDirection::Reverse + }, + }; + + let now = context.current_time_for_animations; + let started_at = now + delay as f64; + let mut starting_progress = (now - started_at) / duration; + let state = match style.animation_play_state_mod(i) { + AnimationPlayState::Paused => AnimationState::Paused(starting_progress), + AnimationPlayState::Running => AnimationState::Pending, + }; + + let computed_steps = ComputedKeyframe::generate_for_keyframes( + element, + &keyframe_animation, + context, + new_style, + style.animation_timing_function_mod(i), + resolver, + ); + + let mut new_animation = Animation { + name: name.clone(), + properties_changed: keyframe_animation.properties_changed.clone(), + computed_steps, + started_at, + duration, + fill_mode: style.animation_fill_mode_mod(i), + delay: delay as f64, + iteration_state, + state, + direction: animation_direction, + current_direction: initial_direction, + cascade_style: new_style.clone(), + is_new: true, + }; + + // If we started with a negative delay, make sure we iterate the animation if + // the delay moves us past the first iteration. + while starting_progress > 1. && !new_animation.on_last_iteration() { + new_animation.iterate(); + starting_progress -= 1.; + } + + animation_state.dirty = true; + + // If the animation was already present in the list for the node, just update its state. + for existing_animation in animation_state.animations.iter_mut() { + if existing_animation.state == AnimationState::Canceled { + continue; + } + + if new_animation.name == existing_animation.name { + existing_animation + .update_from_other(&new_animation, context.current_time_for_animations); + return; + } + } + + animation_state.animations.push(new_animation); + } +} diff --git a/servo/components/style/applicable_declarations.rs b/servo/components/style/applicable_declarations.rs new file mode 100644 index 0000000000..96049b76e3 --- /dev/null +++ b/servo/components/style/applicable_declarations.rs @@ -0,0 +1,215 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Applicable declarations management. + +use crate::properties::PropertyDeclarationBlock; +use crate::rule_tree::{CascadeLevel, StyleSource}; +use crate::shared_lock::Locked; +use crate::stylesheets::layer_rule::LayerOrder; +use servo_arc::Arc; +use smallvec::SmallVec; + +/// List of applicable declarations. This is a transient structure that shuttles +/// declarations between selector matching and inserting into the rule tree, and +/// therefore we want to avoid heap-allocation where possible. +/// +/// In measurements on wikipedia, we pretty much never have more than 8 applicable +/// declarations, so we could consider making this 8 entries instead of 16. +/// However, it may depend a lot on workload, and stack space is cheap. +pub type ApplicableDeclarationList = SmallVec<[ApplicableDeclarationBlock; 16]>; + +/// Blink uses 18 bits to store source order, and does not check overflow [1]. +/// That's a limit that could be reached in realistic webpages, so we use +/// 24 bits and enforce defined behavior in the overflow case. +/// +/// Note that right now this restriction could be lifted if wanted (because we +/// no longer stash the cascade level in the remaining bits), but we keep it in +/// place in case we come up with a use-case for them, lacking reports of the +/// current limit being too small. +/// +/// [1] https://cs.chromium.org/chromium/src/third_party/WebKit/Source/core/css/ +/// RuleSet.h?l=128&rcl=90140ab80b84d0f889abc253410f44ed54ae04f3 +const SOURCE_ORDER_BITS: usize = 24; +const SOURCE_ORDER_MAX: u32 = (1 << SOURCE_ORDER_BITS) - 1; +const SOURCE_ORDER_MASK: u32 = SOURCE_ORDER_MAX; + +/// The cascade-level+layer order of this declaration. +#[derive(Clone, Copy, Debug, Eq, Hash, MallocSizeOf, PartialEq)] +pub struct CascadePriority { + cascade_level: CascadeLevel, + layer_order: LayerOrder, +} + +const_assert_eq!( + std::mem::size_of::(), + std::mem::size_of::() +); + +impl PartialOrd for CascadePriority { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for CascadePriority { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.cascade_level.cmp(&other.cascade_level).then_with(|| { + let ordering = self.layer_order.cmp(&other.layer_order); + if ordering == std::cmp::Ordering::Equal { + return ordering; + } + // https://drafts.csswg.org/css-cascade-5/#cascade-layering + // + // Cascade layers (like declarations) are ordered by order + // of appearance. When comparing declarations that belong to + // different layers, then for normal rules the declaration + // whose cascade layer is last wins, and for important rules + // the declaration whose cascade layer is first wins. + // + // But the style attribute layer for some reason is special. + if self.cascade_level.is_important() && + !self.layer_order.is_style_attribute_layer() && + !other.layer_order.is_style_attribute_layer() + { + ordering.reverse() + } else { + ordering + } + }) + } +} + +impl CascadePriority { + /// Construct a new CascadePriority for a given (level, order) pair. + pub fn new(cascade_level: CascadeLevel, layer_order: LayerOrder) -> Self { + Self { + cascade_level, + layer_order, + } + } + + /// Returns the layer order. + #[inline] + pub fn layer_order(&self) -> LayerOrder { + self.layer_order + } + + /// Returns the cascade level. + #[inline] + pub fn cascade_level(&self) -> CascadeLevel { + self.cascade_level + } + + /// Whether this declaration should be allowed if `revert` or `revert-layer` + /// have been specified on a given origin. + /// + /// `self` is the priority at which the `revert` or `revert-layer` keyword + /// have been specified. + pub fn allows_when_reverted(&self, other: &Self, origin_revert: bool) -> bool { + if origin_revert { + other.cascade_level.origin() < self.cascade_level.origin() + } else { + other.unimportant() < self.unimportant() + } + } + + /// Convert this priority from "important" to "non-important", if needed. + pub fn unimportant(&self) -> Self { + Self::new(self.cascade_level().unimportant(), self.layer_order()) + } + + /// Convert this priority from "non-important" to "important", if needed. + pub fn important(&self) -> Self { + Self::new(self.cascade_level().important(), self.layer_order()) + } + + /// The same tree, in author origin, at the root layer. + pub fn same_tree_author_normal_at_root_layer() -> Self { + Self::new(CascadeLevel::same_tree_author_normal(), LayerOrder::root()) + } +} + +/// A property declaration together with its precedence among rules of equal +/// specificity so that we can sort them. +/// +/// This represents the declarations in a given declaration block for a given +/// importance. +#[derive(Clone, Debug, MallocSizeOf, PartialEq)] +pub struct ApplicableDeclarationBlock { + /// The style source, either a style rule, or a property declaration block. + #[ignore_malloc_size_of = "Arc"] + pub source: StyleSource, + /// The bits containing the source order, cascade level, and shadow cascade + /// order. + source_order: u32, + /// The specificity of the selector. + pub specificity: u32, + /// The cascade priority of the rule. + pub cascade_priority: CascadePriority, +} + +impl ApplicableDeclarationBlock { + /// Constructs an applicable declaration block from a given property + /// declaration block and importance. + #[inline] + pub fn from_declarations( + declarations: Arc>, + level: CascadeLevel, + layer_order: LayerOrder, + ) -> Self { + ApplicableDeclarationBlock { + source: StyleSource::from_declarations(declarations), + source_order: 0, + specificity: 0, + cascade_priority: CascadePriority::new(level, layer_order), + } + } + + /// Constructs an applicable declaration block from the given components. + #[inline] + pub fn new( + source: StyleSource, + source_order: u32, + level: CascadeLevel, + specificity: u32, + layer_order: LayerOrder, + ) -> Self { + ApplicableDeclarationBlock { + source, + source_order: source_order & SOURCE_ORDER_MASK, + specificity, + cascade_priority: CascadePriority::new(level, layer_order), + } + } + + /// Returns the source order of the block. + #[inline] + pub fn source_order(&self) -> u32 { + self.source_order + } + + /// Returns the cascade level of the block. + #[inline] + pub fn level(&self) -> CascadeLevel { + self.cascade_priority.cascade_level() + } + + /// Returns the cascade level of the block. + #[inline] + pub fn layer_order(&self) -> LayerOrder { + self.cascade_priority.layer_order() + } + + /// Convenience method to consume self and return the right thing for the + /// rule tree to iterate over. + #[inline] + pub fn for_rule_tree(self) -> (StyleSource, CascadePriority) { + (self.source, self.cascade_priority) + } +} + +// Size of this struct determines sorting and selector-matching performance. +size_of_test!(ApplicableDeclarationBlock, 24); diff --git a/servo/components/style/attr.rs b/servo/components/style/attr.rs new file mode 100644 index 0000000000..05833fa08d --- /dev/null +++ b/servo/components/style/attr.rs @@ -0,0 +1,599 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Parsed representations of [DOM attributes][attr]. +//! +//! [attr]: https://dom.spec.whatwg.org/#interface-attr + +use crate::properties::PropertyDeclarationBlock; +use crate::shared_lock::Locked; +use crate::str::str_join; +use crate::str::{read_exponent, read_fraction, HTML_SPACE_CHARACTERS}; +use crate::str::{read_numbers, split_commas, split_html_space_chars}; +use crate::values::specified::Length; +use crate::values::AtomString; +use crate::{Atom, LocalName, Namespace, Prefix}; +use app_units::Au; +use cssparser::{self, Color, RGBA}; +use euclid::num::Zero; +use num_traits::ToPrimitive; +use selectors::attr::AttrSelectorOperation; +use servo_arc::Arc; +use servo_url::ServoUrl; +use std::str::FromStr; + +// Duplicated from script::dom::values. +const UNSIGNED_LONG_MAX: u32 = 2147483647; + +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "servo", derive(MallocSizeOf))] +pub enum LengthOrPercentageOrAuto { + Auto, + Percentage(f32), + Length(Au), +} + +#[derive(Clone, Debug)] +#[cfg_attr(feature = "servo", derive(MallocSizeOf))] +pub enum AttrValue { + String(String), + TokenList(String, Vec), + UInt(String, u32), + Int(String, i32), + Double(String, f64), + Atom(Atom), + Length(String, Option), + Color(String, Option), + Dimension(String, LengthOrPercentageOrAuto), + + /// Stores a URL, computed from the input string and a document's base URL. + /// + /// The URL is resolved at setting-time, so this kind of attribute value is + /// not actually suitable for most URL-reflecting IDL attributes. + ResolvedUrl(String, Option), + + /// Note that this variant is only used transitively as a fast path to set + /// the property declaration block relevant to the style of an element when + /// set from the inline declaration of that element (that is, + /// `element.style`). + /// + /// This can, as of this writing, only correspond to the value of the + /// `style` element, and is set from its relevant CSSInlineStyleDeclaration, + /// and then converted to a string in Element::attribute_mutated. + /// + /// Note that we don't necessarily need to do that (we could just clone the + /// declaration block), but that avoids keeping a refcounted + /// declarationblock for longer than needed. + Declaration( + String, + #[ignore_malloc_size_of = "Arc"] Arc>, + ), +} + +/// Shared implementation to parse an integer according to +/// or +/// +fn do_parse_integer>(input: T) -> Result { + let mut input = input + .skip_while(|c| HTML_SPACE_CHARACTERS.iter().any(|s| s == c)) + .peekable(); + + let sign = match input.peek() { + None => return Err(()), + Some(&'-') => { + input.next(); + -1 + }, + Some(&'+') => { + input.next(); + 1 + }, + Some(_) => 1, + }; + + let (value, _) = read_numbers(input); + + value.and_then(|value| value.checked_mul(sign)).ok_or(()) +} + +/// Parse an integer according to +/// . +pub fn parse_integer>(input: T) -> Result { + do_parse_integer(input).and_then(|result| result.to_i32().ok_or(())) +} + +/// Parse an integer according to +/// +pub fn parse_unsigned_integer>(input: T) -> Result { + do_parse_integer(input).and_then(|result| result.to_u32().ok_or(())) +} + +/// Parse a floating-point number according to +/// +pub fn parse_double(string: &str) -> Result { + let trimmed = string.trim_matches(HTML_SPACE_CHARACTERS); + let mut input = trimmed.chars().peekable(); + + let (value, divisor, chars_skipped) = match input.peek() { + None => return Err(()), + Some(&'-') => { + input.next(); + (-1f64, -1f64, 1) + }, + Some(&'+') => { + input.next(); + (1f64, 1f64, 1) + }, + _ => (1f64, 1f64, 0), + }; + + let (value, value_digits) = if let Some(&'.') = input.peek() { + (0f64, 0) + } else { + let (read_val, read_digits) = read_numbers(input); + ( + value * read_val.and_then(|result| result.to_f64()).unwrap_or(1f64), + read_digits, + ) + }; + + let input = trimmed + .chars() + .skip(value_digits + chars_skipped) + .peekable(); + + let (mut value, fraction_digits) = read_fraction(input, divisor, value); + + let input = trimmed + .chars() + .skip(value_digits + chars_skipped + fraction_digits) + .peekable(); + + if let Some(exp) = read_exponent(input) { + value *= 10f64.powi(exp) + }; + + Ok(value) +} + +impl AttrValue { + pub fn from_serialized_tokenlist(tokens: String) -> AttrValue { + let atoms = + split_html_space_chars(&tokens) + .map(Atom::from) + .fold(vec![], |mut acc, atom| { + if !acc.contains(&atom) { + acc.push(atom) + } + acc + }); + AttrValue::TokenList(tokens, atoms) + } + + pub fn from_comma_separated_tokenlist(tokens: String) -> AttrValue { + let atoms = split_commas(&tokens) + .map(Atom::from) + .fold(vec![], |mut acc, atom| { + if !acc.contains(&atom) { + acc.push(atom) + } + acc + }); + AttrValue::TokenList(tokens, atoms) + } + + pub fn from_atomic_tokens(atoms: Vec) -> AttrValue { + // TODO(ajeffrey): effecient conversion of Vec to String + let tokens = String::from(str_join(&atoms, "\x20")); + AttrValue::TokenList(tokens, atoms) + } + + // https://html.spec.whatwg.org/multipage/#reflecting-content-attributes-in-idl-attributes:idl-unsigned-long + pub fn from_u32(string: String, default: u32) -> AttrValue { + let result = parse_unsigned_integer(string.chars()).unwrap_or(default); + let result = if result > UNSIGNED_LONG_MAX { + default + } else { + result + }; + AttrValue::UInt(string, result) + } + + pub fn from_i32(string: String, default: i32) -> AttrValue { + let result = parse_integer(string.chars()).unwrap_or(default); + AttrValue::Int(string, result) + } + + // https://html.spec.whatwg.org/multipage/#reflecting-content-attributes-in-idl-attributes:idl-double + pub fn from_double(string: String, default: f64) -> AttrValue { + let result = parse_double(&string).unwrap_or(default); + + if result.is_normal() { + AttrValue::Double(string, result) + } else { + AttrValue::Double(string, default) + } + } + + // https://html.spec.whatwg.org/multipage/#limited-to-only-non-negative-numbers + pub fn from_limited_i32(string: String, default: i32) -> AttrValue { + let result = parse_integer(string.chars()).unwrap_or(default); + + if result < 0 { + AttrValue::Int(string, default) + } else { + AttrValue::Int(string, result) + } + } + + // https://html.spec.whatwg.org/multipage/#limited-to-only-non-negative-numbers-greater-than-zero + pub fn from_limited_u32(string: String, default: u32) -> AttrValue { + let result = parse_unsigned_integer(string.chars()).unwrap_or(default); + let result = if result == 0 || result > UNSIGNED_LONG_MAX { + default + } else { + result + }; + AttrValue::UInt(string, result) + } + + pub fn from_atomic(string: String) -> AttrValue { + let value = Atom::from(string); + AttrValue::Atom(value) + } + + pub fn from_resolved_url(base: &ServoUrl, url: String) -> AttrValue { + let joined = base.join(&url).ok(); + AttrValue::ResolvedUrl(url, joined) + } + + pub fn from_legacy_color(string: String) -> AttrValue { + let parsed = parse_legacy_color(&string).ok(); + AttrValue::Color(string, parsed) + } + + pub fn from_dimension(string: String) -> AttrValue { + let parsed = parse_length(&string); + AttrValue::Dimension(string, parsed) + } + + pub fn from_nonzero_dimension(string: String) -> AttrValue { + let parsed = parse_nonzero_length(&string); + AttrValue::Dimension(string, parsed) + } + + /// Assumes the `AttrValue` is a `TokenList` and returns its tokens + /// + /// ## Panics + /// + /// Panics if the `AttrValue` is not a `TokenList` + pub fn as_tokens(&self) -> &[Atom] { + match *self { + AttrValue::TokenList(_, ref tokens) => tokens, + _ => panic!("Tokens not found"), + } + } + + /// Assumes the `AttrValue` is an `Atom` and returns its value + /// + /// ## Panics + /// + /// Panics if the `AttrValue` is not an `Atom` + pub fn as_atom(&self) -> &Atom { + match *self { + AttrValue::Atom(ref value) => value, + _ => panic!("Atom not found"), + } + } + + /// Assumes the `AttrValue` is a `Color` and returns its value + /// + /// ## Panics + /// + /// Panics if the `AttrValue` is not a `Color` + pub fn as_color(&self) -> Option<&RGBA> { + match *self { + AttrValue::Color(_, ref color) => color.as_ref(), + _ => panic!("Color not found"), + } + } + + /// Assumes the `AttrValue` is a `Dimension` and returns its value + /// + /// ## Panics + /// + /// Panics if the `AttrValue` is not a `Dimension` + pub fn as_dimension(&self) -> &LengthOrPercentageOrAuto { + match *self { + AttrValue::Dimension(_, ref l) => l, + _ => panic!("Dimension not found"), + } + } + + /// Assumes the `AttrValue` is a `ResolvedUrl` and returns its value. + /// + /// ## Panics + /// + /// Panics if the `AttrValue` is not a `ResolvedUrl` + pub fn as_resolved_url(&self) -> Option<&ServoUrl> { + match *self { + AttrValue::ResolvedUrl(_, ref url) => url.as_ref(), + _ => panic!("Url not found"), + } + } + + /// Return the AttrValue as its integer representation, if any. + /// This corresponds to attribute values returned as `AttrValue::UInt(_)` + /// by `VirtualMethods::parse_plain_attribute()`. + /// + /// ## Panics + /// + /// Panics if the `AttrValue` is not a `UInt` + pub fn as_uint(&self) -> u32 { + if let AttrValue::UInt(_, value) = *self { + value + } else { + panic!("Uint not found"); + } + } + + /// Return the AttrValue as a dimension computed from its integer + /// representation, assuming that integer representation specifies pixels. + /// + /// This corresponds to attribute values returned as `AttrValue::UInt(_)` + /// by `VirtualMethods::parse_plain_attribute()`. + /// + /// ## Panics + /// + /// Panics if the `AttrValue` is not a `UInt` + pub fn as_uint_px_dimension(&self) -> LengthOrPercentageOrAuto { + if let AttrValue::UInt(_, value) = *self { + LengthOrPercentageOrAuto::Length(Au::from_px(value as i32)) + } else { + panic!("Uint not found"); + } + } + + pub fn eval_selector(&self, selector: &AttrSelectorOperation<&AtomString>) -> bool { + // FIXME(SimonSapin) this can be more efficient by matching on `(self, selector)` variants + // and doing Atom comparisons instead of string comparisons where possible, + // with SelectorImpl::AttrValue changed to Atom. + selector.eval_str(self) + } +} + +impl ::std::ops::Deref for AttrValue { + type Target = str; + + fn deref(&self) -> &str { + match *self { + AttrValue::String(ref value) | + AttrValue::TokenList(ref value, _) | + AttrValue::UInt(ref value, _) | + AttrValue::Double(ref value, _) | + AttrValue::Length(ref value, _) | + AttrValue::Color(ref value, _) | + AttrValue::Int(ref value, _) | + AttrValue::ResolvedUrl(ref value, _) | + AttrValue::Declaration(ref value, _) | + AttrValue::Dimension(ref value, _) => &value, + AttrValue::Atom(ref value) => &value, + } + } +} + +impl PartialEq for AttrValue { + fn eq(&self, other: &Atom) -> bool { + match *self { + AttrValue::Atom(ref value) => value == other, + _ => other == &**self, + } + } +} + +/// +pub fn parse_nonzero_length(value: &str) -> LengthOrPercentageOrAuto { + match parse_length(value) { + LengthOrPercentageOrAuto::Length(x) if x == Au::zero() => LengthOrPercentageOrAuto::Auto, + LengthOrPercentageOrAuto::Percentage(x) if x == 0. => LengthOrPercentageOrAuto::Auto, + x => x, + } +} + +/// Parses a [legacy color][color]. If unparseable, `Err` is returned. +/// +/// [color]: https://html.spec.whatwg.org/multipage/#rules-for-parsing-a-legacy-colour-value +pub fn parse_legacy_color(mut input: &str) -> Result { + // Steps 1 and 2. + if input.is_empty() { + return Err(()); + } + + // Step 3. + input = input.trim_matches(HTML_SPACE_CHARACTERS); + + // Step 4. + if input.eq_ignore_ascii_case("transparent") { + return Err(()); + } + + // Step 5. + if let Ok(Color::RGBA(rgba)) = cssparser::parse_color_keyword(input) { + return Ok(rgba); + } + + // Step 6. + if input.len() == 4 { + if let (b'#', Ok(r), Ok(g), Ok(b)) = ( + input.as_bytes()[0], + hex(input.as_bytes()[1] as char), + hex(input.as_bytes()[2] as char), + hex(input.as_bytes()[3] as char), + ) { + return Ok(RGBA::new(r * 17, g * 17, b * 17, 255)); + } + } + + // Step 7. + let mut new_input = String::new(); + for ch in input.chars() { + if ch as u32 > 0xffff { + new_input.push_str("00") + } else { + new_input.push(ch) + } + } + let mut input = &*new_input; + + // Step 8. + for (char_count, (index, _)) in input.char_indices().enumerate() { + if char_count == 128 { + input = &input[..index]; + break; + } + } + + // Step 9. + if input.as_bytes()[0] == b'#' { + input = &input[1..] + } + + // Step 10. + let mut new_input = Vec::new(); + for ch in input.chars() { + if hex(ch).is_ok() { + new_input.push(ch as u8) + } else { + new_input.push(b'0') + } + } + let mut input = new_input; + + // Step 11. + while input.is_empty() || (input.len() % 3) != 0 { + input.push(b'0') + } + + // Step 12. + let mut length = input.len() / 3; + let (mut red, mut green, mut blue) = ( + &input[..length], + &input[length..length * 2], + &input[length * 2..], + ); + + // Step 13. + if length > 8 { + red = &red[length - 8..]; + green = &green[length - 8..]; + blue = &blue[length - 8..]; + length = 8 + } + + // Step 14. + while length > 2 && red[0] == b'0' && green[0] == b'0' && blue[0] == b'0' { + red = &red[1..]; + green = &green[1..]; + blue = &blue[1..]; + length -= 1 + } + + // Steps 15-20. + return Ok(RGBA::new( + hex_string(red).unwrap(), + hex_string(green).unwrap(), + hex_string(blue).unwrap(), + 255, + )); + + fn hex(ch: char) -> Result { + match ch { + '0'..='9' => Ok((ch as u8) - b'0'), + 'a'..='f' => Ok((ch as u8) - b'a' + 10), + 'A'..='F' => Ok((ch as u8) - b'A' + 10), + _ => Err(()), + } + } + + fn hex_string(string: &[u8]) -> Result { + match string.len() { + 0 => Err(()), + 1 => hex(string[0] as char), + _ => { + let upper = hex(string[0] as char)?; + let lower = hex(string[1] as char)?; + Ok((upper << 4) | lower) + }, + } + } +} + +/// Parses a [dimension value][dim]. If unparseable, `Auto` is returned. +/// +/// [dim]: https://html.spec.whatwg.org/multipage/#rules-for-parsing-dimension-values +// TODO: this function can be rewritten to return Result +pub fn parse_length(mut value: &str) -> LengthOrPercentageOrAuto { + // Steps 1 & 2 are not relevant + + // Step 3 + value = value.trim_start_matches(HTML_SPACE_CHARACTERS); + + // Step 4 + match value.chars().nth(0) { + Some('0'..='9') => {}, + _ => return LengthOrPercentageOrAuto::Auto, + } + + // Steps 5 to 8 + // We trim the string length to the minimum of: + // 1. the end of the string + // 2. the first occurence of a '%' (U+0025 PERCENT SIGN) + // 3. the second occurrence of a '.' (U+002E FULL STOP) + // 4. the occurrence of a character that is neither a digit nor '%' nor '.' + // Note: Step 7.4 is directly subsumed by FromStr::from_str + let mut end_index = value.len(); + let (mut found_full_stop, mut found_percent) = (false, false); + for (i, ch) in value.chars().enumerate() { + match ch { + '0'..='9' => continue, + '%' => { + found_percent = true; + end_index = i; + break; + }, + '.' if !found_full_stop => { + found_full_stop = true; + continue; + }, + _ => { + end_index = i; + break; + }, + } + } + value = &value[..end_index]; + + if found_percent { + let result: Result = FromStr::from_str(value); + match result { + Ok(number) => return LengthOrPercentageOrAuto::Percentage((number as f32) / 100.0), + Err(_) => return LengthOrPercentageOrAuto::Auto, + } + } + + match FromStr::from_str(value) { + Ok(number) => LengthOrPercentageOrAuto::Length(Au::from_f64_px(number)), + Err(_) => LengthOrPercentageOrAuto::Auto, + } +} + +/// A struct that uniquely identifies an element's attribute. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "servo", derive(MallocSizeOf))] +pub struct AttrIdentifier { + pub local_name: LocalName, + pub name: LocalName, + pub namespace: Namespace, + pub prefix: Option, +} diff --git a/servo/components/style/author_styles.rs b/servo/components/style/author_styles.rs new file mode 100644 index 0000000000..a0223dcecc --- /dev/null +++ b/servo/components/style/author_styles.rs @@ -0,0 +1,70 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! A set of author stylesheets and their computed representation, such as the +//! ones used for ShadowRoot. + +use crate::dom::TElement; +use crate::invalidation::media_queries::ToMediaListKey; +use crate::shared_lock::SharedRwLockReadGuard; +use crate::stylesheet_set::AuthorStylesheetSet; +use crate::stylesheets::StylesheetInDocument; +use crate::stylist::CascadeData; +use crate::stylist::Stylist; +use servo_arc::Arc; + +/// A set of author stylesheets and their computed representation, such as the +/// ones used for ShadowRoot. +#[derive(MallocSizeOf)] +pub struct GenericAuthorStyles +where + S: StylesheetInDocument + PartialEq + 'static, +{ + /// The sheet collection, which holds the sheet pointers, the invalidations, + /// and all that stuff. + pub stylesheets: AuthorStylesheetSet, + /// The actual cascade data computed from the stylesheets. + #[ignore_malloc_size_of = "Measured as part of the stylist"] + pub data: Arc, +} + +pub use self::GenericAuthorStyles as AuthorStyles; + +lazy_static! { + static ref EMPTY_CASCADE_DATA: Arc = Arc::new_leaked(CascadeData::new()); +} + +impl GenericAuthorStyles +where + S: StylesheetInDocument + PartialEq + 'static, +{ + /// Create an empty AuthorStyles. + #[inline] + pub fn new() -> Self { + Self { + stylesheets: AuthorStylesheetSet::new(), + data: EMPTY_CASCADE_DATA.clone(), + } + } + + /// Flush the pending sheet changes, updating `data` as appropriate. + /// + /// TODO(emilio): Need a host element and a snapshot map to do invalidation + /// properly. + #[inline] + pub fn flush(&mut self, stylist: &mut Stylist, guard: &SharedRwLockReadGuard) + where + E: TElement, + S: ToMediaListKey, + { + let flusher = self + .stylesheets + .flush::(/* host = */ None, /* snapshot_map = */ None); + + let result = stylist.rebuild_author_data(&self.data, flusher.sheets, guard); + if let Ok(Some(new_data)) = result { + self.data = new_data; + } + } +} diff --git a/servo/components/style/bezier.rs b/servo/components/style/bezier.rs new file mode 100644 index 0000000000..dd520ac0ed --- /dev/null +++ b/servo/components/style/bezier.rs @@ -0,0 +1,176 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Parametric Bézier curves. +//! +//! This is based on `WebCore/platform/graphics/UnitBezier.h` in WebKit. + +#![deny(missing_docs)] + +use crate::values::CSSFloat; + +const NEWTON_METHOD_ITERATIONS: u8 = 8; + +/// A unit cubic Bézier curve, used for timing functions in CSS transitions and animations. +pub struct Bezier { + ax: f64, + bx: f64, + cx: f64, + ay: f64, + by: f64, + cy: f64, +} + +impl Bezier { + /// Calculate the output of a unit cubic Bézier curve from the two middle control points. + /// + /// X coordinate is time, Y coordinate is function advancement. + /// The nominal range for both is 0 to 1. + /// + /// The start and end points are always (0, 0) and (1, 1) so that a transition or animation + /// starts at 0% and ends at 100%. + pub fn calculate_bezier_output( + progress: f64, + epsilon: f64, + x1: f32, + y1: f32, + x2: f32, + y2: f32, + ) -> f64 { + // Check for a linear curve. + if x1 == y1 && x2 == y2 { + return progress; + } + + // Ensure that we return 0 or 1 on both edges. + if progress == 0.0 { + return 0.0; + } + if progress == 1.0 { + return 1.0; + } + + // For negative values, try to extrapolate with tangent (p1 - p0) or, + // if p1 is coincident with p0, with (p2 - p0). + if progress < 0.0 { + if x1 > 0.0 { + return progress * y1 as f64 / x1 as f64; + } + if y1 == 0.0 && x2 > 0.0 { + return progress * y2 as f64 / x2 as f64; + } + // If we can't calculate a sensible tangent, don't extrapolate at all. + return 0.0; + } + + // For values greater than 1, try to extrapolate with tangent (p2 - p3) or, + // if p2 is coincident with p3, with (p1 - p3). + if progress > 1.0 { + if x2 < 1.0 { + return 1.0 + (progress - 1.0) * (y2 as f64 - 1.0) / (x2 as f64 - 1.0); + } + if y2 == 1.0 && x1 < 1.0 { + return 1.0 + (progress - 1.0) * (y1 as f64 - 1.0) / (x1 as f64 - 1.0); + } + // If we can't calculate a sensible tangent, don't extrapolate at all. + return 1.0; + } + + Bezier::new(x1, y1, x2, y2).solve(progress, epsilon) + } + + #[inline] + fn new(x1: CSSFloat, y1: CSSFloat, x2: CSSFloat, y2: CSSFloat) -> Bezier { + let cx = 3. * x1 as f64; + let bx = 3. * (x2 as f64 - x1 as f64) - cx; + + let cy = 3. * y1 as f64; + let by = 3. * (y2 as f64 - y1 as f64) - cy; + + Bezier { + ax: 1.0 - cx - bx, + bx: bx, + cx: cx, + ay: 1.0 - cy - by, + by: by, + cy: cy, + } + } + + #[inline] + fn sample_curve_x(&self, t: f64) -> f64 { + // ax * t^3 + bx * t^2 + cx * t + ((self.ax * t + self.bx) * t + self.cx) * t + } + + #[inline] + fn sample_curve_y(&self, t: f64) -> f64 { + ((self.ay * t + self.by) * t + self.cy) * t + } + + #[inline] + fn sample_curve_derivative_x(&self, t: f64) -> f64 { + (3.0 * self.ax * t + 2.0 * self.bx) * t + self.cx + } + + #[inline] + fn solve_curve_x(&self, x: f64, epsilon: f64) -> f64 { + // Fast path: Use Newton's method. + let mut t = x; + for _ in 0..NEWTON_METHOD_ITERATIONS { + let x2 = self.sample_curve_x(t); + if x2.approx_eq(x, epsilon) { + return t; + } + let dx = self.sample_curve_derivative_x(t); + if dx.approx_eq(0.0, 1e-6) { + break; + } + t -= (x2 - x) / dx; + } + + // Slow path: Use bisection. + let (mut lo, mut hi, mut t) = (0.0, 1.0, x); + + if t < lo { + return lo; + } + if t > hi { + return hi; + } + + while lo < hi { + let x2 = self.sample_curve_x(t); + if x2.approx_eq(x, epsilon) { + return t; + } + if x > x2 { + lo = t + } else { + hi = t + } + t = (hi - lo) / 2.0 + lo + } + + t + } + + /// Solve the bezier curve for a given `x` and an `epsilon`, that should be + /// between zero and one. + #[inline] + fn solve(&self, x: f64, epsilon: f64) -> f64 { + self.sample_curve_y(self.solve_curve_x(x, epsilon)) + } +} + +trait ApproxEq { + fn approx_eq(self, value: Self, epsilon: Self) -> bool; +} + +impl ApproxEq for f64 { + #[inline] + fn approx_eq(self, value: f64, epsilon: f64) -> bool { + (self - value).abs() < epsilon + } +} diff --git a/servo/components/style/bloom.rs b/servo/components/style/bloom.rs new file mode 100644 index 0000000000..824acb7114 --- /dev/null +++ b/servo/components/style/bloom.rs @@ -0,0 +1,401 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! The style bloom filter is used as an optimization when matching deep +//! descendant selectors. + +#![deny(missing_docs)] + +use crate::dom::{SendElement, TElement}; +use atomic_refcell::{AtomicRefCell, AtomicRefMut}; +use owning_ref::OwningHandle; +use selectors::bloom::BloomFilter; +use servo_arc::Arc; +use smallvec::SmallVec; +use std::mem::ManuallyDrop; + +thread_local! { + /// Bloom filters are large allocations, so we store them in thread-local storage + /// such that they can be reused across style traversals. StyleBloom is responsible + /// for ensuring that the bloom filter is zeroed when it is dropped. + /// + /// We intentionally leak this from TLS because we don't have the guarantee + /// of TLS destructors to run in worker threads. + /// + /// We could change this once https://github.com/rayon-rs/rayon/issues/688 + /// is fixed, hopefully. + static BLOOM_KEY: ManuallyDrop>> = + ManuallyDrop::new(Arc::new_leaked(Default::default())); +} + +/// A struct that allows us to fast-reject deep descendant selectors avoiding +/// selector-matching. +/// +/// This is implemented using a counting bloom filter, and it's a standard +/// optimization. See Gecko's `AncestorFilter`, and Blink's and WebKit's +/// `SelectorFilter`. +/// +/// The constraints for Servo's style system are a bit different compared to +/// traditional style systems given Servo does a parallel breadth-first +/// traversal instead of a sequential depth-first traversal. +/// +/// This implies that we need to track a bit more state than other browsers to +/// ensure we're doing the correct thing during the traversal, and being able to +/// apply this optimization effectively. +/// +/// Concretely, we have a bloom filter instance per worker thread, and we track +/// the current DOM depth in order to find a common ancestor when it doesn't +/// match the previous element we've styled. +/// +/// This is usually a pretty fast operation (we use to be one level deeper than +/// the previous one), but in the case of work-stealing, we may needed to push +/// and pop multiple elements. +/// +/// See the `insert_parents_recovering`, where most of the magic happens. +/// +/// Regarding thread-safety, this struct is safe because: +/// +/// * We clear this after a restyle. +/// * The DOM shape and attributes (and every other thing we access here) are +/// immutable during a restyle. +/// +pub struct StyleBloom { + /// A handle to the bloom filter from the thread upon which this StyleBloom + /// was created. We use AtomicRefCell so that this is all |Send|, which allows + /// StyleBloom to live in ThreadLocalStyleContext, which is dropped from the + /// parent thread. + filter: OwningHandle>, AtomicRefMut<'static, BloomFilter>>, + + /// The stack of elements that this bloom filter contains, along with the + /// number of hashes pushed for each element. + elements: SmallVec<[PushedElement; 16]>, + + /// Stack of hashes that have been pushed onto this filter. + pushed_hashes: SmallVec<[u32; 64]>, +} + +/// The very rough benchmarks in the selectors crate show clear() +/// costing about 25 times more than remove_hash(). We use this to implement +/// clear() more efficiently when only a small number of hashes have been +/// pushed. +/// +/// One subtly to note is that remove_hash() will not touch the value +/// if the filter overflowed. However, overflow can only occur if we +/// get 255 collisions on the same hash value, and 25 < 255. +const MEMSET_CLEAR_THRESHOLD: usize = 25; + +struct PushedElement { + /// The element that was pushed. + element: SendElement, + + /// The number of hashes pushed for the element. + num_hashes: usize, +} + +impl PushedElement { + fn new(el: E, num_hashes: usize) -> Self { + PushedElement { + element: unsafe { SendElement::new(el) }, + num_hashes, + } + } +} + +/// Returns whether the attribute name is excluded from the bloom filter. +/// +/// We do this for attributes that are very common but not commonly used in +/// selectors. +#[inline] +pub fn is_attr_name_excluded_from_filter(atom: &crate::Atom) -> bool { + *atom == atom!("class") || *atom == atom!("id") || *atom == atom!("style") +} + +/// Gather all relevant hash for fast-reject filters from an element. +pub fn each_relevant_element_hash(element: E, mut f: F) +where + E: TElement, + F: FnMut(u32), +{ + f(element.local_name().get_hash()); + f(element.namespace().get_hash()); + + if let Some(id) = element.id() { + f(id.get_hash()); + } + + element.each_class(|class| f(class.get_hash())); + + element.each_attr_name(|name| { + if !is_attr_name_excluded_from_filter(name) { + f(name.get_hash()) + } + }); +} + +impl Drop for StyleBloom { + fn drop(&mut self) { + // Leave the reusable bloom filter in a zeroed state. + self.clear(); + } +} + +impl StyleBloom { + /// Create an empty `StyleBloom`. Because StyleBloom acquires the thread- + /// local filter buffer, creating multiple live StyleBloom instances at + /// the same time on the same thread will panic. + + // Forced out of line to limit stack frame sizes after extra inlining from + // https://github.com/rust-lang/rust/pull/43931 + // + // See https://github.com/servo/servo/pull/18420#issuecomment-328769322 + #[inline(never)] + pub fn new() -> Self { + let bloom_arc = BLOOM_KEY.with(|b| Arc::clone(&*b)); + let filter = + OwningHandle::new_with_fn(bloom_arc, |x| unsafe { x.as_ref() }.unwrap().borrow_mut()); + debug_assert!( + filter.is_zeroed(), + "Forgot to zero the bloom filter last time" + ); + StyleBloom { + filter, + elements: Default::default(), + pushed_hashes: Default::default(), + } + } + + /// Return the bloom filter used properly by the `selectors` crate. + pub fn filter(&self) -> &BloomFilter { + &*self.filter + } + + /// Push an element to the bloom filter, knowing that it's a child of the + /// last element parent. + pub fn push(&mut self, element: E) { + if cfg!(debug_assertions) { + if self.elements.is_empty() { + assert!(element.traversal_parent().is_none()); + } + } + self.push_internal(element); + } + + /// Same as `push`, but without asserting, in order to use it from + /// `rebuild`. + fn push_internal(&mut self, element: E) { + let mut count = 0; + each_relevant_element_hash(element, |hash| { + count += 1; + self.filter.insert_hash(hash); + self.pushed_hashes.push(hash); + }); + self.elements.push(PushedElement::new(element, count)); + } + + /// Pop the last element in the bloom filter and return it. + #[inline] + fn pop(&mut self) -> Option { + let PushedElement { + element, + num_hashes, + } = self.elements.pop()?; + let popped_element = *element; + + // Verify that the pushed hashes match the ones we'd get from the element. + let mut expected_hashes = vec![]; + if cfg!(debug_assertions) { + each_relevant_element_hash(popped_element, |hash| expected_hashes.push(hash)); + } + + for _ in 0..num_hashes { + let hash = self.pushed_hashes.pop().unwrap(); + debug_assert_eq!(expected_hashes.pop().unwrap(), hash); + self.filter.remove_hash(hash); + } + + Some(popped_element) + } + + /// Returns the DOM depth of elements that can be correctly + /// matched against the bloom filter (that is, the number of + /// elements in our list). + pub fn matching_depth(&self) -> usize { + self.elements.len() + } + + /// Clears the bloom filter. + pub fn clear(&mut self) { + self.elements.clear(); + + if self.pushed_hashes.len() > MEMSET_CLEAR_THRESHOLD { + self.filter.clear(); + self.pushed_hashes.clear(); + } else { + for hash in self.pushed_hashes.drain(..) { + self.filter.remove_hash(hash); + } + debug_assert!(self.filter.is_zeroed()); + } + } + + /// Rebuilds the bloom filter up to the parent of the given element. + pub fn rebuild(&mut self, mut element: E) { + self.clear(); + + let mut parents_to_insert = SmallVec::<[E; 16]>::new(); + while let Some(parent) = element.traversal_parent() { + parents_to_insert.push(parent); + element = parent; + } + + for parent in parents_to_insert.drain(..).rev() { + self.push(parent); + } + } + + /// In debug builds, asserts that all the parents of `element` are in the + /// bloom filter. + /// + /// Goes away in release builds. + pub fn assert_complete(&self, mut element: E) { + if cfg!(debug_assertions) { + let mut checked = 0; + while let Some(parent) = element.traversal_parent() { + assert_eq!( + parent, + *(self.elements[self.elements.len() - 1 - checked].element) + ); + element = parent; + checked += 1; + } + assert_eq!(checked, self.elements.len()); + } + } + + /// Get the element that represents the chain of things inserted + /// into the filter right now. That chain is the given element + /// (if any) and its ancestors. + #[inline] + pub fn current_parent(&self) -> Option { + self.elements.last().map(|ref el| *el.element) + } + + /// Insert the parents of an element in the bloom filter, trying to recover + /// the filter if the last element inserted doesn't match. + /// + /// Gets the element depth in the dom, to make it efficient, or if not + /// provided always rebuilds the filter from scratch. + /// + /// Returns the new bloom filter depth, that the traversal code is + /// responsible to keep around if it wants to get an effective filter. + pub fn insert_parents_recovering(&mut self, element: E, element_depth: usize) { + // Easy case, we're in a different restyle, or we're empty. + if self.elements.is_empty() { + self.rebuild(element); + return; + } + + let traversal_parent = match element.traversal_parent() { + Some(parent) => parent, + None => { + // Yay, another easy case. + self.clear(); + return; + }, + }; + + if self.current_parent() == Some(traversal_parent) { + // Ta da, cache hit, we're all done. + return; + } + + if element_depth == 0 { + self.clear(); + return; + } + + // We should've early exited above. + debug_assert!( + element_depth != 0, + "We should have already cleared the bloom filter" + ); + debug_assert!(!self.elements.is_empty(), "How! We should've just rebuilt!"); + + // Now the fun begins: We have the depth of the dom and the depth of the + // last element inserted in the filter, let's try to find a common + // parent. + // + // The current depth, that is, the depth of the last element inserted in + // the bloom filter, is the number of elements _minus one_, that is: if + // there's one element, it must be the root -> depth zero. + let mut current_depth = self.elements.len() - 1; + + // If the filter represents an element too deep in the dom, we need to + // pop ancestors. + while current_depth > element_depth - 1 { + self.pop().expect("Emilio is bad at math"); + current_depth -= 1; + } + + // Now let's try to find a common parent in the bloom filter chain, + // starting with traversal_parent. + let mut common_parent = traversal_parent; + let mut common_parent_depth = element_depth - 1; + + // Let's collect the parents we are going to need to insert once we've + // found the common one. + let mut parents_to_insert = SmallVec::<[E; 16]>::new(); + + // If the bloom filter still doesn't have enough elements, the common + // parent is up in the dom. + while common_parent_depth > current_depth { + // TODO(emilio): Seems like we could insert parents here, then + // reverse the slice. + parents_to_insert.push(common_parent); + common_parent = common_parent.traversal_parent().expect("We were lied to"); + common_parent_depth -= 1; + } + + // Now the two depths are the same. + debug_assert_eq!(common_parent_depth, current_depth); + + // Happy case: The parents match, we only need to push the ancestors + // we've collected and we'll never enter in this loop. + // + // Not-so-happy case: Parent's don't match, so we need to keep going up + // until we find a common ancestor. + // + // Gecko currently models native anonymous content that conceptually + // hangs off the document (such as scrollbars) as a separate subtree + // from the document root. + // + // Thus it's possible with Gecko that we do not find any common + // ancestor. + while *(self.elements.last().unwrap().element) != common_parent { + parents_to_insert.push(common_parent); + self.pop().unwrap(); + common_parent = match common_parent.traversal_parent() { + Some(parent) => parent, + None => { + debug_assert!(self.elements.is_empty()); + if cfg!(feature = "gecko") { + break; + } else { + panic!("should have found a common ancestor"); + } + }, + } + } + + // Now the parents match, so insert the stack of elements we have been + // collecting so far. + for parent in parents_to_insert.drain(..).rev() { + self.push(parent); + } + + debug_assert_eq!(self.elements.len(), element_depth); + + // We're done! Easy. + } +} diff --git a/servo/components/style/build.rs b/servo/components/style/build.rs new file mode 100644 index 0000000000..2247e87618 --- /dev/null +++ b/servo/components/style/build.rs @@ -0,0 +1,91 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#[macro_use] +extern crate lazy_static; + +use std::env; +use std::path::Path; +use std::process::{exit, Command}; +use walkdir::WalkDir; + +#[cfg(feature = "gecko")] +mod build_gecko; + +#[cfg(not(feature = "gecko"))] +mod build_gecko { + pub fn generate() {} +} + +lazy_static! { + pub static ref PYTHON: String = env::var("PYTHON3").ok().unwrap_or_else(|| { + let candidates = if cfg!(windows) { + ["python3.exe"] + } else { + ["python3"] + }; + for &name in &candidates { + if Command::new(name) + .arg("--version") + .output() + .ok() + .map_or(false, |out| out.status.success()) + { + return name.to_owned(); + } + } + panic!( + "Can't find python (tried {})! Try fixing PATH or setting the PYTHON3 env var", + candidates.join(", ") + ) + }); +} + +fn generate_properties(engine: &str) { + for entry in WalkDir::new("properties") { + let entry = entry.unwrap(); + match entry.path().extension().and_then(|e| e.to_str()) { + Some("mako") | Some("rs") | Some("py") | Some("zip") => { + println!("cargo:rerun-if-changed={}", entry.path().display()); + }, + _ => {}, + } + } + + let script = Path::new(&env::var_os("CARGO_MANIFEST_DIR").unwrap()) + .join("properties") + .join("build.py"); + + let status = Command::new(&*PYTHON) + .arg(&script) + .arg(engine) + .arg("style-crate") + .status() + .unwrap(); + if !status.success() { + exit(1) + } +} + +fn main() { + let gecko = cfg!(feature = "gecko"); + let servo = cfg!(feature = "servo"); + let l2013 = cfg!(feature = "servo-layout-2013"); + let l2020 = cfg!(feature = "servo-layout-2020"); + let engine = match (gecko, servo, l2013, l2020) { + (true, false, false, false) => "gecko", + (false, true, true, false) => "servo-2013", + (false, true, false, true) => "servo-2020", + _ => panic!( + "\n\n\ + The style crate requires enabling one of its 'servo' or 'gecko' feature flags \ + and, in the 'servo' case, one of 'servo-layout-2013' or 'servo-layout-2020'.\ + \n\n" + ), + }; + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:out_dir={}", env::var("OUT_DIR").unwrap()); + generate_properties(engine); + build_gecko::generate(); +} diff --git a/servo/components/style/build_gecko.rs b/servo/components/style/build_gecko.rs new file mode 100644 index 0000000000..a83c5dbc6d --- /dev/null +++ b/servo/components/style/build_gecko.rs @@ -0,0 +1,400 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use super::PYTHON; +use bindgen::{Builder, CodegenConfig}; +use regex::Regex; +use std::cmp; +use std::collections::HashSet; +use std::env; +use std::fs::{self, File}; +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; +use std::process::{exit, Command}; +use std::slice; +use std::sync::Mutex; +use std::time::SystemTime; +use toml; +use toml::value::Table; + +lazy_static! { + static ref OUTDIR_PATH: PathBuf = PathBuf::from(env::var_os("OUT_DIR").unwrap()).join("gecko"); +} + +const STRUCTS_FILE: &'static str = "structs.rs"; + +fn read_config(path: &PathBuf) -> Table { + println!("cargo:rerun-if-changed={}", path.to_str().unwrap()); + update_last_modified(&path); + + let mut contents = String::new(); + File::open(path) + .expect("Failed to open config file") + .read_to_string(&mut contents) + .expect("Failed to read config file"); + match toml::from_str::(&contents) { + Ok(result) => result, + Err(e) => panic!("Failed to parse config file: {}", e), + } +} + +lazy_static! { + static ref CONFIG: Table = { + // Load Gecko's binding generator config from the source tree. + let path = mozbuild::TOPSRCDIR.join("layout/style/ServoBindings.toml"); + read_config(&path) + }; + static ref BINDGEN_FLAGS: Vec = { + // Load build-specific config overrides. + let path = mozbuild::TOPOBJDIR.join("layout/style/extra-bindgen-flags"); + println!("cargo:rerun-if-changed={}", path.to_str().unwrap()); + fs::read_to_string(path).expect("Failed to read extra-bindgen-flags file") + .split_whitespace() + .map(std::borrow::ToOwned::to_owned) + .collect() + }; + static ref INCLUDE_RE: Regex = Regex::new(r#"#include\s*"(.+?)""#).unwrap(); + static ref DISTDIR_PATH: PathBuf = mozbuild::TOPOBJDIR.join("dist"); + static ref SEARCH_PATHS: Vec = vec![ + DISTDIR_PATH.join("include"), + DISTDIR_PATH.join("include/nspr"), + ]; + static ref ADDED_PATHS: Mutex> = Mutex::new(HashSet::new()); + static ref LAST_MODIFIED: Mutex = + Mutex::new(get_modified_time(&env::current_exe().unwrap()) + .expect("Failed to get modified time of executable")); +} + +fn get_modified_time(file: &Path) -> Option { + file.metadata().and_then(|m| m.modified()).ok() +} + +fn update_last_modified(file: &Path) { + let modified = get_modified_time(file).expect("Couldn't get file modification time"); + let mut last_modified = LAST_MODIFIED.lock().unwrap(); + *last_modified = cmp::max(modified, *last_modified); +} + +fn search_include(name: &str) -> Option { + for path in SEARCH_PATHS.iter() { + let file = path.join(name); + if file.is_file() { + update_last_modified(&file); + return Some(file); + } + } + None +} + +fn add_headers_recursively(path: PathBuf, added_paths: &mut HashSet) { + if added_paths.contains(&path) { + return; + } + let mut file = File::open(&path).unwrap(); + let mut content = String::new(); + file.read_to_string(&mut content).unwrap(); + added_paths.insert(path); + // Find all includes and add them recursively + for cap in INCLUDE_RE.captures_iter(&content) { + if let Some(path) = search_include(cap.get(1).unwrap().as_str()) { + add_headers_recursively(path, added_paths); + } + } +} + +fn add_include(name: &str) -> String { + let mut added_paths = ADDED_PATHS.lock().unwrap(); + let file = match search_include(name) { + Some(file) => file, + None => panic!("Include not found: {}", name), + }; + let result = String::from(file.to_str().unwrap()); + add_headers_recursively(file, &mut *added_paths); + result +} + +trait BuilderExt { + fn get_initial_builder() -> Builder; + fn include>(self, file: T) -> Builder; +} + +impl BuilderExt for Builder { + fn get_initial_builder() -> Builder { + // Disable rust unions, because we replace some types inside of + // them. + let mut builder = Builder::default() + .size_t_is_usize(true) + .disable_untagged_union(); + + let rustfmt_path = env::var_os("RUSTFMT") + // This can be replaced with + // > .filter(|p| !p.is_empty()).map(PathBuf::from) + // once we can use 1.27+. + .and_then(|p| { + if p.is_empty() { + None + } else { + Some(PathBuf::from(p)) + } + }); + if let Some(path) = rustfmt_path { + builder = builder.with_rustfmt(path); + } + + for dir in SEARCH_PATHS.iter() { + builder = builder.clang_arg("-I").clang_arg(dir.to_str().unwrap()); + } + + builder = builder.include(add_include("mozilla-config.h")); + + if env::var("CARGO_FEATURE_GECKO_DEBUG").is_ok() { + builder = builder.clang_arg("-DDEBUG=1").clang_arg("-DJS_DEBUG=1"); + } + + for item in &*BINDGEN_FLAGS { + builder = builder.clang_arg(item); + } + + builder + } + fn include>(self, file: T) -> Builder { + self.clang_arg("-include").clang_arg(file) + } +} + +struct Fixup { + pat: String, + rep: String, +} + +fn write_binding_file(builder: Builder, file: &str, fixups: &[Fixup]) { + let out_file = OUTDIR_PATH.join(file); + if let Some(modified) = get_modified_time(&out_file) { + // Don't generate the file if nothing it depends on was modified. + let last_modified = LAST_MODIFIED.lock().unwrap(); + if *last_modified <= modified { + return; + } + } + let command_line_opts = builder.command_line_flags(); + let result = builder.generate(); + let mut result = match result { + Ok(bindings) => bindings.to_string(), + Err(_) => { + panic!( + "Failed to generate bindings, flags: {:?}", + command_line_opts + ); + }, + }; + + for fixup in fixups.iter() { + result = Regex::new(&fixup.pat) + .unwrap() + .replace_all(&result, &*fixup.rep) + .into_owned() + .into(); + } + let bytes = result.into_bytes(); + File::create(&out_file) + .unwrap() + .write_all(&bytes) + .expect("Unable to write output"); +} + +struct BuilderWithConfig<'a> { + builder: Builder, + config: &'a Table, + used_keys: HashSet<&'static str>, +} +impl<'a> BuilderWithConfig<'a> { + fn new(builder: Builder, config: &'a Table) -> Self { + BuilderWithConfig { + builder, + config, + used_keys: HashSet::new(), + } + } + + fn handle_list(self, key: &'static str, func: F) -> BuilderWithConfig<'a> + where + F: FnOnce(Builder, slice::Iter<'a, toml::Value>) -> Builder, + { + let mut builder = self.builder; + let config = self.config; + let mut used_keys = self.used_keys; + if let Some(list) = config.get(key) { + used_keys.insert(key); + builder = func(builder, list.as_array().unwrap().as_slice().iter()); + } + BuilderWithConfig { + builder, + config, + used_keys, + } + } + fn handle_items(self, key: &'static str, mut func: F) -> BuilderWithConfig<'a> + where + F: FnMut(Builder, &'a toml::Value) -> Builder, + { + self.handle_list(key, |b, iter| iter.fold(b, |b, item| func(b, item))) + } + fn handle_str_items(self, key: &'static str, mut func: F) -> BuilderWithConfig<'a> + where + F: FnMut(Builder, &'a str) -> Builder, + { + self.handle_items(key, |b, item| func(b, item.as_str().unwrap())) + } + fn handle_table_items(self, key: &'static str, mut func: F) -> BuilderWithConfig<'a> + where + F: FnMut(Builder, &'a Table) -> Builder, + { + self.handle_items(key, |b, item| func(b, item.as_table().unwrap())) + } + fn handle_common(self, fixups: &mut Vec) -> BuilderWithConfig<'a> { + self.handle_str_items("headers", |b, item| b.header(add_include(item))) + .handle_str_items("raw-lines", |b, item| b.raw_line(item)) + .handle_str_items("hide-types", |b, item| b.blocklist_type(item)) + .handle_table_items("fixups", |builder, item| { + fixups.push(Fixup { + pat: item["pat"].as_str().unwrap().into(), + rep: item["rep"].as_str().unwrap().into(), + }); + builder + }) + } + + fn get_builder(self) -> Builder { + for key in self.config.keys() { + if !self.used_keys.contains(key.as_str()) { + panic!("Unknown key: {}", key); + } + } + self.builder + } +} + +fn generate_structs() { + let builder = Builder::get_initial_builder() + .enable_cxx_namespaces() + .with_codegen_config(CodegenConfig::TYPES | CodegenConfig::VARS | CodegenConfig::FUNCTIONS); + let mut fixups = vec![]; + let builder = BuilderWithConfig::new(builder, CONFIG["structs"].as_table().unwrap()) + .handle_common(&mut fixups) + .handle_str_items("allowlist-functions", |b, item| b.allowlist_function(item)) + .handle_str_items("bitfield-enums", |b, item| b.bitfield_enum(item)) + .handle_str_items("rusty-enums", |b, item| b.rustified_enum(item)) + .handle_str_items("allowlist-vars", |b, item| b.allowlist_var(item)) + .handle_str_items("allowlist-types", |b, item| b.allowlist_type(item)) + .handle_str_items("opaque-types", |b, item| b.opaque_type(item)) + .handle_table_items("cbindgen-types", |b, item| { + let gecko = item["gecko"].as_str().unwrap(); + let servo = item["servo"].as_str().unwrap(); + b.blocklist_type(format!("mozilla::{}", gecko)) + .module_raw_line("root::mozilla", format!("pub use {} as {};", servo, gecko)) + }) + .handle_table_items("mapped-generic-types", |builder, item| { + let generic = item["generic"].as_bool().unwrap(); + let gecko = item["gecko"].as_str().unwrap(); + let servo = item["servo"].as_str().unwrap(); + let gecko_name = gecko.rsplit("::").next().unwrap(); + let gecko = gecko + .split("::") + .map(|s| format!("\\s*{}\\s*", s)) + .collect::>() + .join("::"); + + fixups.push(Fixup { + pat: format!("\\broot\\s*::\\s*{}\\b", gecko), + rep: format!("crate::gecko_bindings::structs::{}", gecko_name), + }); + builder.blocklist_type(gecko).raw_line(format!( + "pub type {0}{2} = {1}{2};", + gecko_name, + servo, + if generic { "" } else { "" } + )) + }) + .get_builder(); + write_binding_file(builder, STRUCTS_FILE, &fixups); +} + +fn setup_logging() -> bool { + struct BuildLogger { + file: Option>, + filter: String, + } + + impl log::Log for BuildLogger { + fn enabled(&self, meta: &log::Metadata) -> bool { + self.file.is_some() && meta.target().contains(&self.filter) + } + + fn log(&self, record: &log::Record) { + if !self.enabled(record.metadata()) { + return; + } + + let mut file = self.file.as_ref().unwrap().lock().unwrap(); + let _ = writeln!( + file, + "{} - {} - {} @ {}:{}", + record.level(), + record.target(), + record.args(), + record.file().unwrap_or(""), + record.line().unwrap_or(0) + ); + } + + fn flush(&self) { + if let Some(ref file) = self.file { + file.lock().unwrap().flush().unwrap(); + } + } + } + + if let Some(path) = env::var_os("STYLO_BUILD_LOG") { + log::set_max_level(log::LevelFilter::Debug); + log::set_boxed_logger(Box::new(BuildLogger { + file: fs::File::create(path).ok().map(Mutex::new), + filter: env::var("STYLO_BUILD_FILTER") + .ok() + .unwrap_or_else(|| "bindgen".to_owned()), + })) + .expect("Failed to set logger."); + + true + } else { + false + } +} + +fn generate_atoms() { + let script = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()) + .join("gecko") + .join("regen_atoms.py"); + println!("cargo:rerun-if-changed={}", script.display()); + let status = Command::new(&*PYTHON) + .arg(&script) + .arg(DISTDIR_PATH.as_os_str()) + .arg(OUTDIR_PATH.as_os_str()) + .status() + .unwrap(); + if !status.success() { + exit(1); + } +} + +pub fn generate() { + println!("cargo:rerun-if-changed=build_gecko.rs"); + fs::create_dir_all(&*OUTDIR_PATH).unwrap(); + setup_logging(); + generate_structs(); + generate_atoms(); + + for path in ADDED_PATHS.lock().unwrap().iter() { + println!("cargo:rerun-if-changed={}", path.to_str().unwrap()); + } +} diff --git a/servo/components/style/color/convert.rs b/servo/components/style/color/convert.rs new file mode 100644 index 0000000000..a6274db39a --- /dev/null +++ b/servo/components/style/color/convert.rs @@ -0,0 +1,902 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Color conversion algorithms. +//! +//! Algorithms, matrices and constants are from the [color-4] specification, +//! unless otherwise specified: +//! +//! https://drafts.csswg.org/css-color-4/#color-conversion-code +//! +//! NOTE: Matrices has to be transposed from the examples in the spec for use +//! with the `euclid` library. + +use crate::color::ColorComponents; +use crate::values::normalize; + +type Transform = euclid::default::Transform3D; +type Vector = euclid::default::Vector3D; + +/// Normalize hue into [0, 360). +#[inline] +pub fn normalize_hue(hue: f32) -> f32 { + hue - 360. * (hue / 360.).floor() +} + +/// Calculate the hue from RGB components and return it along with the min and +/// max RGB values. +#[inline] +fn rgb_to_hue_min_max(red: f32, green: f32, blue: f32) -> (f32, f32, f32) { + let max = red.max(green).max(blue); + let min = red.min(green).min(blue); + + let delta = max - min; + + let hue = if delta != 0.0 { + 60.0 * if max == red { + (green - blue) / delta + if green < blue { 6.0 } else { 0.0 } + } else if max == green { + (blue - red) / delta + 2.0 + } else { + (red - green) / delta + 4.0 + } + } else { + f32::NAN + }; + + (hue, min, max) +} + +/// Convert from HSL notation to RGB notation. +/// https://drafts.csswg.org/css-color-4/#hsl-to-rgb +#[inline] +pub fn hsl_to_rgb(from: &ColorComponents) -> ColorComponents { + fn hue_to_rgb(t1: f32, t2: f32, hue: f32) -> f32 { + let hue = normalize_hue(hue); + + if hue * 6.0 < 360.0 { + t1 + (t2 - t1) * hue / 60.0 + } else if hue * 2.0 < 360.0 { + t2 + } else if hue * 3.0 < 720.0 { + t1 + (t2 - t1) * (240.0 - hue) / 60.0 + } else { + t1 + } + } + + // Convert missing components to 0.0. + let ColorComponents(hue, saturation, lightness) = from.map(normalize); + let saturation = saturation / 100.0; + let lightness = lightness / 100.0; + + let t2 = if lightness <= 0.5 { + lightness * (saturation + 1.0) + } else { + lightness + saturation - lightness * saturation + }; + let t1 = lightness * 2.0 - t2; + + ColorComponents( + hue_to_rgb(t1, t2, hue + 120.0), + hue_to_rgb(t1, t2, hue), + hue_to_rgb(t1, t2, hue - 120.0), + ) +} + +/// Convert from RGB notation to HSL notation. +/// https://drafts.csswg.org/css-color-4/#rgb-to-hsl +pub fn rgb_to_hsl(from: &ColorComponents) -> ColorComponents { + let ColorComponents(red, green, blue) = *from; + + let (hue, min, max) = rgb_to_hue_min_max(red, green, blue); + + let lightness = (min + max) / 2.0; + let delta = max - min; + + let saturation = if delta != 0.0 { + if lightness == 0.0 || lightness == 1.0 { + 0.0 + } else { + (max - lightness) / lightness.min(1.0 - lightness) + } + } else { + 0.0 + }; + + ColorComponents(hue, saturation * 100.0, lightness * 100.0) +} + +/// Convert from HWB notation to RGB notation. +/// https://drafts.csswg.org/css-color-4/#hwb-to-rgb +#[inline] +pub fn hwb_to_rgb(from: &ColorComponents) -> ColorComponents { + // Convert missing components to 0.0. + let ColorComponents(hue, whiteness, blackness) = from.map(normalize); + + let whiteness = whiteness / 100.0; + let blackness = blackness / 100.0; + + if whiteness + blackness >= 1.0 { + let gray = whiteness / (whiteness + blackness); + return ColorComponents(gray, gray, gray); + } + + let x = 1.0 - whiteness - blackness; + hsl_to_rgb(&ColorComponents(hue, 100.0, 50.0)).map(|v| v * x + whiteness) +} + +/// Convert from RGB notation to HWB notation. +/// https://drafts.csswg.org/css-color-4/#rgb-to-hwb +#[inline] +pub fn rgb_to_hwb(from: &ColorComponents) -> ColorComponents { + let ColorComponents(red, green, blue) = *from; + + let (hue, min, max) = rgb_to_hue_min_max(red, green, blue); + + let whiteness = min; + let blackness = 1.0 - max; + + ColorComponents(hue, whiteness * 100.0, blackness * 100.0) +} + +/// Convert from the rectangular orthogonal to the cylindrical polar coordinate +/// system. This is used to convert (ok)lab to (ok)lch. +/// +#[inline] +pub fn orthogonal_to_polar(from: &ColorComponents) -> ColorComponents { + let ColorComponents(lightness, a, b) = *from; + + let chroma = (a * a + b * b).sqrt(); + + // Very small chroma values make the hue component powerless. + let hue = if chroma.abs() < 1.0e-6 { + f32::NAN + } else { + normalize_hue(b.atan2(a).to_degrees()) + }; + + ColorComponents(lightness, chroma, hue) +} + +/// Convert from the cylindrical polar to the rectangular orthogonal coordinate +/// system. This is used to convert (ok)lch to (ok)lab. +/// +#[inline] +pub fn polar_to_orthogonal(from: &ColorComponents) -> ColorComponents { + let ColorComponents(lightness, chroma, hue) = *from; + + // A missing hue component results in an achromatic color. + if hue.is_nan() { + return ColorComponents(lightness, 0.0, 0.0); + } + + let hue = hue.to_radians(); + let a = chroma * hue.cos(); + let b = chroma * hue.sin(); + + ColorComponents(lightness, a, b) +} + +#[inline] +fn transform(from: &ColorComponents, mat: &Transform) -> ColorComponents { + let result = mat.transform_vector3d(Vector::new(from.0, from.1, from.2)); + ColorComponents(result.x, result.y, result.z) +} + +fn xyz_d65_to_xyz_d50(from: &ColorComponents) -> ColorComponents { + #[rustfmt::skip] + const MAT: Transform = Transform::new( + 1.0479298208405488, 0.029627815688159344, -0.009243058152591178, 0.0, + 0.022946793341019088, 0.990434484573249, 0.015055144896577895, 0.0, + -0.05019222954313557, -0.01707382502938514, 0.7518742899580008, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); + + transform(from, &MAT) +} + +fn xyz_d50_to_xyz_d65(from: &ColorComponents) -> ColorComponents { + #[rustfmt::skip] + const MAT: Transform = Transform::new( + 0.9554734527042182, -0.028369706963208136, 0.012314001688319899, 0.0, + -0.023098536874261423, 1.0099954580058226, -0.020507696433477912, 0.0, + 0.0632593086610217, 0.021041398966943008, 1.3303659366080753, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); + + transform(from, &MAT) +} + +/// A reference white that is used during color conversion. +pub enum WhitePoint { + /// D50 white reference. + D50, + /// D65 white reference. + D65, +} + +impl WhitePoint { + const fn values(&self) -> ColorComponents { + // + match self { + // [0.3457 / 0.3585, 1.00000, (1.0 - 0.3457 - 0.3585) / 0.3585] + WhitePoint::D50 => ColorComponents(0.9642956764295677, 1.0, 0.8251046025104602), + // [0.3127 / 0.3290, 1.00000, (1.0 - 0.3127 - 0.3290) / 0.3290] + WhitePoint::D65 => ColorComponents(0.9504559270516716, 1.0, 1.0890577507598784), + } + } +} + +fn convert_white_point(from: WhitePoint, to: WhitePoint, components: &mut ColorComponents) { + match (from, to) { + (WhitePoint::D50, WhitePoint::D65) => *components = xyz_d50_to_xyz_d65(components), + (WhitePoint::D65, WhitePoint::D50) => *components = xyz_d65_to_xyz_d50(components), + _ => {}, + } +} + +/// A trait that allows conversion of color spaces to and from XYZ coordinate +/// space with a specified white point. +/// +/// Allows following the specified method of converting between color spaces: +/// - Convert to values to sRGB linear light. +/// - Convert to XYZ coordinate space. +/// - Adjust white point to target white point. +/// - Convert to sRGB linear light in target color space. +/// - Convert to sRGB gamma encoded in target color space. +/// +/// https://drafts.csswg.org/css-color-4/#color-conversion +pub trait ColorSpaceConversion { + /// The white point that the implementer is represented in. + const WHITE_POINT: WhitePoint; + + /// Convert the components from sRGB gamma encoded values to sRGB linear + /// light values. + fn to_linear_light(from: &ColorComponents) -> ColorComponents; + + /// Convert the components from sRGB linear light values to XYZ coordinate + /// space. + fn to_xyz(from: &ColorComponents) -> ColorComponents; + + /// Convert the components from XYZ coordinate space to sRGB linear light + /// values. + fn from_xyz(from: &ColorComponents) -> ColorComponents; + + /// Convert the components from sRGB linear light values to sRGB gamma + /// encoded values. + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents; +} + +/// Convert the color components from the specified color space to XYZ and +/// return the components and the white point they are in. +pub fn to_xyz(from: &ColorComponents) -> (ColorComponents, WhitePoint) { + // Convert the color components where in-gamut values are in the range + // [0 - 1] to linear light (un-companded) form. + let result = From::to_linear_light(from); + + // Convert the color components from the source color space to XYZ. + (From::to_xyz(&result), From::WHITE_POINT) +} + +/// Convert the color components from XYZ at the given white point to the +/// specified color space. +pub fn from_xyz( + from: &ColorComponents, + white_point: WhitePoint, +) -> ColorComponents { + let mut xyz = from.clone(); + + // Convert the white point if needed. + convert_white_point(white_point, To::WHITE_POINT, &mut xyz); + + // Convert the color from XYZ to the target color space. + let result = To::from_xyz(&xyz); + + // Convert the color components of linear-light values in the range + // [0 - 1] to a gamma corrected form. + To::to_gamma_encoded(&result) +} + +/// The sRGB color space. +/// https://drafts.csswg.org/css-color-4/#predefined-sRGB +pub struct Srgb; + +impl Srgb { + #[rustfmt::skip] + const TO_XYZ: Transform = Transform::new( + 0.4123907992659595, 0.21263900587151036, 0.01933081871559185, 0.0, + 0.35758433938387796, 0.7151686787677559, 0.11919477979462599, 0.0, + 0.1804807884018343, 0.07219231536073371, 0.9505321522496606, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); + + #[rustfmt::skip] + const FROM_XYZ: Transform = Transform::new( + 3.2409699419045213, -0.9692436362808798, 0.05563007969699361, 0.0, + -1.5373831775700935, 1.8759675015077206, -0.20397695888897657, 0.0, + -0.4986107602930033, 0.04155505740717561, 1.0569715142428786, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); +} + +impl ColorSpaceConversion for Srgb { + const WHITE_POINT: WhitePoint = WhitePoint::D65; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + from.clone().map(|value| { + let abs = value.abs(); + + if abs < 0.04045 { + value / 12.92 + } else { + value.signum() * ((abs + 0.055) / 1.055).powf(2.4) + } + }) + } + + fn to_xyz(from: &ColorComponents) -> ColorComponents { + transform(from, &Self::TO_XYZ) + } + + fn from_xyz(from: &ColorComponents) -> ColorComponents { + transform(from, &Self::FROM_XYZ) + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + from.clone().map(|value| { + let abs = value.abs(); + + if abs > 0.0031308 { + value.signum() * (1.055 * abs.powf(1.0 / 2.4) - 0.055) + } else { + 12.92 * value + } + }) + } +} + +/// Color specified with hue, saturation and lightness components. +pub struct Hsl; + +impl ColorSpaceConversion for Hsl { + const WHITE_POINT: WhitePoint = Srgb::WHITE_POINT; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + Srgb::to_linear_light(&hsl_to_rgb(from)) + } + + #[inline] + fn to_xyz(from: &ColorComponents) -> ColorComponents { + Srgb::to_xyz(from) + } + + #[inline] + fn from_xyz(from: &ColorComponents) -> ColorComponents { + Srgb::from_xyz(from) + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + rgb_to_hsl(&Srgb::to_gamma_encoded(from)) + } +} + +/// Color specified with hue, whiteness and blackness components. +pub struct Hwb; + +impl ColorSpaceConversion for Hwb { + const WHITE_POINT: WhitePoint = Srgb::WHITE_POINT; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + Srgb::to_linear_light(&hwb_to_rgb(from)) + } + + #[inline] + fn to_xyz(from: &ColorComponents) -> ColorComponents { + Srgb::to_xyz(from) + } + + #[inline] + fn from_xyz(from: &ColorComponents) -> ColorComponents { + Srgb::from_xyz(from) + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + rgb_to_hwb(&Srgb::to_gamma_encoded(from)) + } +} + +/// The same as sRGB color space, except the transfer function is linear light. +/// https://drafts.csswg.org/css-color-4/#predefined-sRGB-linear +pub struct SrgbLinear; + +impl ColorSpaceConversion for SrgbLinear { + const WHITE_POINT: WhitePoint = Srgb::WHITE_POINT; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + // Already in linear light form. + from.clone() + } + + fn to_xyz(from: &ColorComponents) -> ColorComponents { + Srgb::to_xyz(from) + } + + fn from_xyz(from: &ColorComponents) -> ColorComponents { + Srgb::from_xyz(from) + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + // Stay in linear light form. + from.clone() + } +} + +/// The Display-P3 color space. +/// https://drafts.csswg.org/css-color-4/#predefined-display-p3 +pub struct DisplayP3; + +impl DisplayP3 { + #[rustfmt::skip] + const TO_XYZ: Transform = Transform::new( + 0.48657094864821626, 0.22897456406974884, 0.0, 0.0, + 0.26566769316909294, 0.6917385218365062, 0.045113381858902575, 0.0, + 0.1982172852343625, 0.079286914093745, 1.0439443689009757, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); + + #[rustfmt::skip] + const FROM_XYZ: Transform = Transform::new( + 2.4934969119414245, -0.829488969561575, 0.035845830243784335, 0.0, + -0.9313836179191236, 1.7626640603183468, -0.07617238926804171, 0.0, + -0.40271078445071684, 0.02362468584194359, 0.9568845240076873, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); +} + +impl ColorSpaceConversion for DisplayP3 { + const WHITE_POINT: WhitePoint = WhitePoint::D65; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + Srgb::to_linear_light(from) + } + + fn to_xyz(from: &ColorComponents) -> ColorComponents { + transform(from, &Self::TO_XYZ) + } + + fn from_xyz(from: &ColorComponents) -> ColorComponents { + transform(from, &Self::FROM_XYZ) + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + Srgb::to_gamma_encoded(from) + } +} + +/// The a98-rgb color space. +/// https://drafts.csswg.org/css-color-4/#predefined-a98-rgb +pub struct A98Rgb; + +impl A98Rgb { + #[rustfmt::skip] + const TO_XYZ: Transform = Transform::new( + 0.5766690429101308, 0.29734497525053616, 0.027031361386412378, 0.0, + 0.18555823790654627, 0.627363566255466, 0.07068885253582714, 0.0, + 0.18822864623499472, 0.07529145849399789, 0.9913375368376389, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); + + #[rustfmt::skip] + const FROM_XYZ: Transform = Transform::new( + 2.041587903810746, -0.9692436362808798, 0.013444280632031024, 0.0, + -0.5650069742788596, 1.8759675015077206, -0.11836239223101824, 0.0, + -0.3447313507783295, 0.04155505740717561, 1.0151749943912054, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); +} + +impl ColorSpaceConversion for A98Rgb { + const WHITE_POINT: WhitePoint = WhitePoint::D65; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + from.clone().map(|v| v.signum() * v.abs().powf(2.19921875)) + } + + fn to_xyz(from: &ColorComponents) -> ColorComponents { + transform(from, &Self::TO_XYZ) + } + + fn from_xyz(from: &ColorComponents) -> ColorComponents { + transform(from, &Self::FROM_XYZ) + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + from.clone() + .map(|v| v.signum() * v.abs().powf(0.4547069271758437)) + } +} + +/// The ProPhoto RGB color space. +/// https://drafts.csswg.org/css-color-4/#predefined-prophoto-rgb +pub struct ProphotoRgb; + +impl ProphotoRgb { + #[rustfmt::skip] + const TO_XYZ: Transform = Transform::new( + 0.7977604896723027, 0.2880711282292934, 0.0, 0.0, + 0.13518583717574031, 0.7118432178101014, 0.0, 0.0, + 0.0313493495815248, 0.00008565396060525902, 0.8251046025104601, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); + + #[rustfmt::skip] + const FROM_XYZ: Transform = Transform::new( + 1.3457989731028281, -0.5446224939028347, 0.0, 0.0, + -0.25558010007997534, 1.5082327413132781, 0.0, 0.0, + -0.05110628506753401, 0.02053603239147973, 1.2119675456389454, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); +} + +impl ColorSpaceConversion for ProphotoRgb { + const WHITE_POINT: WhitePoint = WhitePoint::D50; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + from.clone().map(|value| { + const ET2: f32 = 16.0 / 512.0; + + let abs = value.abs(); + + if abs <= ET2 { + value / 16.0 + } else { + value.signum() * abs.powf(1.8) + } + }) + } + + fn to_xyz(from: &ColorComponents) -> ColorComponents { + transform(from, &Self::TO_XYZ) + } + + fn from_xyz(from: &ColorComponents) -> ColorComponents { + transform(from, &Self::FROM_XYZ) + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + const ET: f32 = 1.0 / 512.0; + + from.clone().map(|v| { + let abs = v.abs(); + if abs >= ET { + v.signum() * abs.powf(1.0 / 1.8) + } else { + 16.0 * v + } + }) + } +} + +/// The Rec.2020 color space. +/// https://drafts.csswg.org/css-color-4/#predefined-rec2020 +pub struct Rec2020; + +impl Rec2020 { + const ALPHA: f32 = 1.09929682680944; + const BETA: f32 = 0.018053968510807; + + #[rustfmt::skip] + const TO_XYZ: Transform = Transform::new( + 0.6369580483012913, 0.26270021201126703, 0.0, 0.0, + 0.14461690358620838, 0.677998071518871, 0.028072693049087508, 0.0, + 0.16888097516417205, 0.059301716469861945, 1.0609850577107909, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); + + #[rustfmt::skip] + const FROM_XYZ: Transform = Transform::new( + 1.7166511879712676, -0.666684351832489, 0.017639857445310915, 0.0, + -0.3556707837763924, 1.616481236634939, -0.042770613257808655, 0.0, + -0.2533662813736598, 0.01576854581391113, 0.942103121235474, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); +} + +impl ColorSpaceConversion for Rec2020 { + const WHITE_POINT: WhitePoint = WhitePoint::D65; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + from.clone().map(|value| { + let abs = value.abs(); + + if abs < Self::BETA * 4.5 { + value / 4.5 + } else { + value.signum() * ((abs + Self::ALPHA - 1.0) / Self::ALPHA).powf(1.0 / 0.45) + } + }) + } + + fn to_xyz(from: &ColorComponents) -> ColorComponents { + transform(from, &Self::TO_XYZ) + } + + fn from_xyz(from: &ColorComponents) -> ColorComponents { + transform(from, &Self::FROM_XYZ) + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + from.clone().map(|v| { + let abs = v.abs(); + + if abs > Self::BETA { + v.signum() * (Self::ALPHA * abs.powf(0.45) - (Self::ALPHA - 1.0)) + } else { + 4.5 * v + } + }) + } +} + +/// A color in the XYZ coordinate space with a D50 white reference. +/// https://drafts.csswg.org/css-color-4/#predefined-xyz +pub struct XyzD50; + +impl ColorSpaceConversion for XyzD50 { + const WHITE_POINT: WhitePoint = WhitePoint::D50; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + from.clone() + } + + fn to_xyz(from: &ColorComponents) -> ColorComponents { + from.clone() + } + + fn from_xyz(from: &ColorComponents) -> ColorComponents { + from.clone() + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + from.clone() + } +} + +/// A color in the XYZ coordinate space with a D65 white reference. +/// https://drafts.csswg.org/css-color-4/#predefined-xyz +pub struct XyzD65; + +impl ColorSpaceConversion for XyzD65 { + const WHITE_POINT: WhitePoint = WhitePoint::D65; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + from.clone() + } + + fn to_xyz(from: &ColorComponents) -> ColorComponents { + from.clone() + } + + fn from_xyz(from: &ColorComponents) -> ColorComponents { + from.clone() + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + from.clone() + } +} + +/// The Lab color space. +/// https://drafts.csswg.org/css-color-4/#specifying-lab-lch +pub struct Lab; + +impl Lab { + const KAPPA: f32 = 24389.0 / 27.0; + const EPSILON: f32 = 216.0 / 24389.0; +} + +impl ColorSpaceConversion for Lab { + const WHITE_POINT: WhitePoint = WhitePoint::D50; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + // No need for conversion. + from.clone() + } + + /// Convert a CIELAB color to XYZ as specified in [1] and [2]. + /// + /// [1]: https://drafts.csswg.org/css-color/#lab-to-predefined + /// [2]: https://drafts.csswg.org/css-color/#color-conversion-code + fn to_xyz(from: &ColorComponents) -> ColorComponents { + let ColorComponents(lightness, a, b) = *from; + + let f1 = (lightness + 16.0) / 116.0; + let f0 = f1 + a / 500.0; + let f2 = f1 - b / 200.0; + + let f0_cubed = f0 * f0 * f0; + let x = if f0_cubed > Self::EPSILON { + f0_cubed + } else { + (116.0 * f0 - 16.0) / Self::KAPPA + }; + + let y = if lightness > Self::KAPPA * Self::EPSILON { + let v = (lightness + 16.0) / 116.0; + v * v * v + } else { + lightness / Self::KAPPA + }; + + let f2_cubed = f2 * f2 * f2; + let z = if f2_cubed > Self::EPSILON { + f2_cubed + } else { + (116.0 * f2 - 16.0) / Self::KAPPA + }; + + ColorComponents(x, y, z) * Self::WHITE_POINT.values() + } + + /// Convert an XYZ color to LAB as specified in [1] and [2]. + /// + /// [1]: https://drafts.csswg.org/css-color/#rgb-to-lab + /// [2]: https://drafts.csswg.org/css-color/#color-conversion-code + fn from_xyz(from: &ColorComponents) -> ColorComponents { + let adapted = *from / Self::WHITE_POINT.values(); + + // 4. Convert D50-adapted XYZ to Lab. + let ColorComponents(f0, f1, f2) = adapted.map(|v| { + if v > Self::EPSILON { + v.cbrt() + } else { + (Self::KAPPA * v + 16.0) / 116.0 + } + }); + + let lightness = 116.0 * f1 - 16.0; + let a = 500.0 * (f0 - f1); + let b = 200.0 * (f1 - f2); + + ColorComponents(lightness, a, b) + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + // No need for conversion. + from.clone() + } +} + +/// The Lch color space. +/// https://drafts.csswg.org/css-color-4/#specifying-lab-lch +pub struct Lch; + +impl ColorSpaceConversion for Lch { + const WHITE_POINT: WhitePoint = Lab::WHITE_POINT; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + // No need for conversion. + from.clone() + } + + fn to_xyz(from: &ColorComponents) -> ColorComponents { + // Convert LCH to Lab first. + let lab = polar_to_orthogonal(from); + + // Then convert the Lab to XYZ. + Lab::to_xyz(&lab) + } + + fn from_xyz(from: &ColorComponents) -> ColorComponents { + // First convert the XYZ to LAB. + let lab = Lab::from_xyz(&from); + + // Then convert the Lab to LCH. + orthogonal_to_polar(&lab) + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + // No need for conversion. + from.clone() + } +} + +/// The Oklab color space. +/// https://drafts.csswg.org/css-color-4/#specifying-oklab-oklch +pub struct Oklab; + +impl Oklab { + #[rustfmt::skip] + const XYZ_TO_LMS: Transform = Transform::new( + 0.8190224432164319, 0.0329836671980271, 0.048177199566046255, 0.0, + 0.3619062562801221, 0.9292868468965546, 0.26423952494422764, 0.0, + -0.12887378261216414, 0.03614466816999844, 0.6335478258136937, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); + + #[rustfmt::skip] + const LMS_TO_OKLAB: Transform = Transform::new( + 0.2104542553, 1.9779984951, 0.0259040371, 0.0, + 0.7936177850, -2.4285922050, 0.7827717662, 0.0, + -0.0040720468, 0.4505937099, -0.8086757660, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); + + #[rustfmt::skip] + const LMS_TO_XYZ: Transform = Transform::new( + 1.2268798733741557, -0.04057576262431372, -0.07637294974672142, 0.0, + -0.5578149965554813, 1.1122868293970594, -0.4214933239627914, 0.0, + 0.28139105017721583, -0.07171106666151701, 1.5869240244272418, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); + + #[rustfmt::skip] + const OKLAB_TO_LMS: Transform = Transform::new( + 0.99999999845051981432, 1.0000000088817607767, 1.0000000546724109177, 0.0, + 0.39633779217376785678, -0.1055613423236563494, -0.089484182094965759684, 0.0, + 0.21580375806075880339, -0.063854174771705903402, -1.2914855378640917399, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); +} + +impl ColorSpaceConversion for Oklab { + const WHITE_POINT: WhitePoint = WhitePoint::D65; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + // No need for conversion. + from.clone() + } + + fn to_xyz(from: &ColorComponents) -> ColorComponents { + let lms = transform(&from, &Self::OKLAB_TO_LMS); + let lms = lms.map(|v| v * v * v); + transform(&lms, &Self::LMS_TO_XYZ) + } + + fn from_xyz(from: &ColorComponents) -> ColorComponents { + let lms = transform(&from, &Self::XYZ_TO_LMS); + let lms = lms.map(|v| v.cbrt()); + transform(&lms, &Self::LMS_TO_OKLAB) + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + // No need for conversion. + from.clone() + } +} + +/// The Oklch color space. +/// https://drafts.csswg.org/css-color-4/#specifying-oklab-oklch +pub struct Oklch; + +impl ColorSpaceConversion for Oklch { + const WHITE_POINT: WhitePoint = Oklab::WHITE_POINT; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + // No need for conversion. + from.clone() + } + + fn to_xyz(from: &ColorComponents) -> ColorComponents { + // First convert OkLCH to Oklab. + let oklab = polar_to_orthogonal(from); + + // Then convert Oklab to XYZ. + Oklab::to_xyz(&oklab) + } + + fn from_xyz(from: &ColorComponents) -> ColorComponents { + // First convert XYZ to Oklab. + let lab = Oklab::from_xyz(&from); + + // Then convert Oklab to OkLCH. + orthogonal_to_polar(&lab) + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + // No need for conversion. + from.clone() + } +} diff --git a/servo/components/style/color/mix.rs b/servo/components/style/color/mix.rs new file mode 100644 index 0000000000..bcc4628575 --- /dev/null +++ b/servo/components/style/color/mix.rs @@ -0,0 +1,558 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Color mixing/interpolation. + +use super::{AbsoluteColor, ColorFlags, ColorSpace}; +use crate::parser::{Parse, ParserContext}; +use crate::values::generics::color::ColorMixFlags; +use cssparser::Parser; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError, ToCss}; + +/// A hue-interpolation-method as defined in [1]. +/// +/// [1]: https://drafts.csswg.org/css-color-4/#typedef-hue-interpolation-method +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + ToAnimatedValue, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum HueInterpolationMethod { + /// https://drafts.csswg.org/css-color-4/#shorter + Shorter, + /// https://drafts.csswg.org/css-color-4/#longer + Longer, + /// https://drafts.csswg.org/css-color-4/#increasing + Increasing, + /// https://drafts.csswg.org/css-color-4/#decreasing + Decreasing, + /// https://drafts.csswg.org/css-color-4/#specified + Specified, +} + +/// https://drafts.csswg.org/css-color-4/#color-interpolation-method +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + ToShmem, + ToAnimatedValue, + ToComputedValue, + ToResolvedValue, +)] +#[repr(C)] +pub struct ColorInterpolationMethod { + /// The color-space the interpolation should be done in. + pub space: ColorSpace, + /// The hue interpolation method. + pub hue: HueInterpolationMethod, +} + +impl ColorInterpolationMethod { + /// Returns the srgb interpolation method. + pub const fn srgb() -> Self { + Self { + space: ColorSpace::Srgb, + hue: HueInterpolationMethod::Shorter, + } + } + + /// Return the oklab interpolation method used for default color + /// interpolcation. + pub const fn oklab() -> Self { + Self { + space: ColorSpace::Oklab, + hue: HueInterpolationMethod::Shorter, + } + } + + /// Decides the best method for interpolating between the given colors. + /// https://drafts.csswg.org/css-color-4/#interpolation-space + pub fn best_interpolation_between(left: &AbsoluteColor, right: &AbsoluteColor) -> Self { + // The preferred color space to use for interpolating colors is Oklab. + // However, if either of the colors are in legacy rgb(), hsl() or hwb(), + // then interpolation is done in sRGB. + if !left.is_legacy_syntax() || !right.is_legacy_syntax() { + Self::oklab() + } else { + Self::srgb() + } + } +} + +impl Parse for ColorInterpolationMethod { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + input.expect_ident_matching("in")?; + let space = ColorSpace::parse(input)?; + // https://drafts.csswg.org/css-color-4/#hue-interpolation + // Unless otherwise specified, if no specific hue interpolation + // algorithm is selected by the host syntax, the default is shorter. + let hue = if space.is_polar() { + input + .try_parse(|input| -> Result<_, ParseError<'i>> { + let hue = HueInterpolationMethod::parse(input)?; + input.expect_ident_matching("hue")?; + Ok(hue) + }) + .unwrap_or(HueInterpolationMethod::Shorter) + } else { + HueInterpolationMethod::Shorter + }; + Ok(Self { space, hue }) + } +} + +impl ToCss for ColorInterpolationMethod { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: Write, + { + dest.write_str("in ")?; + self.space.to_css(dest)?; + if self.hue != HueInterpolationMethod::Shorter { + dest.write_char(' ')?; + self.hue.to_css(dest)?; + dest.write_str(" hue")?; + } + Ok(()) + } +} + +/// Mix two colors into one. +pub fn mix( + interpolation: ColorInterpolationMethod, + left_color: &AbsoluteColor, + mut left_weight: f32, + right_color: &AbsoluteColor, + mut right_weight: f32, + flags: ColorMixFlags, +) -> AbsoluteColor { + // https://drafts.csswg.org/css-color-5/#color-mix-percent-norm + let mut alpha_multiplier = 1.0; + if flags.contains(ColorMixFlags::NORMALIZE_WEIGHTS) { + let sum = left_weight + right_weight; + if sum != 1.0 { + let scale = 1.0 / sum; + left_weight *= scale; + right_weight *= scale; + if sum < 1.0 { + alpha_multiplier = sum; + } + } + } + + let result = mix_in( + interpolation.space, + left_color, + left_weight, + right_color, + right_weight, + interpolation.hue, + alpha_multiplier, + ); + + if flags.contains(ColorMixFlags::RESULT_IN_MODERN_SYNTAX) { + // If the result *MUST* be in modern syntax, then make sure it is in a + // color space that allows the modern syntax. So hsl and hwb will be + // converted to srgb. + if result.is_legacy_syntax() { + result.to_color_space(ColorSpace::Srgb) + } else { + result + } + } else if left_color.is_legacy_syntax() && right_color.is_legacy_syntax() { + // If both sides of the mix is legacy then convert the result back into + // legacy. + result.into_srgb_legacy() + } else { + result + } +} + +/// What the outcome of each component should be in a mix result. +#[derive(Clone, Copy)] +#[repr(u8)] +enum ComponentMixOutcome { + /// Mix the left and right sides to give the result. + Mix, + /// Carry the left side forward to the result. + UseLeft, + /// Carry the right side forward to the result. + UseRight, + /// The resulting component should also be none. + None, +} + +impl ComponentMixOutcome { + fn from_colors( + left: &AbsoluteColor, + right: &AbsoluteColor, + flags_to_check: ColorFlags, + ) -> Self { + match ( + left.flags.contains(flags_to_check), + right.flags.contains(flags_to_check), + ) { + (true, true) => Self::None, + (true, false) => Self::UseRight, + (false, true) => Self::UseLeft, + (false, false) => Self::Mix, + } + } +} + +/// Calculate the flags that should be carried forward a color before converting +/// it to the interpolation color space according to: +/// +fn carry_forward_analogous_missing_components( + from: ColorSpace, + to: ColorSpace, + flags: ColorFlags, +) -> ColorFlags { + use ColorFlags as F; + use ColorSpace as S; + + if from == to { + return flags; + } + + // Reds r, x + // Greens g, y + // Blues b, z + if from.is_rgb_or_xyz_like() && to.is_rgb_or_xyz_like() { + return flags; + } + + let mut result = flags; + + // Lightness L + if matches!(from, S::Lab | S::Lch | S::Oklab | S::Oklch) { + if matches!(to, S::Lab | S::Lch | S::Oklab | S::Oklch) { + result.set(F::C0_IS_NONE, flags.contains(F::C0_IS_NONE)); + } else if matches!(to, S::Hsl) { + result.set(F::C2_IS_NONE, flags.contains(F::C0_IS_NONE)); + } + } else if matches!(from, S::Hsl) && matches!(to, S::Lab | S::Lch | S::Oklab | S::Oklch) { + result.set(F::C0_IS_NONE, flags.contains(F::C2_IS_NONE)); + } + + // Colorfulness C, S + if matches!(from, S::Hsl | S::Lch | S::Oklch) && matches!(to, S::Hsl | S::Lch | S::Oklch) { + result.set(F::C1_IS_NONE, flags.contains(F::C1_IS_NONE)); + } + + // Hue H + if matches!(from, S::Hsl | S::Hwb) { + if matches!(to, S::Hsl | S::Hwb) { + result.set(F::C0_IS_NONE, flags.contains(F::C0_IS_NONE)); + } else if matches!(to, S::Lch | S::Oklch) { + result.set(F::C2_IS_NONE, flags.contains(F::C0_IS_NONE)); + } + } else if matches!(from, S::Lch | S::Oklch) { + if matches!(to, S::Hsl | S::Hwb) { + result.set(F::C0_IS_NONE, flags.contains(F::C2_IS_NONE)); + } else if matches!(to, S::Lch | S::Oklch) { + result.set(F::C2_IS_NONE, flags.contains(F::C2_IS_NONE)); + } + } + + // Opponent a, a + // Opponent b, b + if matches!(from, S::Lab | S::Oklab) && matches!(to, S::Lab | S::Oklab) { + result.set(F::C1_IS_NONE, flags.contains(F::C1_IS_NONE)); + result.set(F::C2_IS_NONE, flags.contains(F::C2_IS_NONE)); + } + + result +} + +fn mix_in( + color_space: ColorSpace, + left_color: &AbsoluteColor, + left_weight: f32, + right_color: &AbsoluteColor, + right_weight: f32, + hue_interpolation: HueInterpolationMethod, + alpha_multiplier: f32, +) -> AbsoluteColor { + // Convert both colors into the interpolation color space. + let mut left = left_color.to_color_space(color_space); + left.flags = + carry_forward_analogous_missing_components(left_color.color_space, color_space, left.flags); + let mut right = right_color.to_color_space(color_space); + right.flags = carry_forward_analogous_missing_components( + right_color.color_space, + color_space, + right.flags, + ); + + let outcomes = [ + ComponentMixOutcome::from_colors(&left, &right, ColorFlags::C0_IS_NONE), + ComponentMixOutcome::from_colors(&left, &right, ColorFlags::C1_IS_NONE), + ComponentMixOutcome::from_colors(&left, &right, ColorFlags::C2_IS_NONE), + ComponentMixOutcome::from_colors(&left, &right, ColorFlags::ALPHA_IS_NONE), + ]; + + // Convert both sides into just components. + let left = left.raw_components(); + let right = right.raw_components(); + + let (result, result_flags) = interpolate_premultiplied( + &left, + left_weight, + &right, + right_weight, + color_space.hue_index(), + hue_interpolation, + &outcomes, + ); + + let alpha = if alpha_multiplier != 1.0 { + result[3] * alpha_multiplier + } else { + result[3] + }; + + // FIXME: In rare cases we end up with 0.999995 in the alpha channel, + // so we reduce the precision to avoid serializing to + // rgba(?, ?, ?, 1). This is not ideal, so we should look into + // ways to avoid it. Maybe pre-multiply all color components and + // then divide after calculations? + let alpha = (alpha * 1000.0).round() / 1000.0; + + let mut result = AbsoluteColor::new(color_space, result[0], result[1], result[2], alpha); + + result.flags = result_flags; + + result +} + +fn interpolate_premultiplied_component( + left: f32, + left_weight: f32, + left_alpha: f32, + right: f32, + right_weight: f32, + right_alpha: f32, +) -> f32 { + left * left_weight * left_alpha + right * right_weight * right_alpha +} + +// Normalize hue into [0, 360) +#[inline] +fn normalize_hue(v: f32) -> f32 { + v - 360. * (v / 360.).floor() +} + +fn adjust_hue(left: &mut f32, right: &mut f32, hue_interpolation: HueInterpolationMethod) { + // Adjust the hue angle as per + // https://drafts.csswg.org/css-color/#hue-interpolation. + // + // If both hue angles are NAN, they should be set to 0. Otherwise, if a + // single hue angle is NAN, it should use the other hue angle. + if left.is_nan() { + if right.is_nan() { + *left = 0.; + *right = 0.; + } else { + *left = *right; + } + } else if right.is_nan() { + *right = *left; + } + + if hue_interpolation == HueInterpolationMethod::Specified { + // Angles are not adjusted. They are interpolated like any other + // component. + return; + } + + *left = normalize_hue(*left); + *right = normalize_hue(*right); + + match hue_interpolation { + // https://drafts.csswg.org/css-color/#shorter + HueInterpolationMethod::Shorter => { + let delta = *right - *left; + + if delta > 180. { + *left += 360.; + } else if delta < -180. { + *right += 360.; + } + }, + // https://drafts.csswg.org/css-color/#longer + HueInterpolationMethod::Longer => { + let delta = *right - *left; + if 0. < delta && delta < 180. { + *left += 360.; + } else if -180. < delta && delta <= 0. { + *right += 360.; + } + }, + // https://drafts.csswg.org/css-color/#increasing + HueInterpolationMethod::Increasing => { + if *right < *left { + *right += 360.; + } + }, + // https://drafts.csswg.org/css-color/#decreasing + HueInterpolationMethod::Decreasing => { + if *left < *right { + *left += 360.; + } + }, + HueInterpolationMethod::Specified => unreachable!("Handled above"), + } +} + +fn interpolate_hue( + mut left: f32, + left_weight: f32, + mut right: f32, + right_weight: f32, + hue_interpolation: HueInterpolationMethod, +) -> f32 { + adjust_hue(&mut left, &mut right, hue_interpolation); + left * left_weight + right * right_weight +} + +struct InterpolatedAlpha { + /// The adjusted left alpha value. + left: f32, + /// The adjusted right alpha value. + right: f32, + /// The interpolated alpha value. + interpolated: f32, + /// Whether the alpha component should be `none`. + is_none: bool, +} + +fn interpolate_alpha( + left: f32, + left_weight: f32, + right: f32, + right_weight: f32, + outcome: ComponentMixOutcome, +) -> InterpolatedAlpha { + // + let mut result = match outcome { + ComponentMixOutcome::Mix => { + let interpolated = left * left_weight + right * right_weight; + InterpolatedAlpha { + left, + right, + interpolated, + is_none: false, + } + }, + ComponentMixOutcome::UseLeft => InterpolatedAlpha { + left, + right: left, + interpolated: left, + is_none: false, + }, + ComponentMixOutcome::UseRight => InterpolatedAlpha { + left: right, + right, + interpolated: right, + is_none: false, + }, + ComponentMixOutcome::None => InterpolatedAlpha { + left: 1.0, + right: 1.0, + interpolated: 0.0, + is_none: true, + }, + }; + + // Clip all alpha values to [0.0..1.0]. + result.left = result.left.clamp(0.0, 1.0); + result.right = result.right.clamp(0.0, 1.0); + result.interpolated = result.interpolated.clamp(0.0, 1.0); + + result +} + +fn interpolate_premultiplied( + left: &[f32; 4], + left_weight: f32, + right: &[f32; 4], + right_weight: f32, + hue_index: Option, + hue_interpolation: HueInterpolationMethod, + outcomes: &[ComponentMixOutcome; 4], +) -> ([f32; 4], ColorFlags) { + let alpha = interpolate_alpha(left[3], left_weight, right[3], right_weight, outcomes[3]); + let mut flags = if alpha.is_none { + ColorFlags::ALPHA_IS_NONE + } else { + ColorFlags::empty() + }; + + let mut result = [0.; 4]; + + for i in 0..3 { + match outcomes[i] { + ComponentMixOutcome::Mix => { + let is_hue = hue_index == Some(i); + result[i] = if is_hue { + normalize_hue(interpolate_hue( + left[i], + left_weight, + right[i], + right_weight, + hue_interpolation, + )) + } else { + let interpolated = interpolate_premultiplied_component( + left[i], + left_weight, + alpha.left, + right[i], + right_weight, + alpha.right, + ); + + if alpha.interpolated == 0.0 { + interpolated + } else { + interpolated / alpha.interpolated + } + }; + }, + ComponentMixOutcome::UseLeft => result[i] = left[i], + ComponentMixOutcome::UseRight => result[i] = right[i], + ComponentMixOutcome::None => { + result[i] = 0.0; + match i { + 0 => flags.insert(ColorFlags::C0_IS_NONE), + 1 => flags.insert(ColorFlags::C1_IS_NONE), + 2 => flags.insert(ColorFlags::C2_IS_NONE), + _ => unreachable!(), + } + }, + } + } + result[3] = alpha.interpolated; + + (result, flags) +} diff --git a/servo/components/style/color/mod.rs b/servo/components/style/color/mod.rs new file mode 100644 index 0000000000..797a1cb00f --- /dev/null +++ b/servo/components/style/color/mod.rs @@ -0,0 +1,613 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Color support functions. + +/// cbindgen:ignore +pub mod convert; +pub mod mix; +pub mod parsing; + +use cssparser::color::PredefinedColorSpace; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ToCss}; + +/// The 3 components that make up a color. (Does not include the alpha component) +#[derive(Copy, Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] +#[repr(C)] +pub struct ColorComponents(pub f32, pub f32, pub f32); + +impl ColorComponents { + /// Apply a function to each of the 3 components of the color. + #[must_use] + pub fn map(self, f: impl Fn(f32) -> f32) -> Self { + Self(f(self.0), f(self.1), f(self.2)) + } +} + +impl std::ops::Mul for ColorComponents { + type Output = Self; + + fn mul(self, rhs: Self) -> Self::Output { + Self(self.0 * rhs.0, self.1 * rhs.1, self.2 * rhs.2) + } +} + +impl std::ops::Div for ColorComponents { + type Output = Self; + + fn div(self, rhs: Self) -> Self::Output { + Self(self.0 / rhs.0, self.1 / rhs.1, self.2 / rhs.2) + } +} + +/// A color space representation in the CSS specification. +/// +/// https://drafts.csswg.org/css-color-4/#typedef-color-space +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + ToAnimatedValue, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum ColorSpace { + /// A color specified in the sRGB color space with either the rgb/rgba(..) + /// functions or the newer color(srgb ..) function. If the color(..) + /// function is used, the AS_COLOR_FUNCTION flag will be set. Examples: + /// "color(srgb 0.691 0.139 0.259)", "rgb(176, 35, 66)" + Srgb = 0, + /// A color specified in the Hsl notation in the sRGB color space, e.g. + /// "hsl(289.18 93.136% 65.531%)" + /// https://drafts.csswg.org/css-color-4/#the-hsl-notation + Hsl, + /// A color specified in the Hwb notation in the sRGB color space, e.g. + /// "hwb(740deg 20% 30%)" + /// https://drafts.csswg.org/css-color-4/#the-hwb-notation + Hwb, + /// A color specified in the Lab color format, e.g. + /// "lab(29.2345% 39.3825 20.0664)". + /// https://w3c.github.io/csswg-drafts/css-color-4/#lab-colors + Lab, + /// A color specified in the Lch color format, e.g. + /// "lch(29.2345% 44.2 27)". + /// https://w3c.github.io/csswg-drafts/css-color-4/#lch-colors + Lch, + /// A color specified in the Oklab color format, e.g. + /// "oklab(40.101% 0.1147 0.0453)". + /// https://w3c.github.io/csswg-drafts/css-color-4/#lab-colors + Oklab, + /// A color specified in the Oklch color format, e.g. + /// "oklch(40.101% 0.12332 21.555)". + /// https://w3c.github.io/csswg-drafts/css-color-4/#lch-colors + Oklch, + /// A color specified with the color(..) function and the "srgb-linear" + /// color space, e.g. "color(srgb-linear 0.435 0.017 0.055)". + SrgbLinear, + /// A color specified with the color(..) function and the "display-p3" + /// color space, e.g. "color(display-p3 0.84 0.19 0.72)". + DisplayP3, + /// A color specified with the color(..) function and the "a98-rgb" color + /// space, e.g. "color(a98-rgb 0.44091 0.49971 0.37408)". + A98Rgb, + /// A color specified with the color(..) function and the "prophoto-rgb" + /// color space, e.g. "color(prophoto-rgb 0.36589 0.41717 0.31333)". + ProphotoRgb, + /// A color specified with the color(..) function and the "rec2020" color + /// space, e.g. "color(rec2020 0.42210 0.47580 0.35605)". + Rec2020, + /// A color specified with the color(..) function and the "xyz-d50" color + /// space, e.g. "color(xyz-d50 0.2005 0.14089 0.4472)". + XyzD50, + /// A color specified with the color(..) function and the "xyz-d65" or "xyz" + /// color space, e.g. "color(xyz-d65 0.21661 0.14602 0.59452)". + /// NOTE: https://drafts.csswg.org/css-color-4/#resolving-color-function-values + /// specifies that `xyz` is an alias for the `xyz-d65` color space. + #[parse(aliases = "xyz")] + XyzD65, +} + +impl ColorSpace { + /// Returns whether this is a ``. + #[inline] + pub fn is_rectangular(&self) -> bool { + !self.is_polar() + } + + /// Returns whether this is a ``. + #[inline] + pub fn is_polar(&self) -> bool { + matches!(self, Self::Hsl | Self::Hwb | Self::Lch | Self::Oklch) + } + + /// Returns true if the color has RGB or XYZ components. + #[inline] + pub fn is_rgb_or_xyz_like(&self) -> bool { + match self { + Self::Srgb | + Self::SrgbLinear | + Self::DisplayP3 | + Self::A98Rgb | + Self::ProphotoRgb | + Self::Rec2020 | + Self::XyzD50 | + Self::XyzD65 => true, + _ => false, + } + } + + /// Returns an index of the hue component in the color space, otherwise + /// `None`. + #[inline] + pub fn hue_index(&self) -> Option { + match self { + Self::Hsl | Self::Hwb => Some(0), + Self::Lch | Self::Oklch => Some(2), + + _ => { + debug_assert!(!self.is_polar()); + None + }, + } + } +} + +/// Flags used when serializing colors. +#[derive(Clone, Copy, Debug, Default, MallocSizeOf, PartialEq, ToShmem)] +#[repr(C)] +pub struct ColorFlags(u8); +bitflags! { + impl ColorFlags : u8 { + /// Whether the 1st color component is `none`. + const C0_IS_NONE = 1 << 0; + /// Whether the 2nd color component is `none`. + const C1_IS_NONE = 1 << 1; + /// Whether the 3rd color component is `none`. + const C2_IS_NONE = 1 << 2; + /// Whether the alpha component is `none`. + const ALPHA_IS_NONE = 1 << 3; + /// Marks that this color is in the legacy color format. This flag is + /// only valid for the `Srgb` color space. + const IS_LEGACY_SRGB = 1 << 4; + } +} + +/// An absolutely specified color, using either rgb(), rgba(), lab(), lch(), +/// oklab(), oklch() or color(). +#[derive(Copy, Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] +#[repr(C)] +pub struct AbsoluteColor { + /// The 3 components that make up colors in any color space. + pub components: ColorComponents, + /// The alpha component of the color. + pub alpha: f32, + /// The current color space that the components represent. + pub color_space: ColorSpace, + /// Extra flags used durring serialization of this color. + pub flags: ColorFlags, +} + +/// Given an [`AbsoluteColor`], return the 4 float components as the type given, +/// e.g.: +/// +/// ```rust +/// let srgb = AbsoluteColor::new(ColorSpace::Srgb, 1.0, 0.0, 0.0, 0.0); +/// let floats = color_components_as!(&srgb, [f32; 4]); // [1.0, 0.0, 0.0, 0.0] +/// ``` +macro_rules! color_components_as { + ($c:expr, $t:ty) => {{ + // This macro is not an inline function, because we can't use the + // generic type ($t) in a constant expression as per: + // https://github.com/rust-lang/rust/issues/76560 + const_assert_eq!(std::mem::size_of::<$t>(), std::mem::size_of::<[f32; 4]>()); + const_assert_eq!(std::mem::align_of::<$t>(), std::mem::align_of::<[f32; 4]>()); + const_assert!(std::mem::size_of::() >= std::mem::size_of::<$t>()); + const_assert_eq!( + std::mem::align_of::(), + std::mem::align_of::<$t>() + ); + + std::mem::transmute::<&ColorComponents, &$t>(&$c.components) + }}; +} + +/// Holds details about each component passed into creating a new [`AbsoluteColor`]. +pub struct ComponentDetails { + value: f32, + is_none: bool, +} + +impl From for ComponentDetails { + fn from(value: f32) -> Self { + Self { + value, + is_none: false, + } + } +} + +impl From for ComponentDetails { + fn from(value: u8) -> Self { + Self { + value: value as f32 / 255.0, + is_none: false, + } + } +} + +impl From> for ComponentDetails { + fn from(value: Option) -> Self { + if let Some(value) = value { + Self { + value, + is_none: false, + } + } else { + Self { + value: 0.0, + is_none: true, + } + } + } +} + +impl AbsoluteColor { + /// A fully transparent color in the legacy syntax. + pub const TRANSPARENT_BLACK: Self = Self { + components: ColorComponents(0.0, 0.0, 0.0), + alpha: 0.0, + color_space: ColorSpace::Srgb, + flags: ColorFlags::IS_LEGACY_SRGB, + }; + + /// An opaque black color in the legacy syntax. + pub const BLACK: Self = Self { + components: ColorComponents(0.0, 0.0, 0.0), + alpha: 1.0, + color_space: ColorSpace::Srgb, + flags: ColorFlags::IS_LEGACY_SRGB, + }; + + /// An opaque white color in the legacy syntax. + pub const WHITE: Self = Self { + components: ColorComponents(1.0, 1.0, 1.0), + alpha: 1.0, + color_space: ColorSpace::Srgb, + flags: ColorFlags::IS_LEGACY_SRGB, + }; + + /// Create a new [`AbsoluteColor`] with the given [`ColorSpace`] and + /// components. + pub fn new( + color_space: ColorSpace, + c1: impl Into, + c2: impl Into, + c3: impl Into, + alpha: impl Into, + ) -> Self { + let mut flags = ColorFlags::empty(); + + macro_rules! cd { + ($c:expr,$flag:expr) => {{ + let component_details = $c.into(); + if component_details.is_none { + flags |= $flag; + } + component_details.value + }}; + } + + let mut components = ColorComponents( + cd!(c1, ColorFlags::C0_IS_NONE), + cd!(c2, ColorFlags::C1_IS_NONE), + cd!(c3, ColorFlags::C2_IS_NONE), + ); + + let alpha = cd!(alpha, ColorFlags::ALPHA_IS_NONE); + + // Lightness for Lab and Lch is clamped to [0..100]. + if matches!(color_space, ColorSpace::Lab | ColorSpace::Lch) { + components.0 = components.0.clamp(0.0, 100.0); + } + + // Lightness for Oklab and Oklch is clamped to [0..1]. + if matches!(color_space, ColorSpace::Oklab | ColorSpace::Oklch) { + components.0 = components.0.clamp(0.0, 1.0); + } + + // Chroma must not be less than 0. + if matches!(color_space, ColorSpace::Lch | ColorSpace::Oklch) { + components.1 = components.1.max(0.0); + } + + // Alpha is always clamped to [0..1]. + let alpha = alpha.clamp(0.0, 1.0); + + Self { + components, + alpha, + color_space, + flags, + } + } + + /// Convert this color into the sRGB color space and set it to the legacy + /// syntax. + #[inline] + #[must_use] + pub fn into_srgb_legacy(self) -> Self { + let mut result = if !matches!(self.color_space, ColorSpace::Srgb) { + self.to_color_space(ColorSpace::Srgb) + } else { + self + }; + + // Explicitly set the flags to IS_LEGACY_SRGB only to clear out the + // *_IS_NONE flags, because the legacy syntax doesn't allow "none". + result.flags = ColorFlags::IS_LEGACY_SRGB; + + result + } + + /// Create a new [`AbsoluteColor`] from rgba legacy syntax values in the sRGB color space. + pub fn srgb_legacy(red: u8, green: u8, blue: u8, alpha: f32) -> Self { + let mut result = Self::new(ColorSpace::Srgb, red, green, blue, alpha); + result.flags = ColorFlags::IS_LEGACY_SRGB; + result + } + + /// Return all the components of the color in an array. (Includes alpha) + #[inline] + pub fn raw_components(&self) -> &[f32; 4] { + unsafe { color_components_as!(self, [f32; 4]) } + } + + /// Returns true if this color is in the legacy color syntax. + #[inline] + pub fn is_legacy_syntax(&self) -> bool { + // rgb(), rgba(), hsl(), hsla(), hwb(), hwba() + match self.color_space { + ColorSpace::Srgb => self.flags.contains(ColorFlags::IS_LEGACY_SRGB), + ColorSpace::Hsl | ColorSpace::Hwb => true, + _ => false, + } + } + + /// Returns true if this color is fully transparent. + #[inline] + pub fn is_transparent(&self) -> bool { + self.flags.contains(ColorFlags::ALPHA_IS_NONE) || self.alpha == 0.0 + } + + /// Return an optional first component. + #[inline] + pub fn c0(&self) -> Option { + if self.flags.contains(ColorFlags::C0_IS_NONE) { + None + } else { + Some(self.components.0) + } + } + + /// Return an optional second component. + #[inline] + pub fn c1(&self) -> Option { + if self.flags.contains(ColorFlags::C1_IS_NONE) { + None + } else { + Some(self.components.1) + } + } + + /// Return an optional second component. + #[inline] + pub fn c2(&self) -> Option { + if self.flags.contains(ColorFlags::C2_IS_NONE) { + None + } else { + Some(self.components.2) + } + } + + /// Return an optional alpha component. + #[inline] + pub fn alpha(&self) -> Option { + if self.flags.contains(ColorFlags::ALPHA_IS_NONE) { + None + } else { + Some(self.alpha) + } + } + + /// Convert this color to the specified color space. + pub fn to_color_space(&self, color_space: ColorSpace) -> Self { + use ColorSpace::*; + + if self.color_space == color_space { + return self.clone(); + } + + // Conversion functions doesn't handle NAN component values, so they are + // converted to 0.0. They do however need to know if a component is + // missing, so we use NAN as the marker for that. + macro_rules! missing_to_nan { + ($c:expr) => {{ + if let Some(v) = $c { + crate::values::normalize(v) + } else { + f32::NAN + } + }}; + } + + let components = ColorComponents( + missing_to_nan!(self.c0()), + missing_to_nan!(self.c1()), + missing_to_nan!(self.c2()), + ); + + let result = match (self.color_space, color_space) { + // We have simplified conversions that do not need to convert to XYZ + // first. This improves performance, because it skips at least 2 + // matrix multiplications and reduces float rounding errors. + (Srgb, Hsl) => convert::rgb_to_hsl(&components), + (Srgb, Hwb) => convert::rgb_to_hwb(&components), + (Hsl, Srgb) => convert::hsl_to_rgb(&components), + (Hwb, Srgb) => convert::hwb_to_rgb(&components), + (Lab, Lch) | (Oklab, Oklch) => convert::orthogonal_to_polar(&components), + (Lch, Lab) | (Oklch, Oklab) => convert::polar_to_orthogonal(&components), + + // All other conversions need to convert to XYZ first. + _ => { + let (xyz, white_point) = match self.color_space { + Lab => convert::to_xyz::(&components), + Lch => convert::to_xyz::(&components), + Oklab => convert::to_xyz::(&components), + Oklch => convert::to_xyz::(&components), + Srgb => convert::to_xyz::(&components), + Hsl => convert::to_xyz::(&components), + Hwb => convert::to_xyz::(&components), + SrgbLinear => convert::to_xyz::(&components), + DisplayP3 => convert::to_xyz::(&components), + A98Rgb => convert::to_xyz::(&components), + ProphotoRgb => convert::to_xyz::(&components), + Rec2020 => convert::to_xyz::(&components), + XyzD50 => convert::to_xyz::(&components), + XyzD65 => convert::to_xyz::(&components), + }; + + match color_space { + Lab => convert::from_xyz::(&xyz, white_point), + Lch => convert::from_xyz::(&xyz, white_point), + Oklab => convert::from_xyz::(&xyz, white_point), + Oklch => convert::from_xyz::(&xyz, white_point), + Srgb => convert::from_xyz::(&xyz, white_point), + Hsl => convert::from_xyz::(&xyz, white_point), + Hwb => convert::from_xyz::(&xyz, white_point), + SrgbLinear => convert::from_xyz::(&xyz, white_point), + DisplayP3 => convert::from_xyz::(&xyz, white_point), + A98Rgb => convert::from_xyz::(&xyz, white_point), + ProphotoRgb => convert::from_xyz::(&xyz, white_point), + Rec2020 => convert::from_xyz::(&xyz, white_point), + XyzD50 => convert::from_xyz::(&xyz, white_point), + XyzD65 => convert::from_xyz::(&xyz, white_point), + } + }, + }; + + // A NAN value coming from a conversion function means the the component + // is missing, so we convert it to None. + macro_rules! nan_to_missing { + ($v:expr) => {{ + if $v.is_nan() { + None + } else { + Some($v) + } + }}; + } + + Self::new( + color_space, + nan_to_missing!(result.0), + nan_to_missing!(result.1), + nan_to_missing!(result.2), + self.alpha(), + ) + } +} + +impl From for ColorSpace { + fn from(value: PredefinedColorSpace) -> Self { + match value { + PredefinedColorSpace::Srgb => ColorSpace::Srgb, + PredefinedColorSpace::SrgbLinear => ColorSpace::SrgbLinear, + PredefinedColorSpace::DisplayP3 => ColorSpace::DisplayP3, + PredefinedColorSpace::A98Rgb => ColorSpace::A98Rgb, + PredefinedColorSpace::ProphotoRgb => ColorSpace::ProphotoRgb, + PredefinedColorSpace::Rec2020 => ColorSpace::Rec2020, + PredefinedColorSpace::XyzD50 => ColorSpace::XyzD50, + PredefinedColorSpace::XyzD65 => ColorSpace::XyzD65, + } + } +} + +impl ToCss for AbsoluteColor { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: Write, + { + match self.color_space { + ColorSpace::Srgb if self.flags.contains(ColorFlags::IS_LEGACY_SRGB) => { + // The "none" keyword is not supported in the rgb/rgba legacy syntax. + cssparser::ToCss::to_css( + &parsing::RgbaLegacy::from_floats( + self.components.0, + self.components.1, + self.components.2, + self.alpha, + ), + dest, + ) + }, + ColorSpace::Hsl | ColorSpace::Hwb => self.into_srgb_legacy().to_css(dest), + ColorSpace::Lab => cssparser::ToCss::to_css( + &parsing::Lab::new(self.c0(), self.c1(), self.c2(), self.alpha()), + dest, + ), + ColorSpace::Lch => cssparser::ToCss::to_css( + &parsing::Lch::new(self.c0(), self.c1(), self.c2(), self.alpha()), + dest, + ), + ColorSpace::Oklab => cssparser::ToCss::to_css( + &parsing::Oklab::new(self.c0(), self.c1(), self.c2(), self.alpha()), + dest, + ), + ColorSpace::Oklch => cssparser::ToCss::to_css( + &parsing::Oklch::new(self.c0(), self.c1(), self.c2(), self.alpha()), + dest, + ), + _ => { + let color_space = match self.color_space { + ColorSpace::Srgb => { + debug_assert!( + !self.flags.contains(ColorFlags::IS_LEGACY_SRGB), + "legacy srgb is not a color function" + ); + PredefinedColorSpace::Srgb + }, + ColorSpace::SrgbLinear => PredefinedColorSpace::SrgbLinear, + ColorSpace::DisplayP3 => PredefinedColorSpace::DisplayP3, + ColorSpace::A98Rgb => PredefinedColorSpace::A98Rgb, + ColorSpace::ProphotoRgb => PredefinedColorSpace::ProphotoRgb, + ColorSpace::Rec2020 => PredefinedColorSpace::Rec2020, + ColorSpace::XyzD50 => PredefinedColorSpace::XyzD50, + ColorSpace::XyzD65 => PredefinedColorSpace::XyzD65, + + _ => { + unreachable!("other color spaces do not support color() syntax") + }, + }; + + let color_function = parsing::ColorFunction { + color_space, + c1: self.c0(), + c2: self.c1(), + c3: self.c2(), + alpha: self.alpha(), + }; + let color = parsing::Color::ColorFunction(color_function); + cssparser::ToCss::to_css(&color, dest) + }, + } + } +} diff --git a/servo/components/style/color/parsing.rs b/servo/components/style/color/parsing.rs new file mode 100644 index 0000000000..f60b44c5b6 --- /dev/null +++ b/servo/components/style/color/parsing.rs @@ -0,0 +1,1246 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#![deny(missing_docs)] + +//! Fairly complete css-color implementation. +//! Relative colors, color-mix, system colors, and other such things require better calc() support +//! and integration. + +use super::{ + convert::{hsl_to_rgb, hwb_to_rgb, normalize_hue}, + ColorComponents, +}; +use crate::values::normalize; +use cssparser::color::{ + clamp_floor_256_f32, clamp_unit_f32, parse_hash_color, serialize_color_alpha, + PredefinedColorSpace, OPAQUE, +}; +use cssparser::{match_ignore_ascii_case, CowRcStr, ParseError, Parser, ToCss, Token}; +#[cfg(feature = "serde")] +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::f32::consts::PI; +use std::fmt; +use std::str::FromStr; + +/// Return the named color with the given name. +/// +/// Matching is case-insensitive in the ASCII range. +/// CSS escaping (if relevant) should be resolved before calling this function. +/// (For example, the value of an `Ident` token is fine.) +#[inline] +pub fn parse_color_keyword(ident: &str) -> Result +where + Output: FromParsedColor, +{ + Ok(match_ignore_ascii_case! { ident , + "transparent" => Output::from_rgba(0, 0, 0, 0.0), + "currentcolor" => Output::from_current_color(), + _ => { + let (r, g, b) = cssparser::color::parse_named_color(ident)?; + Output::from_rgba(r, g, b, OPAQUE) + } + }) +} + +/// Parse a CSS color using the specified [`ColorParser`] and return a new color +/// value on success. +pub fn parse_color_with<'i, 't, P>( + color_parser: &P, + input: &mut Parser<'i, 't>, +) -> Result> +where + P: ColorParser<'i>, +{ + let location = input.current_source_location(); + let token = input.next()?; + match *token { + Token::Hash(ref value) | Token::IDHash(ref value) => { + parse_hash_color(value.as_bytes()).map(|(r, g, b, a)| P::Output::from_rgba(r, g, b, a)) + }, + Token::Ident(ref value) => parse_color_keyword(value), + Token::Function(ref name) => { + let name = name.clone(); + return input.parse_nested_block(|arguments| { + parse_color_function(color_parser, name, arguments) + }); + }, + _ => Err(()), + } + .map_err(|()| location.new_unexpected_token_error(token.clone())) +} + +/// Parse one of the color functions: rgba(), lab(), color(), etc. +#[inline] +fn parse_color_function<'i, 't, P>( + color_parser: &P, + name: CowRcStr<'i>, + arguments: &mut Parser<'i, 't>, +) -> Result> +where + P: ColorParser<'i>, +{ + let color = match_ignore_ascii_case! { &name, + "rgb" | "rgba" => parse_rgb(color_parser, arguments), + + "hsl" | "hsla" => parse_hsl(color_parser, arguments), + + "hwb" => parse_hwb(color_parser, arguments), + + // for L: 0% = 0.0, 100% = 100.0 + // for a and b: -100% = -125, 100% = 125 + "lab" => parse_lab_like(color_parser, arguments, 100.0, 125.0, P::Output::from_lab), + + // for L: 0% = 0.0, 100% = 100.0 + // for C: 0% = 0, 100% = 150 + "lch" => parse_lch_like(color_parser, arguments, 100.0, 150.0, P::Output::from_lch), + + // for L: 0% = 0.0, 100% = 1.0 + // for a and b: -100% = -0.4, 100% = 0.4 + "oklab" => parse_lab_like(color_parser, arguments, 1.0, 0.4, P::Output::from_oklab), + + // for L: 0% = 0.0, 100% = 1.0 + // for C: 0% = 0.0 100% = 0.4 + "oklch" => parse_lch_like(color_parser, arguments, 1.0, 0.4, P::Output::from_oklch), + + "color" => parse_color_with_color_space(color_parser, arguments), + + _ => return Err(arguments.new_unexpected_token_error(Token::Ident(name))), + }?; + + arguments.expect_exhausted()?; + + Ok(color) +} + +/// Parse the alpha component by itself from either number or percentage, +/// clipping the result to [0.0..1.0]. +#[inline] +fn parse_alpha_component<'i, 't, P>( + color_parser: &P, + arguments: &mut Parser<'i, 't>, +) -> Result> +where + P: ColorParser<'i>, +{ + // Percent reference range for alpha: 0% = 0.0, 100% = 1.0 + let alpha = color_parser + .parse_number_or_percentage(arguments)? + .to_number(1.0); + Ok(normalize(alpha).clamp(0.0, OPAQUE)) +} + +fn parse_legacy_alpha<'i, 't, P>( + color_parser: &P, + arguments: &mut Parser<'i, 't>, +) -> Result> +where + P: ColorParser<'i>, +{ + Ok(if !arguments.is_exhausted() { + arguments.expect_comma()?; + parse_alpha_component(color_parser, arguments)? + } else { + OPAQUE + }) +} + +fn parse_modern_alpha<'i, 't, P>( + color_parser: &P, + arguments: &mut Parser<'i, 't>, +) -> Result, ParseError<'i, P::Error>> +where + P: ColorParser<'i>, +{ + if !arguments.is_exhausted() { + arguments.expect_delim('/')?; + parse_none_or(arguments, |p| parse_alpha_component(color_parser, p)) + } else { + Ok(Some(OPAQUE)) + } +} + +#[inline] +fn parse_rgb<'i, 't, P>( + color_parser: &P, + arguments: &mut Parser<'i, 't>, +) -> Result> +where + P: ColorParser<'i>, +{ + let maybe_red = parse_none_or(arguments, |p| color_parser.parse_number_or_percentage(p))?; + + // If the first component is not "none" and is followed by a comma, then we + // are parsing the legacy syntax. + let is_legacy_syntax = maybe_red.is_some() && arguments.try_parse(|p| p.expect_comma()).is_ok(); + + let (red, green, blue, alpha) = if is_legacy_syntax { + let (red, green, blue) = match maybe_red.unwrap() { + NumberOrPercentage::Number { value } => { + let red = clamp_floor_256_f32(value); + let green = clamp_floor_256_f32(color_parser.parse_number(arguments)?); + arguments.expect_comma()?; + let blue = clamp_floor_256_f32(color_parser.parse_number(arguments)?); + (red, green, blue) + }, + NumberOrPercentage::Percentage { unit_value } => { + let red = clamp_unit_f32(unit_value); + let green = clamp_unit_f32(color_parser.parse_percentage(arguments)?); + arguments.expect_comma()?; + let blue = clamp_unit_f32(color_parser.parse_percentage(arguments)?); + (red, green, blue) + }, + }; + + let alpha = parse_legacy_alpha(color_parser, arguments)?; + + (red, green, blue, alpha) + } else { + #[inline] + fn get_component_value(c: Option) -> u8 { + c.map(|c| match c { + NumberOrPercentage::Number { value } => clamp_floor_256_f32(value), + NumberOrPercentage::Percentage { unit_value } => clamp_unit_f32(unit_value), + }) + .unwrap_or(0) + } + + let red = get_component_value(maybe_red); + + let green = get_component_value(parse_none_or(arguments, |p| { + color_parser.parse_number_or_percentage(p) + })?); + + let blue = get_component_value(parse_none_or(arguments, |p| { + color_parser.parse_number_or_percentage(p) + })?); + + let alpha = parse_modern_alpha(color_parser, arguments)?.unwrap_or(0.0); + + (red, green, blue, alpha) + }; + + Ok(P::Output::from_rgba(red, green, blue, alpha)) +} + +/// Parses hsl syntax. +/// +/// +#[inline] +fn parse_hsl<'i, 't, P>( + color_parser: &P, + arguments: &mut Parser<'i, 't>, +) -> Result> +where + P: ColorParser<'i>, +{ + // Percent reference range for S and L: 0% = 0.0, 100% = 100.0 + const LIGHTNESS_RANGE: f32 = 100.0; + const SATURATION_RANGE: f32 = 100.0; + + let maybe_hue = parse_none_or(arguments, |p| color_parser.parse_angle_or_number(p))?; + + // If the hue is not "none" and is followed by a comma, then we are parsing + // the legacy syntax. + let is_legacy_syntax = maybe_hue.is_some() && arguments.try_parse(|p| p.expect_comma()).is_ok(); + + let saturation: Option; + let lightness: Option; + + let alpha = if is_legacy_syntax { + saturation = Some(color_parser.parse_percentage(arguments)? * SATURATION_RANGE); + arguments.expect_comma()?; + lightness = Some(color_parser.parse_percentage(arguments)? * LIGHTNESS_RANGE); + Some(parse_legacy_alpha(color_parser, arguments)?) + } else { + saturation = parse_none_or(arguments, |p| color_parser.parse_number_or_percentage(p))? + .map(|v| v.to_number(SATURATION_RANGE)); + lightness = parse_none_or(arguments, |p| color_parser.parse_number_or_percentage(p))? + .map(|v| v.to_number(LIGHTNESS_RANGE)); + parse_modern_alpha(color_parser, arguments)? + }; + + let hue = maybe_hue.map(|h| normalize_hue(h.degrees())); + let saturation = saturation.map(|s| s.clamp(0.0, SATURATION_RANGE)); + let lightness = lightness.map(|l| l.clamp(0.0, LIGHTNESS_RANGE)); + + Ok(P::Output::from_hsl(hue, saturation, lightness, alpha)) +} + +/// Parses hwb syntax. +/// +/// +#[inline] +fn parse_hwb<'i, 't, P>( + color_parser: &P, + arguments: &mut Parser<'i, 't>, +) -> Result> +where + P: ColorParser<'i>, +{ + // Percent reference range for W and B: 0% = 0.0, 100% = 100.0 + const WHITENESS_RANGE: f32 = 100.0; + const BLACKNESS_RANGE: f32 = 100.0; + + let (hue, whiteness, blackness, alpha) = parse_components( + color_parser, + arguments, + P::parse_angle_or_number, + P::parse_number_or_percentage, + P::parse_number_or_percentage, + )?; + + let hue = hue.map(|h| normalize_hue(h.degrees())); + let whiteness = whiteness.map(|w| w.to_number(WHITENESS_RANGE).clamp(0.0, WHITENESS_RANGE)); + let blackness = blackness.map(|b| b.to_number(BLACKNESS_RANGE).clamp(0.0, BLACKNESS_RANGE)); + + Ok(P::Output::from_hwb(hue, whiteness, blackness, alpha)) +} + +type IntoColorFn = + fn(l: Option, a: Option, b: Option, alpha: Option) -> Output; + +#[inline] +fn parse_lab_like<'i, 't, P>( + color_parser: &P, + arguments: &mut Parser<'i, 't>, + lightness_range: f32, + a_b_range: f32, + into_color: IntoColorFn, +) -> Result> +where + P: ColorParser<'i>, +{ + let (lightness, a, b, alpha) = parse_components( + color_parser, + arguments, + P::parse_number_or_percentage, + P::parse_number_or_percentage, + P::parse_number_or_percentage, + )?; + + let lightness = lightness.map(|l| l.to_number(lightness_range)); + let a = a.map(|a| a.to_number(a_b_range)); + let b = b.map(|b| b.to_number(a_b_range)); + + Ok(into_color(lightness, a, b, alpha)) +} + +#[inline] +fn parse_lch_like<'i, 't, P>( + color_parser: &P, + arguments: &mut Parser<'i, 't>, + lightness_range: f32, + chroma_range: f32, + into_color: IntoColorFn, +) -> Result> +where + P: ColorParser<'i>, +{ + let (lightness, chroma, hue, alpha) = parse_components( + color_parser, + arguments, + P::parse_number_or_percentage, + P::parse_number_or_percentage, + P::parse_angle_or_number, + )?; + + let lightness = lightness.map(|l| l.to_number(lightness_range)); + let chroma = chroma.map(|c| c.to_number(chroma_range)); + let hue = hue.map(|h| normalize_hue(h.degrees())); + + Ok(into_color(lightness, chroma, hue, alpha)) +} + +/// Parse the color() function. +#[inline] +fn parse_color_with_color_space<'i, 't, P>( + color_parser: &P, + arguments: &mut Parser<'i, 't>, +) -> Result> +where + P: ColorParser<'i>, +{ + let color_space = { + let location = arguments.current_source_location(); + + let ident = arguments.expect_ident()?; + PredefinedColorSpace::from_str(ident) + .map_err(|_| location.new_unexpected_token_error(Token::Ident(ident.clone())))? + }; + + let (c1, c2, c3, alpha) = parse_components( + color_parser, + arguments, + P::parse_number_or_percentage, + P::parse_number_or_percentage, + P::parse_number_or_percentage, + )?; + + let c1 = c1.map(|c| c.to_number(1.0)); + let c2 = c2.map(|c| c.to_number(1.0)); + let c3 = c3.map(|c| c.to_number(1.0)); + + Ok(P::Output::from_color_function( + color_space, + c1, + c2, + c3, + alpha, + )) +} + +type ComponentParseResult<'i, R1, R2, R3, Error> = + Result<(Option, Option, Option, Option), ParseError<'i, Error>>; + +/// Parse the color components and alpha with the modern [color-4] syntax. +pub fn parse_components<'i, 't, P, F1, F2, F3, R1, R2, R3>( + color_parser: &P, + input: &mut Parser<'i, 't>, + f1: F1, + f2: F2, + f3: F3, +) -> ComponentParseResult<'i, R1, R2, R3, P::Error> +where + P: ColorParser<'i>, + F1: FnOnce(&P, &mut Parser<'i, 't>) -> Result>, + F2: FnOnce(&P, &mut Parser<'i, 't>) -> Result>, + F3: FnOnce(&P, &mut Parser<'i, 't>) -> Result>, +{ + let r1 = parse_none_or(input, |p| f1(color_parser, p))?; + let r2 = parse_none_or(input, |p| f2(color_parser, p))?; + let r3 = parse_none_or(input, |p| f3(color_parser, p))?; + + let alpha = parse_modern_alpha(color_parser, input)?; + + Ok((r1, r2, r3, alpha)) +} + +fn parse_none_or<'i, 't, F, T, E>(input: &mut Parser<'i, 't>, thing: F) -> Result, E> +where + F: FnOnce(&mut Parser<'i, 't>) -> Result, +{ + match input.try_parse(|p| p.expect_ident_matching("none")) { + Ok(_) => Ok(None), + Err(_) => Ok(Some(thing(input)?)), + } +} + +/// A [`ModernComponent`] can serialize to `none`, `nan`, `infinity` and +/// floating point values. +struct ModernComponent<'a>(&'a Option); + +impl<'a> ToCss for ModernComponent<'a> { + fn to_css(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + if let Some(value) = self.0 { + if value.is_finite() { + value.to_css(dest) + } else if value.is_nan() { + dest.write_str("calc(NaN)") + } else { + debug_assert!(value.is_infinite()); + if value.is_sign_negative() { + dest.write_str("calc(-infinity)") + } else { + dest.write_str("calc(infinity)") + } + } + } else { + dest.write_str("none") + } + } +} + +/// A color with red, green, blue, and alpha components, in a byte each. +#[derive(Clone, Copy, PartialEq, Debug)] +pub struct RgbaLegacy { + /// The red component. + pub red: u8, + /// The green component. + pub green: u8, + /// The blue component. + pub blue: u8, + /// The alpha component. + pub alpha: f32, +} + +impl RgbaLegacy { + /// Constructs a new RGBA value from float components. It expects the red, + /// green, blue and alpha channels in that order, and all values will be + /// clamped to the 0.0 ... 1.0 range. + #[inline] + pub fn from_floats(red: f32, green: f32, blue: f32, alpha: f32) -> Self { + Self::new( + clamp_unit_f32(red), + clamp_unit_f32(green), + clamp_unit_f32(blue), + alpha.clamp(0.0, OPAQUE), + ) + } + + /// Same thing, but with `u8` values instead of floats in the 0 to 1 range. + #[inline] + pub const fn new(red: u8, green: u8, blue: u8, alpha: f32) -> Self { + Self { + red, + green, + blue, + alpha, + } + } +} + +#[cfg(feature = "serde")] +impl Serialize for RgbaLegacy { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + (self.red, self.green, self.blue, self.alpha).serialize(serializer) + } +} + +#[cfg(feature = "serde")] +impl<'de> Deserialize<'de> for RgbaLegacy { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let (r, g, b, a) = Deserialize::deserialize(deserializer)?; + Ok(RgbaLegacy::new(r, g, b, a)) + } +} + +impl ToCss for RgbaLegacy { + fn to_css(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + let has_alpha = self.alpha != OPAQUE; + + dest.write_str(if has_alpha { "rgba(" } else { "rgb(" })?; + self.red.to_css(dest)?; + dest.write_str(", ")?; + self.green.to_css(dest)?; + dest.write_str(", ")?; + self.blue.to_css(dest)?; + + // Legacy syntax does not allow none components. + serialize_color_alpha(dest, Some(self.alpha), true)?; + + dest.write_char(')') + } +} + +/// Color specified by hue, saturation and lightness components. +#[derive(Clone, Copy, PartialEq, Debug)] +pub struct Hsl { + /// The hue component. + pub hue: Option, + /// The saturation component. + pub saturation: Option, + /// The lightness component. + pub lightness: Option, + /// The alpha component. + pub alpha: Option, +} + +impl Hsl { + /// Construct a new HSL color from it's components. + pub fn new( + hue: Option, + saturation: Option, + lightness: Option, + alpha: Option, + ) -> Self { + Self { + hue, + saturation, + lightness, + alpha, + } + } +} + +impl ToCss for Hsl { + fn to_css(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + // HSL serializes to RGB, so we have to convert it. + let ColorComponents(red, green, blue) = hsl_to_rgb(&ColorComponents( + self.hue.unwrap_or(0.0) / 360.0, + self.saturation.unwrap_or(0.0), + self.lightness.unwrap_or(0.0), + )); + + RgbaLegacy::from_floats(red, green, blue, self.alpha.unwrap_or(OPAQUE)).to_css(dest) + } +} + +#[cfg(feature = "serde")] +impl Serialize for Hsl { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + (self.hue, self.saturation, self.lightness, self.alpha).serialize(serializer) + } +} + +#[cfg(feature = "serde")] +impl<'de> Deserialize<'de> for Hsl { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let (lightness, a, b, alpha) = Deserialize::deserialize(deserializer)?; + Ok(Self::new(lightness, a, b, alpha)) + } +} + +/// Color specified by hue, whiteness and blackness components. +#[derive(Clone, Copy, PartialEq, Debug)] +pub struct Hwb { + /// The hue component. + pub hue: Option, + /// The whiteness component. + pub whiteness: Option, + /// The blackness component. + pub blackness: Option, + /// The alpha component. + pub alpha: Option, +} + +impl Hwb { + /// Construct a new HWB color from it's components. + pub fn new( + hue: Option, + whiteness: Option, + blackness: Option, + alpha: Option, + ) -> Self { + Self { + hue, + whiteness, + blackness, + alpha, + } + } +} + +impl ToCss for Hwb { + fn to_css(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + // HWB serializes to RGB, so we have to convert it. + let ColorComponents(red, green, blue) = hwb_to_rgb(&ColorComponents( + self.hue.unwrap_or(0.0) / 360.0, + self.whiteness.unwrap_or(0.0), + self.blackness.unwrap_or(0.0), + )); + + RgbaLegacy::from_floats(red, green, blue, self.alpha.unwrap_or(OPAQUE)).to_css(dest) + } +} + +#[cfg(feature = "serde")] +impl Serialize for Hwb { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + (self.hue, self.whiteness, self.blackness, self.alpha).serialize(serializer) + } +} + +#[cfg(feature = "serde")] +impl<'de> Deserialize<'de> for Hwb { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let (lightness, whiteness, blackness, alpha) = Deserialize::deserialize(deserializer)?; + Ok(Self::new(lightness, whiteness, blackness, alpha)) + } +} + +// NOTE: LAB and OKLAB is not declared inside the [impl_lab_like] macro, +// because it causes cbindgen to ignore them. + +/// Color specified by lightness, a- and b-axis components. +#[derive(Clone, Copy, PartialEq, Debug)] +pub struct Lab { + /// The lightness component. + pub lightness: Option, + /// The a-axis component. + pub a: Option, + /// The b-axis component. + pub b: Option, + /// The alpha component. + pub alpha: Option, +} + +/// Color specified by lightness, a- and b-axis components. +#[derive(Clone, Copy, PartialEq, Debug)] +pub struct Oklab { + /// The lightness component. + pub lightness: Option, + /// The a-axis component. + pub a: Option, + /// The b-axis component. + pub b: Option, + /// The alpha component. + pub alpha: Option, +} + +macro_rules! impl_lab_like { + ($cls:ident, $fname:literal) => { + impl $cls { + /// Construct a new Lab color format with lightness, a, b and alpha components. + pub fn new( + lightness: Option, + a: Option, + b: Option, + alpha: Option, + ) -> Self { + Self { + lightness, + a, + b, + alpha, + } + } + } + + #[cfg(feature = "serde")] + impl Serialize for $cls { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + (self.lightness, self.a, self.b, self.alpha).serialize(serializer) + } + } + + #[cfg(feature = "serde")] + impl<'de> Deserialize<'de> for $cls { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let (lightness, a, b, alpha) = Deserialize::deserialize(deserializer)?; + Ok(Self::new(lightness, a, b, alpha)) + } + } + + impl ToCss for $cls { + fn to_css(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + dest.write_str($fname)?; + dest.write_str("(")?; + ModernComponent(&self.lightness).to_css(dest)?; + dest.write_char(' ')?; + ModernComponent(&self.a).to_css(dest)?; + dest.write_char(' ')?; + ModernComponent(&self.b).to_css(dest)?; + serialize_color_alpha(dest, self.alpha, false)?; + dest.write_char(')') + } + } + }; +} + +impl_lab_like!(Lab, "lab"); +impl_lab_like!(Oklab, "oklab"); + +// NOTE: LCH and OKLCH is not declared inside the [impl_lch_like] macro, +// because it causes cbindgen to ignore them. + +/// Color specified by lightness, chroma and hue components. +#[derive(Clone, Copy, PartialEq, Debug)] +pub struct Lch { + /// The lightness component. + pub lightness: Option, + /// The chroma component. + pub chroma: Option, + /// The hue component. + pub hue: Option, + /// The alpha component. + pub alpha: Option, +} + +/// Color specified by lightness, chroma and hue components. +#[derive(Clone, Copy, PartialEq, Debug)] +pub struct Oklch { + /// The lightness component. + pub lightness: Option, + /// The chroma component. + pub chroma: Option, + /// The hue component. + pub hue: Option, + /// The alpha component. + pub alpha: Option, +} + +macro_rules! impl_lch_like { + ($cls:ident, $fname:literal) => { + impl $cls { + /// Construct a new color with lightness, chroma and hue components. + pub fn new( + lightness: Option, + chroma: Option, + hue: Option, + alpha: Option, + ) -> Self { + Self { + lightness, + chroma, + hue, + alpha, + } + } + } + + #[cfg(feature = "serde")] + impl Serialize for $cls { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + (self.lightness, self.chroma, self.hue, self.alpha).serialize(serializer) + } + } + + #[cfg(feature = "serde")] + impl<'de> Deserialize<'de> for $cls { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let (lightness, chroma, hue, alpha) = Deserialize::deserialize(deserializer)?; + Ok(Self::new(lightness, chroma, hue, alpha)) + } + } + + impl ToCss for $cls { + fn to_css(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + dest.write_str($fname)?; + dest.write_str("(")?; + ModernComponent(&self.lightness).to_css(dest)?; + dest.write_char(' ')?; + ModernComponent(&self.chroma).to_css(dest)?; + dest.write_char(' ')?; + ModernComponent(&self.hue).to_css(dest)?; + serialize_color_alpha(dest, self.alpha, false)?; + dest.write_char(')') + } + } + }; +} + +impl_lch_like!(Lch, "lch"); +impl_lch_like!(Oklch, "oklch"); + +/// A color specified by the color() function. +/// +#[derive(Clone, Copy, PartialEq, Debug)] +pub struct ColorFunction { + /// The color space for this color. + pub color_space: PredefinedColorSpace, + /// The first component of the color. Either red or x. + pub c1: Option, + /// The second component of the color. Either green or y. + pub c2: Option, + /// The third component of the color. Either blue or z. + pub c3: Option, + /// The alpha component of the color. + pub alpha: Option, +} + +impl ColorFunction { + /// Construct a new color function definition with the given color space and + /// color components. + pub fn new( + color_space: PredefinedColorSpace, + c1: Option, + c2: Option, + c3: Option, + alpha: Option, + ) -> Self { + Self { + color_space, + c1, + c2, + c3, + alpha, + } + } +} + +impl ToCss for ColorFunction { + fn to_css(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + dest.write_str("color(")?; + self.color_space.to_css(dest)?; + dest.write_char(' ')?; + ModernComponent(&self.c1).to_css(dest)?; + dest.write_char(' ')?; + ModernComponent(&self.c2).to_css(dest)?; + dest.write_char(' ')?; + ModernComponent(&self.c3).to_css(dest)?; + + serialize_color_alpha(dest, self.alpha, false)?; + + dest.write_char(')') + } +} + +/// Describes one of the value values according to the CSS +/// specification. +/// +/// Most components are `Option<_>`, so when the value is `None`, that component +/// serializes to the "none" keyword. +/// +/// +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum Color { + /// The 'currentcolor' keyword. + CurrentColor, + /// Specify sRGB colors directly by their red/green/blue/alpha chanels. + Rgba(RgbaLegacy), + /// Specifies a color in sRGB using hue, saturation and lightness components. + Hsl(Hsl), + /// Specifies a color in sRGB using hue, whiteness and blackness components. + Hwb(Hwb), + /// Specifies a CIELAB color by CIE Lightness and its a- and b-axis hue + /// coordinates (red/green-ness, and yellow/blue-ness) using the CIE LAB + /// rectangular coordinate model. + Lab(Lab), + /// Specifies a CIELAB color by CIE Lightness, Chroma, and hue using the + /// CIE LCH cylindrical coordinate model. + Lch(Lch), + /// Specifies an Oklab color by Oklab Lightness and its a- and b-axis hue + /// coordinates (red/green-ness, and yellow/blue-ness) using the Oklab + /// rectangular coordinate model. + Oklab(Oklab), + /// Specifies an Oklab color by Oklab Lightness, Chroma, and hue using + /// the OKLCH cylindrical coordinate model. + Oklch(Oklch), + /// Specifies a color in a predefined color space. + ColorFunction(ColorFunction), +} + +impl ToCss for Color { + fn to_css(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + match *self { + Color::CurrentColor => dest.write_str("currentcolor"), + Color::Rgba(rgba) => rgba.to_css(dest), + Color::Hsl(hsl) => hsl.to_css(dest), + Color::Hwb(hwb) => hwb.to_css(dest), + Color::Lab(lab) => lab.to_css(dest), + Color::Lch(lch) => lch.to_css(dest), + Color::Oklab(lab) => lab.to_css(dest), + Color::Oklch(lch) => lch.to_css(dest), + Color::ColorFunction(color_function) => color_function.to_css(dest), + } + } +} + +/// Either a number or a percentage. +pub enum NumberOrPercentage { + /// ``. + Number { + /// The numeric value parsed, as a float. + value: f32, + }, + /// `` + Percentage { + /// The value as a float, divided by 100 so that the nominal range is + /// 0.0 to 1.0. + unit_value: f32, + }, +} + +impl NumberOrPercentage { + /// Return the value as a number. Percentages will be adjusted to the range + /// [0..percent_basis]. + pub fn to_number(&self, percentage_basis: f32) -> f32 { + match *self { + Self::Number { value } => value, + Self::Percentage { unit_value } => unit_value * percentage_basis, + } + } +} + +/// Either an angle or a number. +pub enum AngleOrNumber { + /// ``. + Number { + /// The numeric value parsed, as a float. + value: f32, + }, + /// `` + Angle { + /// The value as a number of degrees. + degrees: f32, + }, +} + +impl AngleOrNumber { + /// Return the angle in degrees. `AngleOrNumber::Number` is returned as + /// degrees, because it is the canonical unit. + pub fn degrees(&self) -> f32 { + match *self { + AngleOrNumber::Number { value } => value, + AngleOrNumber::Angle { degrees } => degrees, + } + } +} + +/// A trait that can be used to hook into how `cssparser` parses color +/// components, with the intention of implementing more complicated behavior. +/// +/// For example, this is used by Servo to support calc() in color. +pub trait ColorParser<'i> { + /// The type that the parser will construct on a successful parse. + type Output: FromParsedColor; + + /// A custom error type that can be returned from the parsing functions. + type Error: 'i; + + /// Parse an `` or ``. + /// + /// Returns the result in degrees. + fn parse_angle_or_number<'t>( + &self, + input: &mut Parser<'i, 't>, + ) -> Result> { + let location = input.current_source_location(); + Ok(match *input.next()? { + Token::Number { value, .. } => AngleOrNumber::Number { value }, + Token::Dimension { + value: v, ref unit, .. + } => { + let degrees = match_ignore_ascii_case! { unit, + "deg" => v, + "grad" => v * 360. / 400., + "rad" => v * 360. / (2. * PI), + "turn" => v * 360., + _ => { + return Err(location.new_unexpected_token_error(Token::Ident(unit.clone()))) + } + }; + + AngleOrNumber::Angle { degrees } + }, + ref t => return Err(location.new_unexpected_token_error(t.clone())), + }) + } + + /// Parse a `` value. + /// + /// Returns the result in a number from 0.0 to 1.0. + fn parse_percentage<'t>( + &self, + input: &mut Parser<'i, 't>, + ) -> Result> { + input.expect_percentage().map_err(From::from) + } + + /// Parse a `` value. + fn parse_number<'t>( + &self, + input: &mut Parser<'i, 't>, + ) -> Result> { + input.expect_number().map_err(From::from) + } + + /// Parse a `` value or a `` value. + fn parse_number_or_percentage<'t>( + &self, + input: &mut Parser<'i, 't>, + ) -> Result> { + let location = input.current_source_location(); + Ok(match *input.next()? { + Token::Number { value, .. } => NumberOrPercentage::Number { value }, + Token::Percentage { unit_value, .. } => NumberOrPercentage::Percentage { unit_value }, + ref t => return Err(location.new_unexpected_token_error(t.clone())), + }) + } +} + +/// Default implementation of a [`ColorParser`] +pub struct DefaultColorParser; + +impl<'i> ColorParser<'i> for DefaultColorParser { + type Output = Color; + type Error = (); +} + +impl Color { + /// Parse a value, per CSS Color Module Level 3. + /// + /// FIXME(#2) Deprecated CSS2 System Colors are not supported yet. + pub fn parse<'i>(input: &mut Parser<'i, '_>) -> Result> { + parse_color_with(&DefaultColorParser, input) + } +} + +/// This trait is used by the [`ColorParser`] to construct colors of any type. +pub trait FromParsedColor { + /// Construct a new color from the CSS `currentcolor` keyword. + fn from_current_color() -> Self; + + /// Construct a new color from red, green, blue and alpha components. + fn from_rgba(red: u8, green: u8, blue: u8, alpha: f32) -> Self; + + /// Construct a new color from hue, saturation, lightness and alpha components. + fn from_hsl( + hue: Option, + saturation: Option, + lightness: Option, + alpha: Option, + ) -> Self; + + /// Construct a new color from hue, blackness, whiteness and alpha components. + fn from_hwb( + hue: Option, + whiteness: Option, + blackness: Option, + alpha: Option, + ) -> Self; + + /// Construct a new color from the `lab` notation. + fn from_lab(lightness: Option, a: Option, b: Option, alpha: Option) + -> Self; + + /// Construct a new color from the `lch` notation. + fn from_lch( + lightness: Option, + chroma: Option, + hue: Option, + alpha: Option, + ) -> Self; + + /// Construct a new color from the `oklab` notation. + fn from_oklab( + lightness: Option, + a: Option, + b: Option, + alpha: Option, + ) -> Self; + + /// Construct a new color from the `oklch` notation. + fn from_oklch( + lightness: Option, + chroma: Option, + hue: Option, + alpha: Option, + ) -> Self; + + /// Construct a new color with a predefined color space. + fn from_color_function( + color_space: PredefinedColorSpace, + c1: Option, + c2: Option, + c3: Option, + alpha: Option, + ) -> Self; +} + +impl FromParsedColor for Color { + #[inline] + fn from_current_color() -> Self { + Color::CurrentColor + } + + #[inline] + fn from_rgba(red: u8, green: u8, blue: u8, alpha: f32) -> Self { + Color::Rgba(RgbaLegacy::new(red, green, blue, alpha)) + } + + fn from_hsl( + hue: Option, + saturation: Option, + lightness: Option, + alpha: Option, + ) -> Self { + Color::Hsl(Hsl::new(hue, saturation, lightness, alpha)) + } + + fn from_hwb( + hue: Option, + blackness: Option, + whiteness: Option, + alpha: Option, + ) -> Self { + Color::Hwb(Hwb::new(hue, blackness, whiteness, alpha)) + } + + #[inline] + fn from_lab( + lightness: Option, + a: Option, + b: Option, + alpha: Option, + ) -> Self { + Color::Lab(Lab::new(lightness, a, b, alpha)) + } + + #[inline] + fn from_lch( + lightness: Option, + chroma: Option, + hue: Option, + alpha: Option, + ) -> Self { + Color::Lch(Lch::new(lightness, chroma, hue, alpha)) + } + + #[inline] + fn from_oklab( + lightness: Option, + a: Option, + b: Option, + alpha: Option, + ) -> Self { + Color::Oklab(Oklab::new(lightness, a, b, alpha)) + } + + #[inline] + fn from_oklch( + lightness: Option, + chroma: Option, + hue: Option, + alpha: Option, + ) -> Self { + Color::Oklch(Oklch::new(lightness, chroma, hue, alpha)) + } + + #[inline] + fn from_color_function( + color_space: PredefinedColorSpace, + c1: Option, + c2: Option, + c3: Option, + alpha: Option, + ) -> Self { + Color::ColorFunction(ColorFunction::new(color_space, c1, c2, c3, alpha)) + } +} diff --git a/servo/components/style/context.rs b/servo/components/style/context.rs new file mode 100644 index 0000000000..a2c020475b --- /dev/null +++ b/servo/components/style/context.rs @@ -0,0 +1,698 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! The context within which style is calculated. + +#[cfg(feature = "servo")] +use crate::animation::DocumentAnimationSet; +use crate::bloom::StyleBloom; +use crate::computed_value_flags::ComputedValueFlags; +use crate::data::{EagerPseudoStyles, ElementData}; +use crate::dom::{SendElement, TElement}; +#[cfg(feature = "gecko")] +use crate::gecko_bindings::structs; +use crate::parallel::{STACK_SAFETY_MARGIN_KB, STYLE_THREAD_STACK_SIZE_KB}; +use crate::properties::ComputedValues; +#[cfg(feature = "servo")] +use crate::properties::PropertyId; +use crate::rule_cache::RuleCache; +use crate::rule_tree::StrongRuleNode; +use crate::selector_parser::{SnapshotMap, EAGER_PSEUDO_COUNT}; +use crate::shared_lock::StylesheetGuards; +use crate::sharing::StyleSharingCache; +use crate::stylist::Stylist; +use crate::thread_state::{self, ThreadState}; +use crate::traversal::DomTraversal; +use crate::traversal_flags::TraversalFlags; +use app_units::Au; +use euclid::default::Size2D; +use euclid::Scale; +#[cfg(feature = "servo")] +use fxhash::FxHashMap; +use selectors::context::SelectorCaches; +#[cfg(feature = "gecko")] +use servo_arc::Arc; +#[cfg(feature = "servo")] +use servo_atoms::Atom; +use std::fmt; +use std::ops; +use style_traits::CSSPixel; +use style_traits::DevicePixel; +#[cfg(feature = "servo")] +use style_traits::SpeculativePainter; +use time; + +pub use selectors::matching::QuirksMode; + +/// A global options structure for the style system. We use this instead of +/// opts to abstract across Gecko and Servo. +#[derive(Clone)] +pub struct StyleSystemOptions { + /// Whether the style sharing cache is disabled. + pub disable_style_sharing_cache: bool, + /// Whether we should dump statistics about the style system. + pub dump_style_statistics: bool, + /// The minimum number of elements that must be traversed to trigger a dump + /// of style statistics. + pub style_statistics_threshold: usize, +} + +#[cfg(feature = "gecko")] +fn get_env_bool(name: &str) -> bool { + use std::env; + match env::var(name) { + Ok(s) => !s.is_empty(), + Err(_) => false, + } +} + +const DEFAULT_STATISTICS_THRESHOLD: usize = 50; + +#[cfg(feature = "gecko")] +fn get_env_usize(name: &str) -> Option { + use std::env; + env::var(name).ok().map(|s| { + s.parse::() + .expect("Couldn't parse environmental variable as usize") + }) +} + +/// A global variable holding the state of +/// `StyleSystemOptions::default().disable_style_sharing_cache`. +/// See [#22854](https://github.com/servo/servo/issues/22854). +#[cfg(feature = "servo")] +pub static DEFAULT_DISABLE_STYLE_SHARING_CACHE: std::sync::atomic::AtomicBool = + std::sync::atomic::AtomicBool::new(false); + +/// A global variable holding the state of +/// `StyleSystemOptions::default().dump_style_statistics`. +/// See [#22854](https://github.com/servo/servo/issues/22854). +#[cfg(feature = "servo")] +pub static DEFAULT_DUMP_STYLE_STATISTICS: std::sync::atomic::AtomicBool = + std::sync::atomic::AtomicBool::new(false); + +impl Default for StyleSystemOptions { + #[cfg(feature = "servo")] + fn default() -> Self { + use std::sync::atomic::Ordering; + + StyleSystemOptions { + disable_style_sharing_cache: DEFAULT_DISABLE_STYLE_SHARING_CACHE + .load(Ordering::Relaxed), + dump_style_statistics: DEFAULT_DUMP_STYLE_STATISTICS.load(Ordering::Relaxed), + style_statistics_threshold: DEFAULT_STATISTICS_THRESHOLD, + } + } + + #[cfg(feature = "gecko")] + fn default() -> Self { + StyleSystemOptions { + disable_style_sharing_cache: get_env_bool("DISABLE_STYLE_SHARING_CACHE"), + dump_style_statistics: get_env_bool("DUMP_STYLE_STATISTICS"), + style_statistics_threshold: get_env_usize("STYLE_STATISTICS_THRESHOLD") + .unwrap_or(DEFAULT_STATISTICS_THRESHOLD), + } + } +} + +/// A shared style context. +/// +/// There's exactly one of these during a given restyle traversal, and it's +/// shared among the worker threads. +pub struct SharedStyleContext<'a> { + /// The CSS selector stylist. + pub stylist: &'a Stylist, + + /// Whether visited styles are enabled. + /// + /// They may be disabled when Gecko's pref layout.css.visited_links_enabled + /// is false, or when in private browsing mode. + pub visited_styles_enabled: bool, + + /// Configuration options. + pub options: StyleSystemOptions, + + /// Guards for pre-acquired locks + pub guards: StylesheetGuards<'a>, + + /// The current time for transitions and animations. This is needed to ensure + /// a consistent sampling time and also to adjust the time for testing. + pub current_time_for_animations: f64, + + /// Flags controlling how we traverse the tree. + pub traversal_flags: TraversalFlags, + + /// A map with our snapshots in order to handle restyle hints. + pub snapshot_map: &'a SnapshotMap, + + /// The state of all animations for our styled elements. + #[cfg(feature = "servo")] + pub animations: DocumentAnimationSet, + + /// Paint worklets + #[cfg(feature = "servo")] + pub registered_speculative_painters: &'a dyn RegisteredSpeculativePainters, +} + +impl<'a> SharedStyleContext<'a> { + /// Return a suitable viewport size in order to be used for viewport units. + pub fn viewport_size(&self) -> Size2D { + self.stylist.device().au_viewport_size() + } + + /// The device pixel ratio + pub fn device_pixel_ratio(&self) -> Scale { + self.stylist.device().device_pixel_ratio() + } + + /// The quirks mode of the document. + pub fn quirks_mode(&self) -> QuirksMode { + self.stylist.quirks_mode() + } +} + +/// The structure holds various intermediate inputs that are eventually used by +/// by the cascade. +/// +/// The matching and cascading process stores them in this format temporarily +/// within the `CurrentElementInfo`. At the end of the cascade, they are folded +/// down into the main `ComputedValues` to reduce memory usage per element while +/// still remaining accessible. +#[derive(Clone, Debug, Default)] +pub struct CascadeInputs { + /// The rule node representing the ordered list of rules matched for this + /// node. + pub rules: Option, + + /// The rule node representing the ordered list of rules matched for this + /// node if visited, only computed if there's a relevant link for this + /// element. A element's "relevant link" is the element being matched if it + /// is a link or the nearest ancestor link. + pub visited_rules: Option, + + /// The set of flags from container queries that we need for invalidation. + pub flags: ComputedValueFlags, +} + +impl CascadeInputs { + /// Construct inputs from previous cascade results, if any. + pub fn new_from_style(style: &ComputedValues) -> Self { + Self { + rules: style.rules.clone(), + visited_rules: style.visited_style().and_then(|v| v.rules.clone()), + flags: style.flags.for_cascade_inputs(), + } + } +} + +/// A list of cascade inputs for eagerly-cascaded pseudo-elements. +/// The list is stored inline. +#[derive(Debug)] +pub struct EagerPseudoCascadeInputs(Option<[Option; EAGER_PSEUDO_COUNT]>); + +// Manually implement `Clone` here because the derived impl of `Clone` for +// array types assumes the value inside is `Copy`. +impl Clone for EagerPseudoCascadeInputs { + fn clone(&self) -> Self { + if self.0.is_none() { + return EagerPseudoCascadeInputs(None); + } + let self_inputs = self.0.as_ref().unwrap(); + let mut inputs: [Option; EAGER_PSEUDO_COUNT] = Default::default(); + for i in 0..EAGER_PSEUDO_COUNT { + inputs[i] = self_inputs[i].clone(); + } + EagerPseudoCascadeInputs(Some(inputs)) + } +} + +impl EagerPseudoCascadeInputs { + /// Construct inputs from previous cascade results, if any. + fn new_from_style(styles: &EagerPseudoStyles) -> Self { + EagerPseudoCascadeInputs(styles.as_optional_array().map(|styles| { + let mut inputs: [Option; EAGER_PSEUDO_COUNT] = Default::default(); + for i in 0..EAGER_PSEUDO_COUNT { + inputs[i] = styles[i].as_ref().map(|s| CascadeInputs::new_from_style(s)); + } + inputs + })) + } + + /// Returns the list of rules, if they exist. + pub fn into_array(self) -> Option<[Option; EAGER_PSEUDO_COUNT]> { + self.0 + } +} + +/// The cascade inputs associated with a node, including those for any +/// pseudo-elements. +/// +/// The matching and cascading process stores them in this format temporarily +/// within the `CurrentElementInfo`. At the end of the cascade, they are folded +/// down into the main `ComputedValues` to reduce memory usage per element while +/// still remaining accessible. +#[derive(Clone, Debug)] +pub struct ElementCascadeInputs { + /// The element's cascade inputs. + pub primary: CascadeInputs, + /// A list of the inputs for the element's eagerly-cascaded pseudo-elements. + pub pseudos: EagerPseudoCascadeInputs, +} + +impl ElementCascadeInputs { + /// Construct inputs from previous cascade results, if any. + #[inline] + pub fn new_from_element_data(data: &ElementData) -> Self { + debug_assert!(data.has_styles()); + ElementCascadeInputs { + primary: CascadeInputs::new_from_style(data.styles.primary()), + pseudos: EagerPseudoCascadeInputs::new_from_style(&data.styles.pseudos), + } + } +} + +/// Statistics gathered during the traversal. We gather statistics on each +/// thread and then combine them after the threads join via the Add +/// implementation below. +#[derive(AddAssign, Clone, Default)] +pub struct PerThreadTraversalStatistics { + /// The total number of elements traversed. + pub elements_traversed: u32, + /// The number of elements where has_styles() went from false to true. + pub elements_styled: u32, + /// The number of elements for which we performed selector matching. + pub elements_matched: u32, + /// The number of cache hits from the StyleSharingCache. + pub styles_shared: u32, + /// The number of styles reused via rule node comparison from the + /// StyleSharingCache. + pub styles_reused: u32, +} + +/// Statistics gathered during the traversal plus some information from +/// other sources including stylist. +#[derive(Default)] +pub struct TraversalStatistics { + /// Aggregated statistics gathered during the traversal. + pub aggregated: PerThreadTraversalStatistics, + /// The number of selectors in the stylist. + pub selectors: u32, + /// The number of revalidation selectors. + pub revalidation_selectors: u32, + /// The number of state/attr dependencies in the dependency set. + pub dependency_selectors: u32, + /// The number of declarations in the stylist. + pub declarations: u32, + /// The number of times the stylist was rebuilt. + pub stylist_rebuilds: u32, + /// Time spent in the traversal, in milliseconds. + pub traversal_time_ms: f64, + /// Whether this was a parallel traversal. + pub is_parallel: bool, + /// Whether this is a "large" traversal. + pub is_large: bool, +} + +/// Format the statistics in a way that the performance test harness understands. +/// See https://bugzilla.mozilla.org/show_bug.cgi?id=1331856#c2 +impl fmt::Display for TraversalStatistics { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + debug_assert!( + self.traversal_time_ms != 0.0, + "should have set traversal time" + ); + writeln!(f, "[PERF] perf block start")?; + writeln!( + f, + "[PERF],traversal,{}", + if self.is_parallel { + "parallel" + } else { + "sequential" + } + )?; + writeln!( + f, + "[PERF],elements_traversed,{}", + self.aggregated.elements_traversed + )?; + writeln!( + f, + "[PERF],elements_styled,{}", + self.aggregated.elements_styled + )?; + writeln!( + f, + "[PERF],elements_matched,{}", + self.aggregated.elements_matched + )?; + writeln!(f, "[PERF],styles_shared,{}", self.aggregated.styles_shared)?; + writeln!(f, "[PERF],styles_reused,{}", self.aggregated.styles_reused)?; + writeln!(f, "[PERF],selectors,{}", self.selectors)?; + writeln!( + f, + "[PERF],revalidation_selectors,{}", + self.revalidation_selectors + )?; + writeln!( + f, + "[PERF],dependency_selectors,{}", + self.dependency_selectors + )?; + writeln!(f, "[PERF],declarations,{}", self.declarations)?; + writeln!(f, "[PERF],stylist_rebuilds,{}", self.stylist_rebuilds)?; + writeln!(f, "[PERF],traversal_time_ms,{}", self.traversal_time_ms)?; + writeln!(f, "[PERF] perf block end") + } +} + +impl TraversalStatistics { + /// Generate complete traversal statistics. + /// + /// The traversal time is computed given the start time in seconds. + pub fn new( + aggregated: PerThreadTraversalStatistics, + traversal: &D, + parallel: bool, + start: f64, + ) -> TraversalStatistics + where + E: TElement, + D: DomTraversal, + { + let threshold = traversal + .shared_context() + .options + .style_statistics_threshold; + let stylist = traversal.shared_context().stylist; + let is_large = aggregated.elements_traversed as usize >= threshold; + TraversalStatistics { + aggregated, + selectors: stylist.num_selectors() as u32, + revalidation_selectors: stylist.num_revalidation_selectors() as u32, + dependency_selectors: stylist.num_invalidations() as u32, + declarations: stylist.num_declarations() as u32, + stylist_rebuilds: stylist.num_rebuilds() as u32, + traversal_time_ms: (time::precise_time_s() - start) * 1000.0, + is_parallel: parallel, + is_large, + } + } +} + +#[cfg(feature = "gecko")] +bitflags! { + /// Represents which tasks are performed in a SequentialTask of + /// UpdateAnimations which is a result of normal restyle. + pub struct UpdateAnimationsTasks: u8 { + /// Update CSS Animations. + const CSS_ANIMATIONS = structs::UpdateAnimationsTasks_CSSAnimations; + /// Update CSS Transitions. + const CSS_TRANSITIONS = structs::UpdateAnimationsTasks_CSSTransitions; + /// Update effect properties. + const EFFECT_PROPERTIES = structs::UpdateAnimationsTasks_EffectProperties; + /// Update animation cacade results for animations running on the compositor. + const CASCADE_RESULTS = structs::UpdateAnimationsTasks_CascadeResults; + /// Display property was changed from none. + /// Script animations keep alive on display:none elements, so we need to trigger + /// the second animation restyles for the script animations in the case where + /// the display property was changed from 'none' to others. + const DISPLAY_CHANGED_FROM_NONE = structs::UpdateAnimationsTasks_DisplayChangedFromNone; + /// Update CSS named scroll progress timelines. + const SCROLL_TIMELINES = structs::UpdateAnimationsTasks_ScrollTimelines; + /// Update CSS named view progress timelines. + const VIEW_TIMELINES = structs::UpdateAnimationsTasks_ViewTimelines; + } +} + +#[cfg(feature = "gecko")] +bitflags! { + /// Represents which tasks are performed in a SequentialTask as a result of + /// animation-only restyle. + pub struct PostAnimationTasks: u8 { + /// Display property was changed from none in animation-only restyle so + /// that we need to resolve styles for descendants in a subsequent + /// normal restyle. + const DISPLAY_CHANGED_FROM_NONE_FOR_SMIL = 0x01; + } +} + +/// A task to be run in sequential mode on the parent (non-worker) thread. This +/// is used by the style system to queue up work which is not safe to do during +/// the parallel traversal. +pub enum SequentialTask { + /// Entry to avoid an unused type parameter error on servo. + Unused(SendElement), + + /// Performs one of a number of possible tasks related to updating + /// animations based on the |tasks| field. These include updating CSS + /// animations/transitions that changed as part of the non-animation style + /// traversal, and updating the computed effect properties. + #[cfg(feature = "gecko")] + UpdateAnimations { + /// The target element or pseudo-element. + el: SendElement, + /// The before-change style for transitions. We use before-change style + /// as the initial value of its Keyframe. Required if |tasks| includes + /// CSSTransitions. + before_change_style: Option>, + /// The tasks which are performed in this SequentialTask. + tasks: UpdateAnimationsTasks, + }, + + /// Performs one of a number of possible tasks as a result of animation-only + /// restyle. + /// + /// Currently we do only process for resolving descendant elements that were + /// display:none subtree for SMIL animation. + #[cfg(feature = "gecko")] + PostAnimation { + /// The target element. + el: SendElement, + /// The tasks which are performed in this SequentialTask. + tasks: PostAnimationTasks, + }, +} + +impl SequentialTask { + /// Executes this task. + pub fn execute(self) { + use self::SequentialTask::*; + debug_assert_eq!(thread_state::get(), ThreadState::LAYOUT); + match self { + Unused(_) => unreachable!(), + #[cfg(feature = "gecko")] + UpdateAnimations { + el, + before_change_style, + tasks, + } => { + el.update_animations(before_change_style, tasks); + }, + #[cfg(feature = "gecko")] + PostAnimation { el, tasks } => { + el.process_post_animation(tasks); + }, + } + } + + /// Creates a task to update various animation-related state on a given + /// (pseudo-)element. + #[cfg(feature = "gecko")] + pub fn update_animations( + el: E, + before_change_style: Option>, + tasks: UpdateAnimationsTasks, + ) -> Self { + use self::SequentialTask::*; + UpdateAnimations { + el: unsafe { SendElement::new(el) }, + before_change_style, + tasks, + } + } + + /// Creates a task to do post-process for a given element as a result of + /// animation-only restyle. + #[cfg(feature = "gecko")] + pub fn process_post_animation(el: E, tasks: PostAnimationTasks) -> Self { + use self::SequentialTask::*; + PostAnimation { + el: unsafe { SendElement::new(el) }, + tasks, + } + } +} + +/// A list of SequentialTasks that get executed on Drop. +pub struct SequentialTaskList(Vec>) +where + E: TElement; + +impl ops::Deref for SequentialTaskList +where + E: TElement, +{ + type Target = Vec>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl ops::DerefMut for SequentialTaskList +where + E: TElement, +{ + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Drop for SequentialTaskList +where + E: TElement, +{ + fn drop(&mut self) { + debug_assert_eq!(thread_state::get(), ThreadState::LAYOUT); + for task in self.0.drain(..) { + task.execute() + } + } +} + +/// A helper type for stack limit checking. This assumes that stacks grow +/// down, which is true for all non-ancient CPU architectures. +pub struct StackLimitChecker { + lower_limit: usize, +} + +impl StackLimitChecker { + /// Create a new limit checker, for this thread, allowing further use + /// of up to |stack_size| bytes beyond (below) the current stack pointer. + #[inline(never)] + pub fn new(stack_size_limit: usize) -> Self { + StackLimitChecker { + lower_limit: StackLimitChecker::get_sp() - stack_size_limit, + } + } + + /// Checks whether the previously stored stack limit has now been exceeded. + #[inline(never)] + pub fn limit_exceeded(&self) -> bool { + let curr_sp = StackLimitChecker::get_sp(); + + // Do some sanity-checking to ensure that our invariants hold, even in + // the case where we've exceeded the soft limit. + // + // The correctness of depends on the assumption that no stack wraps + // around the end of the address space. + if cfg!(debug_assertions) { + // Compute the actual bottom of the stack by subtracting our safety + // margin from our soft limit. Note that this will be slightly below + // the actual bottom of the stack, because there are a few initial + // frames on the stack before we do the measurement that computes + // the limit. + let stack_bottom = self.lower_limit - STACK_SAFETY_MARGIN_KB * 1024; + + // The bottom of the stack should be below the current sp. If it + // isn't, that means we've either waited too long to check the limit + // and burned through our safety margin (in which case we probably + // would have segfaulted by now), or we're using a limit computed for + // a different thread. + debug_assert!(stack_bottom < curr_sp); + + // Compute the distance between the current sp and the bottom of + // the stack, and compare it against the current stack. It should be + // no further from us than the total stack size. We allow some slop + // to handle the fact that stack_bottom is a bit further than the + // bottom of the stack, as discussed above. + let distance_to_stack_bottom = curr_sp - stack_bottom; + let max_allowable_distance = (STYLE_THREAD_STACK_SIZE_KB + 10) * 1024; + debug_assert!(distance_to_stack_bottom <= max_allowable_distance); + } + + // The actual bounds check. + curr_sp <= self.lower_limit + } + + // Technically, rustc can optimize this away, but shouldn't for now. + // We should fix this once black_box is stable. + #[inline(always)] + fn get_sp() -> usize { + let mut foo: usize = 42; + (&mut foo as *mut usize) as usize + } +} + +/// A thread-local style context. +/// +/// This context contains data that needs to be used during restyling, but is +/// not required to be unique among worker threads, so we create one per worker +/// thread in order to be able to mutate it without locking. +pub struct ThreadLocalStyleContext { + /// A cache to share style among siblings. + pub sharing_cache: StyleSharingCache, + /// A cache from matched properties to elements that match those. + pub rule_cache: RuleCache, + /// The bloom filter used to fast-reject selector-matching. + pub bloom_filter: StyleBloom, + /// A set of tasks to be run (on the parent thread) in sequential mode after + /// the rest of the styling is complete. This is useful for + /// infrequently-needed non-threadsafe operations. + /// + /// It's important that goes after the style sharing cache and the bloom + /// filter, to ensure they're dropped before we execute the tasks, which + /// could create another ThreadLocalStyleContext for style computation. + pub tasks: SequentialTaskList, + /// Statistics about the traversal. + pub statistics: PerThreadTraversalStatistics, + /// A checker used to ensure that parallel.rs does not recurse indefinitely + /// even on arbitrarily deep trees. See Gecko bug 1376883. + pub stack_limit_checker: StackLimitChecker, + /// Collection of caches (And cache-likes) for speeding up expensive selector matches. + pub selector_caches: SelectorCaches, +} + +impl ThreadLocalStyleContext { + /// Creates a new `ThreadLocalStyleContext` + pub fn new() -> Self { + ThreadLocalStyleContext { + sharing_cache: StyleSharingCache::new(), + rule_cache: RuleCache::new(), + bloom_filter: StyleBloom::new(), + tasks: SequentialTaskList(Vec::new()), + statistics: PerThreadTraversalStatistics::default(), + stack_limit_checker: StackLimitChecker::new( + (STYLE_THREAD_STACK_SIZE_KB - STACK_SAFETY_MARGIN_KB) * 1024, + ), + selector_caches: SelectorCaches::default(), + } + } +} + +/// A `StyleContext` is just a simple container for a immutable reference to a +/// shared style context, and a mutable reference to a local one. +pub struct StyleContext<'a, E: TElement + 'a> { + /// The shared style context reference. + pub shared: &'a SharedStyleContext<'a>, + /// The thread-local style context (mutable) reference. + pub thread_local: &'a mut ThreadLocalStyleContext, +} + +/// A registered painter +#[cfg(feature = "servo")] +pub trait RegisteredSpeculativePainter: SpeculativePainter { + /// The name it was registered with + fn name(&self) -> Atom; + /// The properties it was registered with + fn properties(&self) -> &FxHashMap; +} + +/// A set of registered painters +#[cfg(feature = "servo")] +pub trait RegisteredSpeculativePainters: Sync { + /// Look up a speculative painter + fn get(&self, name: &Atom) -> Option<&dyn RegisteredSpeculativePainter>; +} diff --git a/servo/components/style/counter_style/mod.rs b/servo/components/style/counter_style/mod.rs new file mode 100644 index 0000000000..fc7a0fb447 --- /dev/null +++ b/servo/components/style/counter_style/mod.rs @@ -0,0 +1,695 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! The [`@counter-style`][counter-style] at-rule. +//! +//! [counter-style]: https://drafts.csswg.org/css-counter-styles/ + +use crate::error_reporting::ContextualParseError; +use crate::parser::{Parse, ParserContext}; +use crate::shared_lock::{SharedRwLockReadGuard, ToCssWithGuard}; +use crate::str::CssStringWriter; +use crate::values::specified::Integer; +use crate::values::CustomIdent; +use crate::Atom; +use cssparser::{ + AtRuleParser, DeclarationParser, QualifiedRuleParser, RuleBodyItemParser, RuleBodyParser, +}; +use cssparser::{CowRcStr, Parser, SourceLocation, Token}; +use selectors::parser::SelectorParseErrorKind; +use std::fmt::{self, Write}; +use std::mem; +use std::num::Wrapping; +use style_traits::{Comma, CssWriter, OneOrMoreSeparated, ParseError}; +use style_traits::{StyleParseErrorKind, ToCss}; + +/// Parse a counter style name reference. +/// +/// This allows the reserved counter style names "decimal" and "disc". +pub fn parse_counter_style_name<'i, 't>( + input: &mut Parser<'i, 't>, +) -> Result> { + macro_rules! predefined { + ($($name: tt,)+) => {{ + ascii_case_insensitive_phf_map! { + predefined -> Atom = { + $( + $name => atom!($name), + )+ + } + } + + let location = input.current_source_location(); + let ident = input.expect_ident()?; + // This effectively performs case normalization only on predefined names. + if let Some(lower_case) = predefined::get(&ident) { + Ok(CustomIdent(lower_case.clone())) + } else { + // none is always an invalid value. + CustomIdent::from_ident(location, ident, &["none"]) + } + }} + } + include!("predefined.rs") +} + +fn is_valid_name_definition(ident: &CustomIdent) -> bool { + ident.0 != atom!("decimal") && + ident.0 != atom!("disc") && + ident.0 != atom!("circle") && + ident.0 != atom!("square") && + ident.0 != atom!("disclosure-closed") && + ident.0 != atom!("disclosure-open") +} + +/// Parse the prelude of an @counter-style rule +pub fn parse_counter_style_name_definition<'i, 't>( + input: &mut Parser<'i, 't>, +) -> Result> { + parse_counter_style_name(input).and_then(|ident| { + if !is_valid_name_definition(&ident) { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } else { + Ok(ident) + } + }) +} + +/// Parse the body (inside `{}`) of an @counter-style rule +pub fn parse_counter_style_body<'i, 't>( + name: CustomIdent, + context: &ParserContext, + input: &mut Parser<'i, 't>, + location: SourceLocation, +) -> Result> { + let start = input.current_source_location(); + let mut rule = CounterStyleRuleData::empty(name, location); + { + let mut parser = CounterStyleRuleParser { + context, + rule: &mut rule, + }; + let mut iter = RuleBodyParser::new(input, &mut parser); + while let Some(declaration) = iter.next() { + if let Err((error, slice)) = declaration { + let location = error.location; + let error = ContextualParseError::UnsupportedCounterStyleDescriptorDeclaration( + slice, error, + ); + context.log_css_error(location, error) + } + } + } + let error = match *rule.resolved_system() { + ref system @ System::Cyclic | + ref system @ System::Fixed { .. } | + ref system @ System::Symbolic | + ref system @ System::Alphabetic | + ref system @ System::Numeric + if rule.symbols.is_none() => + { + let system = system.to_css_string(); + Some(ContextualParseError::InvalidCounterStyleWithoutSymbols( + system, + )) + }, + ref system @ System::Alphabetic | ref system @ System::Numeric + if rule.symbols().unwrap().0.len() < 2 => + { + let system = system.to_css_string(); + Some(ContextualParseError::InvalidCounterStyleNotEnoughSymbols( + system, + )) + }, + System::Additive if rule.additive_symbols.is_none() => { + Some(ContextualParseError::InvalidCounterStyleWithoutAdditiveSymbols) + }, + System::Extends(_) if rule.symbols.is_some() => { + Some(ContextualParseError::InvalidCounterStyleExtendsWithSymbols) + }, + System::Extends(_) if rule.additive_symbols.is_some() => { + Some(ContextualParseError::InvalidCounterStyleExtendsWithAdditiveSymbols) + }, + _ => None, + }; + if let Some(error) = error { + context.log_css_error(start, error); + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } else { + Ok(rule) + } +} + +struct CounterStyleRuleParser<'a, 'b: 'a> { + context: &'a ParserContext<'b>, + rule: &'a mut CounterStyleRuleData, +} + +/// Default methods reject all at rules. +impl<'a, 'b, 'i> AtRuleParser<'i> for CounterStyleRuleParser<'a, 'b> { + type Prelude = (); + type AtRule = (); + type Error = StyleParseErrorKind<'i>; +} + +impl<'a, 'b, 'i> QualifiedRuleParser<'i> for CounterStyleRuleParser<'a, 'b> { + type Prelude = (); + type QualifiedRule = (); + type Error = StyleParseErrorKind<'i>; +} + +impl<'a, 'b, 'i> RuleBodyItemParser<'i, (), StyleParseErrorKind<'i>> + for CounterStyleRuleParser<'a, 'b> +{ + fn parse_qualified(&self) -> bool { + false + } + fn parse_declarations(&self) -> bool { + true + } +} + +macro_rules! checker { + ($self:ident._($value:ident)) => {}; + ($self:ident. $checker:ident($value:ident)) => { + if !$self.$checker(&$value) { + return false; + } + }; +} + +macro_rules! counter_style_descriptors { + ( + $( #[$doc: meta] $name: tt $ident: ident / $setter: ident [$checker: tt]: $ty: ty, )+ + ) => { + /// An @counter-style rule + #[derive(Clone, Debug, ToShmem)] + pub struct CounterStyleRuleData { + name: CustomIdent, + generation: Wrapping, + $( + #[$doc] + $ident: Option<$ty>, + )+ + /// Line and column of the @counter-style rule source code. + pub source_location: SourceLocation, + } + + impl CounterStyleRuleData { + fn empty(name: CustomIdent, source_location: SourceLocation) -> Self { + CounterStyleRuleData { + name: name, + generation: Wrapping(0), + $( + $ident: None, + )+ + source_location, + } + } + + $( + #[$doc] + pub fn $ident(&self) -> Option<&$ty> { + self.$ident.as_ref() + } + )+ + + $( + #[$doc] + pub fn $setter(&mut self, value: $ty) -> bool { + checker!(self.$checker(value)); + self.$ident = Some(value); + self.generation += Wrapping(1); + true + } + )+ + } + + impl<'a, 'b, 'i> DeclarationParser<'i> for CounterStyleRuleParser<'a, 'b> { + type Declaration = (); + type Error = StyleParseErrorKind<'i>; + + fn parse_value<'t>( + &mut self, + name: CowRcStr<'i>, + input: &mut Parser<'i, 't>, + ) -> Result<(), ParseError<'i>> { + match_ignore_ascii_case! { &*name, + $( + $name => { + // DeclarationParser also calls parse_entirely so we’d normally not + // need to, but in this case we do because we set the value as a side + // effect rather than returning it. + let value = input.parse_entirely(|i| Parse::parse(self.context, i))?; + self.rule.$ident = Some(value) + }, + )* + _ => return Err(input.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))), + } + Ok(()) + } + } + + impl ToCssWithGuard for CounterStyleRuleData { + fn to_css(&self, _guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result { + dest.write_str("@counter-style ")?; + self.name.to_css(&mut CssWriter::new(dest))?; + dest.write_str(" { ")?; + $( + if let Some(ref value) = self.$ident { + dest.write_str(concat!($name, ": "))?; + ToCss::to_css(value, &mut CssWriter::new(dest))?; + dest.write_str("; ")?; + } + )+ + dest.write_char('}') + } + } + } +} + +counter_style_descriptors! { + /// + "system" system / set_system [check_system]: System, + + /// + "negative" negative / set_negative [_]: Negative, + + /// + "prefix" prefix / set_prefix [_]: Symbol, + + /// + "suffix" suffix / set_suffix [_]: Symbol, + + /// + "range" range / set_range [_]: CounterRanges, + + /// + "pad" pad / set_pad [_]: Pad, + + /// + "fallback" fallback / set_fallback [_]: Fallback, + + /// + "symbols" symbols / set_symbols [check_symbols]: Symbols, + + /// + "additive-symbols" additive_symbols / + set_additive_symbols [check_additive_symbols]: AdditiveSymbols, + + /// + "speak-as" speak_as / set_speak_as [_]: SpeakAs, +} + +// Implements the special checkers for some setters. +// See +impl CounterStyleRuleData { + /// Check that the system is effectively not changed. Only params + /// of system descriptor is changeable. + fn check_system(&self, value: &System) -> bool { + mem::discriminant(self.resolved_system()) == mem::discriminant(value) + } + + fn check_symbols(&self, value: &Symbols) -> bool { + match *self.resolved_system() { + // These two systems require at least two symbols. + System::Numeric | System::Alphabetic => value.0.len() >= 2, + // No symbols should be set for extends system. + System::Extends(_) => false, + _ => true, + } + } + + fn check_additive_symbols(&self, _value: &AdditiveSymbols) -> bool { + match *self.resolved_system() { + // No additive symbols should be set for extends system. + System::Extends(_) => false, + _ => true, + } + } +} + +impl CounterStyleRuleData { + /// Get the name of the counter style rule. + pub fn name(&self) -> &CustomIdent { + &self.name + } + + /// Set the name of the counter style rule. Caller must ensure that + /// the name is valid. + pub fn set_name(&mut self, name: CustomIdent) { + debug_assert!(is_valid_name_definition(&name)); + self.name = name; + } + + /// Get the current generation of the counter style rule. + pub fn generation(&self) -> u32 { + self.generation.0 + } + + /// Get the system of this counter style rule, default to + /// `symbolic` if not specified. + pub fn resolved_system(&self) -> &System { + match self.system { + Some(ref system) => system, + None => &System::Symbolic, + } + } +} + +/// +#[derive(Clone, Debug, ToShmem)] +pub enum System { + /// 'cyclic' + Cyclic, + /// 'numeric' + Numeric, + /// 'alphabetic' + Alphabetic, + /// 'symbolic' + Symbolic, + /// 'additive' + Additive, + /// 'fixed ?' + Fixed { + /// '?' + first_symbol_value: Option, + }, + /// 'extends ' + Extends(CustomIdent), +} + +impl Parse for System { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + try_match_ident_ignore_ascii_case! { input, + "cyclic" => Ok(System::Cyclic), + "numeric" => Ok(System::Numeric), + "alphabetic" => Ok(System::Alphabetic), + "symbolic" => Ok(System::Symbolic), + "additive" => Ok(System::Additive), + "fixed" => { + let first_symbol_value = input.try_parse(|i| Integer::parse(context, i)).ok(); + Ok(System::Fixed { first_symbol_value }) + }, + "extends" => { + let other = parse_counter_style_name(input)?; + Ok(System::Extends(other)) + }, + } + } +} + +impl ToCss for System { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: Write, + { + match *self { + System::Cyclic => dest.write_str("cyclic"), + System::Numeric => dest.write_str("numeric"), + System::Alphabetic => dest.write_str("alphabetic"), + System::Symbolic => dest.write_str("symbolic"), + System::Additive => dest.write_str("additive"), + System::Fixed { first_symbol_value } => { + if let Some(value) = first_symbol_value { + dest.write_str("fixed ")?; + value.to_css(dest) + } else { + dest.write_str("fixed") + } + }, + System::Extends(ref other) => { + dest.write_str("extends ")?; + other.to_css(dest) + }, + } + } +} + +/// +#[derive( + Clone, Debug, Eq, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToCss, ToShmem, +)] +#[repr(u8)] +pub enum Symbol { + /// + String(crate::OwnedStr), + /// + Ident(CustomIdent), + // Not implemented: + // /// + // Image(Image), +} + +impl Parse for Symbol { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + let location = input.current_source_location(); + match *input.next()? { + Token::QuotedString(ref s) => Ok(Symbol::String(s.as_ref().to_owned().into())), + Token::Ident(ref s) => Ok(Symbol::Ident(CustomIdent::from_ident(location, s, &[])?)), + ref t => Err(location.new_unexpected_token_error(t.clone())), + } + } +} + +impl Symbol { + /// Returns whether this symbol is allowed in symbols() function. + pub fn is_allowed_in_symbols(&self) -> bool { + match self { + // Identifier is not allowed. + &Symbol::Ident(_) => false, + _ => true, + } + } +} + +/// +#[derive(Clone, Debug, ToCss, ToShmem)] +pub struct Negative(pub Symbol, pub Option); + +impl Parse for Negative { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + Ok(Negative( + Symbol::parse(context, input)?, + input.try_parse(|input| Symbol::parse(context, input)).ok(), + )) + } +} + +/// +#[derive(Clone, Debug, ToCss, ToShmem)] +pub struct CounterRange { + /// The start of the range. + pub start: CounterBound, + /// The end of the range. + pub end: CounterBound, +} + +/// +/// +/// Empty represents 'auto' +#[derive(Clone, Debug, ToCss, ToShmem)] +#[css(comma)] +pub struct CounterRanges(#[css(iterable, if_empty = "auto")] pub crate::OwnedSlice); + +/// A bound found in `CounterRanges`. +#[derive(Clone, Copy, Debug, ToCss, ToShmem)] +pub enum CounterBound { + /// An integer bound. + Integer(Integer), + /// The infinite bound. + Infinite, +} + +impl Parse for CounterRanges { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + if input + .try_parse(|input| input.expect_ident_matching("auto")) + .is_ok() + { + return Ok(CounterRanges(Default::default())); + } + + let ranges = input.parse_comma_separated(|input| { + let start = parse_bound(context, input)?; + let end = parse_bound(context, input)?; + if let (CounterBound::Integer(start), CounterBound::Integer(end)) = (start, end) { + if start > end { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + } + Ok(CounterRange { start, end }) + })?; + + Ok(CounterRanges(ranges.into())) + } +} + +fn parse_bound<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, +) -> Result> { + if let Ok(integer) = input.try_parse(|input| Integer::parse(context, input)) { + return Ok(CounterBound::Integer(integer)); + } + input.expect_ident_matching("infinite")?; + Ok(CounterBound::Infinite) +} + +/// +#[derive(Clone, Debug, ToCss, ToShmem)] +pub struct Pad(pub Integer, pub Symbol); + +impl Parse for Pad { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + let pad_with = input.try_parse(|input| Symbol::parse(context, input)); + let min_length = Integer::parse_non_negative(context, input)?; + let pad_with = pad_with.or_else(|_| Symbol::parse(context, input))?; + Ok(Pad(min_length, pad_with)) + } +} + +/// +#[derive(Clone, Debug, ToCss, ToShmem)] +pub struct Fallback(pub CustomIdent); + +impl Parse for Fallback { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + Ok(Fallback(parse_counter_style_name(input)?)) + } +} + +/// +#[derive( + Clone, Debug, Eq, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToCss, ToShmem, +)] +#[repr(C)] +pub struct Symbols(#[css(iterable)] pub crate::OwnedSlice); + +impl Parse for Symbols { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + let mut symbols = Vec::new(); + while let Ok(s) = input.try_parse(|input| Symbol::parse(context, input)) { + symbols.push(s); + } + if symbols.is_empty() { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + Ok(Symbols(symbols.into())) + } +} + +/// +#[derive(Clone, Debug, ToCss, ToShmem)] +#[css(comma)] +pub struct AdditiveSymbols(#[css(iterable)] pub crate::OwnedSlice); + +impl Parse for AdditiveSymbols { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + let tuples = Vec::::parse(context, input)?; + // FIXME maybe? https://github.com/w3c/csswg-drafts/issues/1220 + if tuples + .windows(2) + .any(|window| window[0].weight <= window[1].weight) + { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + Ok(AdditiveSymbols(tuples.into())) + } +} + +/// && +#[derive(Clone, Debug, ToCss, ToShmem)] +pub struct AdditiveTuple { + /// + pub weight: Integer, + /// + pub symbol: Symbol, +} + +impl OneOrMoreSeparated for AdditiveTuple { + type S = Comma; +} + +impl Parse for AdditiveTuple { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + let symbol = input.try_parse(|input| Symbol::parse(context, input)); + let weight = Integer::parse_non_negative(context, input)?; + let symbol = symbol.or_else(|_| Symbol::parse(context, input))?; + Ok(Self { weight, symbol }) + } +} + +/// +#[derive(Clone, Debug, ToCss, ToShmem)] +pub enum SpeakAs { + /// auto + Auto, + /// bullets + Bullets, + /// numbers + Numbers, + /// words + Words, + // /// spell-out, not supported, see bug 1024178 + // SpellOut, + /// + Other(CustomIdent), +} + +impl Parse for SpeakAs { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + let mut is_spell_out = false; + let result = input.try_parse(|input| { + let ident = input.expect_ident().map_err(|_| ())?; + match_ignore_ascii_case! { &*ident, + "auto" => Ok(SpeakAs::Auto), + "bullets" => Ok(SpeakAs::Bullets), + "numbers" => Ok(SpeakAs::Numbers), + "words" => Ok(SpeakAs::Words), + "spell-out" => { + is_spell_out = true; + Err(()) + }, + _ => Err(()), + } + }); + if is_spell_out { + // spell-out is not supported, but don’t parse it as a . + // See bug 1024178. + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + result.or_else(|_| Ok(SpeakAs::Other(parse_counter_style_name(input)?))) + } +} diff --git a/servo/components/style/counter_style/predefined.rs b/servo/components/style/counter_style/predefined.rs new file mode 100644 index 0000000000..7243e3b3f3 --- /dev/null +++ b/servo/components/style/counter_style/predefined.rs @@ -0,0 +1,61 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +predefined! { + "decimal", + "decimal-leading-zero", + "arabic-indic", + "armenian", + "upper-armenian", + "lower-armenian", + "bengali", + "cambodian", + "khmer", + "cjk-decimal", + "devanagari", + "georgian", + "gujarati", + "gurmukhi", + "hebrew", + "kannada", + "lao", + "malayalam", + "mongolian", + "myanmar", + "oriya", + "persian", + "lower-roman", + "upper-roman", + "tamil", + "telugu", + "thai", + "tibetan", + "lower-alpha", + "lower-latin", + "upper-alpha", + "upper-latin", + "cjk-earthly-branch", + "cjk-heavenly-stem", + "lower-greek", + "hiragana", + "hiragana-iroha", + "katakana", + "katakana-iroha", + "disc", + "circle", + "square", + "disclosure-open", + "disclosure-closed", + "japanese-informal", + "japanese-formal", + "korean-hangul-formal", + "korean-hanja-informal", + "korean-hanja-formal", + "simp-chinese-informal", + "simp-chinese-formal", + "trad-chinese-informal", + "trad-chinese-formal", + "cjk-ideographic", + "ethiopic-numeric", +} diff --git a/servo/components/style/counter_style/update_predefined.py b/servo/components/style/counter_style/update_predefined.py new file mode 100755 index 0000000000..1523958ff3 --- /dev/null +++ b/servo/components/style/counter_style/update_predefined.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python + +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import os.path +import re +import urllib + + +def main(filename): + names = [ + re.search('>([^>]+)(| VariableValue; + +struct EnvironmentVariable { + name: Atom, + evaluator: EnvironmentEvaluator, +} + +macro_rules! make_variable { + ($name:expr, $evaluator:expr) => {{ + EnvironmentVariable { + name: $name, + evaluator: $evaluator, + } + }}; +} + +fn get_safearea_inset_top(device: &Device, url_data: &UrlExtraData) -> VariableValue { + VariableValue::pixels(device.safe_area_insets().top, url_data) +} + +fn get_safearea_inset_bottom(device: &Device, url_data: &UrlExtraData) -> VariableValue { + VariableValue::pixels(device.safe_area_insets().bottom, url_data) +} + +fn get_safearea_inset_left(device: &Device, url_data: &UrlExtraData) -> VariableValue { + VariableValue::pixels(device.safe_area_insets().left, url_data) +} + +fn get_safearea_inset_right(device: &Device, url_data: &UrlExtraData) -> VariableValue { + VariableValue::pixels(device.safe_area_insets().right, url_data) +} + +fn get_content_preferred_color_scheme(device: &Device, url_data: &UrlExtraData) -> VariableValue { + use crate::gecko::media_features::PrefersColorScheme; + let prefers_color_scheme = unsafe { + crate::gecko_bindings::bindings::Gecko_MediaFeatures_PrefersColorScheme( + device.document(), + /* use_content = */ true, + ) + }; + VariableValue::ident( + match prefers_color_scheme { + PrefersColorScheme::Light => "light", + PrefersColorScheme::Dark => "dark", + }, + url_data, + ) +} + +fn get_scrollbar_inline_size(device: &Device, url_data: &UrlExtraData) -> VariableValue { + VariableValue::pixels(device.scrollbar_inline_size().px(), url_data) +} + +static ENVIRONMENT_VARIABLES: [EnvironmentVariable; 4] = [ + make_variable!(atom!("safe-area-inset-top"), get_safearea_inset_top), + make_variable!(atom!("safe-area-inset-bottom"), get_safearea_inset_bottom), + make_variable!(atom!("safe-area-inset-left"), get_safearea_inset_left), + make_variable!(atom!("safe-area-inset-right"), get_safearea_inset_right), +]; + +macro_rules! lnf_int { + ($id:ident) => { + unsafe { + crate::gecko_bindings::bindings::Gecko_GetLookAndFeelInt( + crate::gecko_bindings::bindings::LookAndFeel_IntID::$id as i32, + ) + } + }; +} + +macro_rules! lnf_int_variable { + ($atom:expr, $id:ident, $ctor:ident) => {{ + fn __eval(_: &Device, url_data: &UrlExtraData) -> VariableValue { + VariableValue::$ctor(lnf_int!($id), url_data) + } + make_variable!($atom, __eval) + }}; +} + +static CHROME_ENVIRONMENT_VARIABLES: [EnvironmentVariable; 7] = [ + lnf_int_variable!( + atom!("-moz-gtk-csd-titlebar-radius"), + TitlebarRadius, + int_pixels + ), + lnf_int_variable!( + atom!("-moz-gtk-csd-close-button-position"), + GTKCSDCloseButtonPosition, + integer + ), + lnf_int_variable!( + atom!("-moz-gtk-csd-minimize-button-position"), + GTKCSDMinimizeButtonPosition, + integer + ), + lnf_int_variable!( + atom!("-moz-gtk-csd-maximize-button-position"), + GTKCSDMaximizeButtonPosition, + integer + ), + lnf_int_variable!( + atom!("-moz-overlay-scrollbar-fade-duration"), + ScrollbarFadeDuration, + int_ms + ), + make_variable!( + atom!("-moz-content-preferred-color-scheme"), + get_content_preferred_color_scheme + ), + make_variable!(atom!("scrollbar-inline-size"), get_scrollbar_inline_size), +]; + +impl CssEnvironment { + #[inline] + fn get(&self, name: &Atom, device: &Device, url_data: &UrlExtraData) -> Option { + if let Some(var) = ENVIRONMENT_VARIABLES.iter().find(|var| var.name == *name) { + return Some((var.evaluator)(device, url_data)); + } + if !url_data.chrome_rules_enabled() { + return None; + } + let var = CHROME_ENVIRONMENT_VARIABLES + .iter() + .find(|var| var.name == *name)?; + Some((var.evaluator)(device, url_data)) + } +} + +/// A custom property name is just an `Atom`. +/// +/// Note that this does not include the `--` prefix +pub type Name = Atom; + +/// Parse a custom property name. +/// +/// +pub fn parse_name(s: &str) -> Result<&str, ()> { + if s.starts_with("--") && s.len() > 2 { + Ok(&s[2..]) + } else { + Err(()) + } +} + +/// A value for a custom property is just a set of tokens. +/// +/// We preserve the original CSS for serialization, and also the variable +/// references to other custom property names. +#[derive(Clone, Debug, MallocSizeOf, ToShmem)] +pub struct VariableValue { + /// The raw CSS string. + pub css: String, + + /// The url data of the stylesheet where this value came from. + pub url_data: UrlExtraData, + + first_token_type: TokenSerializationType, + last_token_type: TokenSerializationType, + + /// var(), env(), or non-custom property (e.g. through `em`) references. + references: References, +} + +trivial_to_computed_value!(VariableValue); + +// For all purposes, we want values to be considered equal if their css text is equal. +impl PartialEq for VariableValue { + fn eq(&self, other: &Self) -> bool { + self.css == other.css + } +} + +impl Eq for VariableValue {} + +impl ToCss for SpecifiedValue { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: Write, + { + dest.write_str(&self.css) + } +} + +/// A pair of separate CustomPropertiesMaps, split between custom properties +/// that have the inherit flag set and those with the flag unset. +#[repr(C)] +#[derive(Clone, Debug, Default, PartialEq)] +pub struct ComputedCustomProperties { + /// Map for custom properties with inherit flag set, including non-registered + /// ones. + pub inherited: CustomPropertiesMap, + /// Map for custom properties with inherit flag unset. + pub non_inherited: CustomPropertiesMap, +} + +impl ComputedCustomProperties { + /// Return whether the inherited and non_inherited maps are none. + pub fn is_empty(&self) -> bool { + self.inherited.is_empty() && self.non_inherited.is_empty() + } + + /// Return the name and value of the property at specified index, if any. + pub fn property_at(&self, index: usize) -> Option<(&Name, &Option>)> { + // Just expose the custom property items from custom_properties.inherited, followed + // by custom property items from custom_properties.non_inherited. + self.inherited + .get_index(index) + .or_else(|| self.non_inherited.get_index(index - self.inherited.len())) + } + + /// Insert a custom property in the corresponding inherited/non_inherited + /// map, depending on whether the inherit flag is set or unset. + fn insert( + &mut self, + registration: &PropertyRegistrationData, + name: &Name, + value: Arc, + ) { + self.map_mut(registration).insert(name, value) + } + + /// Remove a custom property from the corresponding inherited/non_inherited + /// map, depending on whether the inherit flag is set or unset. + fn remove(&mut self, registration: &PropertyRegistrationData, name: &Name) { + self.map_mut(registration).remove(name); + } + + /// Shrink the capacity of the inherited maps as much as possible. + fn shrink_to_fit(&mut self) { + self.inherited.shrink_to_fit(); + self.non_inherited.shrink_to_fit(); + } + + fn map_mut(&mut self, registration: &PropertyRegistrationData) -> &mut CustomPropertiesMap { + if registration.inherits() { + &mut self.inherited + } else { + &mut self.non_inherited + } + } + + fn get( + &self, + registration: &PropertyRegistrationData, + name: &Name, + ) -> Option<&Arc> { + if registration.inherits() { + self.inherited.get(name) + } else { + self.non_inherited.get(name) + } + } +} + +/// 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; + +/// Set of flags to non-custom references this custom property makes. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, MallocSizeOf, ToShmem)] +struct NonCustomReferences(u8); + +bitflags! { + impl NonCustomReferences: u8 { + /// At least one custom property depends on font-relative units. + const FONT_UNITS = 1 << 0; + /// At least one custom property depends on root element's font-relative units. + const ROOT_FONT_UNITS = 1 << 1; + /// At least one custom property depends on line height units. + const LH_UNITS = 1 << 2; + /// At least one custom property depends on root element's line height units. + const ROOT_LH_UNITS = 1 << 3; + /// All dependencies not depending on the root element. + const NON_ROOT_DEPENDENCIES = Self::FONT_UNITS.bits() | Self::LH_UNITS.bits(); + /// All dependencies depending on the root element. + const ROOT_DEPENDENCIES = Self::ROOT_FONT_UNITS.bits() | Self::ROOT_LH_UNITS.bits(); + } +} + +impl NonCustomReferences { + fn for_each(&self, mut f: F) + where + F: FnMut(SingleNonCustomReference), + { + for (_, r) in self.iter_names() { + let single = match r { + Self::FONT_UNITS => SingleNonCustomReference::FontUnits, + Self::ROOT_FONT_UNITS => SingleNonCustomReference::RootFontUnits, + Self::LH_UNITS => SingleNonCustomReference::LhUnits, + Self::ROOT_LH_UNITS => SingleNonCustomReference::RootLhUnits, + _ => unreachable!("Unexpected single bit value"), + }; + f(single); + } + } + + fn from_unit(value: &CowRcStr) -> Self { + // For registered properties, any reference to font-relative dimensions + // make it dependent on font-related properties. + // TODO(dshin): When we unit algebra gets implemented and handled - + // Is it valid to say that `calc(1em / 2em * 3px)` triggers this? + if value.eq_ignore_ascii_case(FontRelativeLength::LH) { + return Self::FONT_UNITS | Self::LH_UNITS; + } + if value.eq_ignore_ascii_case(FontRelativeLength::EM) || + value.eq_ignore_ascii_case(FontRelativeLength::EX) || + value.eq_ignore_ascii_case(FontRelativeLength::CAP) || + value.eq_ignore_ascii_case(FontRelativeLength::CH) || + value.eq_ignore_ascii_case(FontRelativeLength::IC) + { + return Self::FONT_UNITS; + } + if value.eq_ignore_ascii_case(FontRelativeLength::RLH) { + return Self::ROOT_FONT_UNITS | Self::ROOT_LH_UNITS; + } + if value.eq_ignore_ascii_case(FontRelativeLength::REM) { + return Self::ROOT_FONT_UNITS; + } + Self::empty() + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum SingleNonCustomReference { + FontUnits = 0, + RootFontUnits, + LhUnits, + RootLhUnits, +} + +struct NonCustomReferenceMap([Option; 4]); + +impl Default for NonCustomReferenceMap { + fn default() -> Self { + NonCustomReferenceMap(Default::default()) + } +} + +impl Index for NonCustomReferenceMap { + type Output = Option; + + fn index(&self, reference: SingleNonCustomReference) -> &Self::Output { + &self.0[reference as usize] + } +} + +impl IndexMut for NonCustomReferenceMap { + fn index_mut(&mut self, reference: SingleNonCustomReference) -> &mut Self::Output { + &mut self.0[reference as usize] + } +} + +/// Whether to defer resolving custom properties referencing font relative units. +#[derive(Clone, Copy, PartialEq, Eq)] +#[allow(missing_docs)] +pub enum DeferFontRelativeCustomPropertyResolution { + Yes, + No, +} + +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] +struct VariableFallback { + start: num::NonZeroUsize, + first_token_type: TokenSerializationType, + last_token_type: TokenSerializationType, +} + +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] +struct VarOrEnvReference { + name: Name, + start: usize, + end: usize, + fallback: Option, + prev_token_type: TokenSerializationType, + next_token_type: TokenSerializationType, + is_var: bool, +} + +/// A struct holding information about the external references to that a custom +/// property value may have. +#[derive(Clone, Debug, Default, MallocSizeOf, PartialEq, ToShmem)] +struct References { + refs: Vec, + non_custom_references: NonCustomReferences, + any_env: bool, + any_var: bool, +} + +impl References { + fn has_references(&self) -> bool { + !self.refs.is_empty() + } + + fn get_non_custom_dependencies(&self, is_root_element: bool) -> NonCustomReferences { + let mask = NonCustomReferences::NON_ROOT_DEPENDENCIES; + let mask = if is_root_element { + mask | NonCustomReferences::ROOT_DEPENDENCIES + } else { + mask + }; + + self.non_custom_references & mask + } +} + +impl VariableValue { + fn empty(url_data: &UrlExtraData) -> Self { + Self { + css: String::new(), + last_token_type: Default::default(), + first_token_type: Default::default(), + url_data: url_data.clone(), + references: Default::default(), + } + } + + /// Create a new custom property without parsing if the CSS is known to be valid and contain no + /// references. + pub fn new( + css: String, + url_data: &UrlExtraData, + first_token_type: TokenSerializationType, + last_token_type: TokenSerializationType, + ) -> Self { + Self { + css, + url_data: url_data.clone(), + first_token_type, + last_token_type, + references: Default::default(), + } + } + + fn push<'i>( + &mut self, + css: &str, + css_first_token_type: TokenSerializationType, + css_last_token_type: TokenSerializationType, + ) -> Result<(), ()> { + /// Prevent values from getting terribly big since you can use custom + /// properties exponentially. + /// + /// This number (2MB) is somewhat arbitrary, but silly enough that no + /// reasonable page should hit it. We could limit by number of total + /// substitutions, but that was very easy to work around in practice + /// (just choose a larger initial value and boom). + const MAX_VALUE_LENGTH_IN_BYTES: usize = 2 * 1024 * 1024; + + if self.css.len() + css.len() > MAX_VALUE_LENGTH_IN_BYTES { + return Err(()); + } + + // 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(()) + } + + /// Parse a custom property value. + pub fn parse<'i, 't>( + input: &mut Parser<'i, 't>, + url_data: &UrlExtraData, + ) -> Result> { + input.skip_whitespace(); + + let mut references = References::default(); + let mut missing_closing_characters = String::new(); + let start_position = input.position(); + let (first_token_type, last_token_type) = parse_declaration_value( + input, + start_position, + &mut references, + &mut missing_closing_characters, + )?; + let mut css = input.slice_from(start_position).to_owned(); + 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.pop(); + } + css.push_str(&missing_closing_characters); + } + + css.shrink_to_fit(); + references.refs.shrink_to_fit(); + + Ok(Self { + css, + url_data: url_data.clone(), + first_token_type, + last_token_type, + references, + }) + } + + /// Create VariableValue from an int. + fn integer(number: i32, url_data: &UrlExtraData) -> Self { + Self::from_token( + Token::Number { + has_sign: false, + value: number as f32, + int_value: Some(number), + }, + url_data, + ) + } + + /// Create VariableValue from an int. + fn ident(ident: &'static str, url_data: &UrlExtraData) -> Self { + Self::from_token(Token::Ident(ident.into()), url_data) + } + + /// Create VariableValue from a float amount of CSS pixels. + fn pixels(number: f32, url_data: &UrlExtraData) -> Self { + // FIXME (https://github.com/servo/rust-cssparser/issues/266): + // No way to get TokenSerializationType::Dimension without creating + // Token object. + Self::from_token( + Token::Dimension { + has_sign: false, + value: number, + int_value: None, + unit: CowRcStr::from("px"), + }, + url_data, + ) + } + + /// Create VariableValue from an integer amount of milliseconds. + fn int_ms(number: i32, url_data: &UrlExtraData) -> Self { + Self::from_token( + Token::Dimension { + has_sign: false, + value: number as f32, + int_value: Some(number), + unit: CowRcStr::from("ms"), + }, + url_data, + ) + } + + /// Create VariableValue from an integer amount of CSS pixels. + fn int_pixels(number: i32, url_data: &UrlExtraData) -> Self { + Self::from_token( + Token::Dimension { + has_sign: false, + value: number as f32, + int_value: Some(number), + unit: CowRcStr::from("px"), + }, + url_data, + ) + } + + fn from_token(token: Token, url_data: &UrlExtraData) -> Self { + let token_type = token.serialization_type(); + let mut css = token.to_css_string(); + css.shrink_to_fit(); + + VariableValue { + css, + url_data: url_data.clone(), + first_token_type: token_type, + last_token_type: token_type, + references: Default::default(), + } + } + + /// Returns the raw CSS text from this VariableValue + pub fn css_text(&self) -> &str { + &self.css + } + + /// Returns whether this variable value has any reference to the environment or other + /// variables. + pub fn has_references(&self) -> bool { + self.references.has_references() + } +} + +/// +fn parse_declaration_value<'i, 't>( + input: &mut Parser<'i, 't>, + input_start: SourcePosition, + references: &mut References, + missing_closing_characters: &mut String, +) -> Result<(TokenSerializationType, TokenSerializationType), ParseError<'i>> { + input.parse_until_before(Delimiter::Bang | Delimiter::Semicolon, |input| { + parse_declaration_value_block(input, input_start, 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>, + input_start: SourcePosition, + references: &mut References, + missing_closing_characters: &mut String, +) -> Result<(TokenSerializationType, TokenSerializationType), ParseError<'i>> { + let mut is_first = true; + let mut first_token_type = TokenSerializationType::Nothing; + let mut last_token_type = TokenSerializationType::Nothing; + let mut prev_reference_index: Option = None; + loop { + let token_start = input.position(); + let Ok(token) = input.next_including_whitespace_and_comments() else { break }; + + let prev_token_type = last_token_type; + let serialization_type = token.serialization_type(); + last_token_type = serialization_type; + if is_first { + first_token_type = last_token_type; + is_first = false; + } + + macro_rules! nested { + () => { + input.parse_nested_block(|input| { + parse_declaration_value_block( + input, + input_start, + references, + missing_closing_characters, + ) + })? + }; + } + macro_rules! check_closed { + ($closing:expr) => { + if !input.slice_from(token_start).ends_with($closing) { + missing_closing_characters.push_str($closing) + } + }; + } + if let Some(index) = prev_reference_index.take() { + references.refs[index].next_token_type = serialization_type; + } + match *token { + Token::Comment(_) => { + let token_slice = input.slice_from(token_start); + if !token_slice.ends_with("*/") { + missing_closing_characters.push_str(if token_slice.ends_with('*') { + "/" + } else { + "*/" + }) + } + }, + 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) => { + let is_var = name.eq_ignore_ascii_case("var"); + if is_var || name.eq_ignore_ascii_case("env") { + let our_ref_index = references.refs.len(); + let fallback = input.parse_nested_block(|input| { + // TODO(emilio): For env() this should be per spec, but no other browser does + // that, see https://github.com/w3c/csswg-drafts/issues/3262. + let name = input.expect_ident()?; + let name = Atom::from(if is_var { + match parse_name(name.as_ref()) { + Ok(name) => name, + Err(()) => { + let name = name.clone(); + return Err(input.new_custom_error( + SelectorParseErrorKind::UnexpectedIdent(name), + )); + }, + } + } else { + name.as_ref() + }); + + // We want the order of the references to match source order. So we need to reserve our slot + // now, _before_ parsing our fallback. Note that we don't care if parsing fails after all, since + // if this fails we discard the whole result anyways. + let start = token_start.byte_index() - input_start.byte_index(); + references.refs.push(VarOrEnvReference { + name, + start, + // To be fixed up after parsing fallback and auto-closing via our_ref_index. + end: start, + prev_token_type, + // To be fixed up (if needed) on the next loop iteration via prev_reference_index. + next_token_type: TokenSerializationType::Nothing, + // To be fixed up after parsing fallback. + fallback: None, + is_var, + }); + + let mut fallback = None; + if input.try_parse(|input| input.expect_comma()).is_ok() { + input.skip_whitespace(); + let fallback_start = num::NonZeroUsize::new( + input.position().byte_index() - input_start.byte_index(), + ) + .unwrap(); + // NOTE(emilio): Intentionally using parse_declaration_value rather than + // parse_declaration_value_block, since that's what parse_fallback used to do. + let (first, last) = parse_declaration_value( + input, + input_start, + references, + missing_closing_characters, + )?; + fallback = Some(VariableFallback { + start: fallback_start, + first_token_type: first, + last_token_type: last, + }); + } else { + let state = input.state(); + // We still need to consume the rest of the potentially-unclosed + // tokens, but make sure to not consume tokens that would otherwise be + // invalid, by calling reset(). + parse_declaration_value_block( + input, + input_start, + references, + missing_closing_characters, + )?; + input.reset(&state); + } + Ok(fallback) + })?; + check_closed!(")"); + prev_reference_index = Some(our_ref_index); + let reference = &mut references.refs[our_ref_index]; + reference.end = input.position().byte_index() - input_start.byte_index() + missing_closing_characters.len(); + reference.fallback = fallback; + if is_var { + references.any_var = true; + } else { + references.any_env = true; + } + } else { + nested!(); + check_closed!(")"); + } + }, + Token::ParenthesisBlock => { + nested!(); + check_closed!(")"); + }, + Token::CurlyBracketBlock => { + nested!(); + check_closed!("}"); + }, + Token::SquareBracketBlock => { + nested!(); + check_closed!("]"); + }, + Token::QuotedString(_) => { + 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) + } + }, + 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, .. + } => { + references + .non_custom_references + .insert(NonCustomReferences::from_unit(value)); + 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!(")"); + } + }, + _ => {}, + }; + } + Ok((first_token_type, last_token_type)) +} + +/// A struct that takes care of encapsulating the cascade process for custom properties. +pub struct CustomPropertiesBuilder<'a, 'b: 'a> { + seen: PrecomputedHashSet<&'a Name>, + may_have_cycles: bool, + custom_properties: ComputedCustomProperties, + reverted: PrecomputedHashMap<&'a Name, (CascadePriority, bool)>, + stylist: &'a Stylist, + computed_context: &'a mut computed::Context<'b>, + references_from_non_custom_properties: NonCustomReferenceMap>, +} + +impl<'a, 'b: 'a> CustomPropertiesBuilder<'a, 'b> { + /// Create a new builder, inheriting from a given custom properties map. + /// + /// We expose this publicly mostly for @keyframe blocks. + pub fn new_with_properties(stylist: &'a Stylist, custom_properties: ComputedCustomProperties, computed_context: &'a mut computed::Context<'b>) -> Self { + Self { + seen: PrecomputedHashSet::default(), + reverted: Default::default(), + may_have_cycles: false, + custom_properties, + stylist, + computed_context, + references_from_non_custom_properties: NonCustomReferenceMap::default(), + } + } + + /// Create a new builder, inheriting from the right style given context. + pub fn new(stylist: &'a Stylist, context: &'a mut computed::Context<'b>) -> Self { + let is_root_element = context.is_root_element(); + + let inherited = context.inherited_custom_properties(); + let initial_values = stylist.get_custom_property_initial_values(); + let properties = ComputedCustomProperties { + inherited: if is_root_element { + debug_assert!(inherited.is_empty()); + initial_values.inherited.clone() + } else { + inherited.inherited.clone() + }, + non_inherited: initial_values.non_inherited.clone(), + }; + + // Reuse flags from computing registered custom properties initial values, such as + // whether they depend on viewport units. + context.style().add_flags(stylist.get_custom_property_initial_values_flags()); + Self::new_with_properties(stylist, properties, context) + } + + /// Cascade a given custom property declaration. + pub fn cascade(&mut self, declaration: &'a CustomDeclaration, priority: CascadePriority) { + let CustomDeclaration { + ref name, + ref value, + } = *declaration; + + if let Some(&(reverted_priority, is_origin_revert)) = self.reverted.get(&name) { + if !reverted_priority.allows_when_reverted(&priority, is_origin_revert) { + return; + } + } + + let was_already_present = !self.seen.insert(name); + if was_already_present { + return; + } + + if !self.value_may_affect_style(name, value) { + return; + } + + let map = &mut self.custom_properties; + let registration = self.stylist.get_custom_property_registration(&name); + match *value { + CustomDeclarationValue::Value(ref unparsed_value) => { + let has_custom_property_references = unparsed_value.references.any_var; + let registered_length_property = + registration.syntax.may_reference_font_relative_length(); + // Non-custom dependency is really relevant for registered custom properties + // that require computed value of such dependencies. + let has_non_custom_dependencies = registered_length_property && + !unparsed_value + .references + .get_non_custom_dependencies(self.computed_context.is_root_element()) + .is_empty(); + self.may_have_cycles |= + has_custom_property_references || has_non_custom_dependencies; + + // If the variable value has no references to other properties, perform + // substitution here instead of forcing a full traversal in `substitute_all` + // afterwards. + if !has_custom_property_references && !has_non_custom_dependencies { + return substitute_references_if_needed_and_apply( + name, + unparsed_value, + map, + self.stylist, + self.computed_context, + ); + } + map.insert(registration, name, Arc::clone(unparsed_value)); + }, + CustomDeclarationValue::CSSWideKeyword(keyword) => match keyword { + CSSWideKeyword::RevertLayer | CSSWideKeyword::Revert => { + let origin_revert = keyword == CSSWideKeyword::Revert; + self.seen.remove(name); + self.reverted.insert(name, (priority, origin_revert)); + }, + CSSWideKeyword::Initial => { + // For non-inherited custom properties, 'initial' was handled in value_may_affect_style. + debug_assert!(registration.inherits(), "Should've been handled earlier"); + map.remove(registration, name); + if let Some(ref initial_value) = registration.initial_value { + map.insert(registration, name, initial_value.clone()); + } + }, + CSSWideKeyword::Inherit => { + // For inherited custom properties, 'inherit' was handled in value_may_affect_style. + debug_assert!(!registration.inherits(), "Should've been handled earlier"); + if let Some(inherited_value) = self + .computed_context + .inherited_custom_properties() + .non_inherited + .get(name) + { + map.insert(registration, name, inherited_value.clone()); + } + }, + // handled in value_may_affect_style + CSSWideKeyword::Unset => unreachable!(), + }, + } + } + + /// Note a non-custom property with variable reference that may in turn depend on that property. + /// e.g. `font-size` depending on a custom property that may be a registered property using `em`. + pub fn note_potentially_cyclic_non_custom_dependency(&mut self, id: LonghandId, decl: &VariableDeclaration) { + // With unit algebra in `calc()`, references aren't limited to `font-size`. + // For example, `--foo: 100ex; font-weight: calc(var(--foo) / 1ex);`, + // or `--foo: 1em; zoom: calc(var(--foo) * 30px / 2em);` + let references = match id { + LonghandId::FontSize => { + if self.computed_context.is_root_element() { + NonCustomReferences::ROOT_FONT_UNITS + } else { + NonCustomReferences::FONT_UNITS + } + }, + LonghandId::LineHeight => { + if self.computed_context.is_root_element() { + NonCustomReferences::ROOT_LH_UNITS | + NonCustomReferences::ROOT_FONT_UNITS + } else { + NonCustomReferences::LH_UNITS | NonCustomReferences::FONT_UNITS + } + }, + _ => return, + }; + let refs = &decl.value.variable_value.references; + if !refs.any_var { + return; + } + + let variables: Vec = refs.refs.iter().filter_map(|reference| { + if !reference.is_var { + return None; + } + if !self.stylist.get_custom_property_registration(&reference.name).syntax.may_compute_length() { + return None; + } + Some(reference.name.clone()) + }).collect(); + references.for_each(|idx| { + let entry = &mut self.references_from_non_custom_properties[idx]; + let was_none = entry.is_none(); + let v = entry.get_or_insert_with(|| variables.clone()); + if was_none { + return; + } + v.extend(variables.clone().into_iter()); + }); + } + + fn value_may_affect_style(&self, name: &Name, value: &CustomDeclarationValue) -> bool { + let registration = self.stylist.get_custom_property_registration(&name); + match *value { + CustomDeclarationValue::CSSWideKeyword(CSSWideKeyword::Inherit) => { + // For inherited custom properties, explicit 'inherit' means we + // can just use any existing value in the inherited + // CustomPropertiesMap. + if registration.inherits() { + return false; + } + }, + CustomDeclarationValue::CSSWideKeyword(CSSWideKeyword::Initial) => { + // For non-inherited custom properties, explicit 'initial' means + // we can just use any initial value in the registration. + if !registration.inherits() { + return false; + } + }, + CustomDeclarationValue::CSSWideKeyword(CSSWideKeyword::Unset) => { + // Explicit 'unset' means we can either just use any existing + // value in the inherited CustomPropertiesMap or the initial + // value in the registration. + return false; + }, + _ => {}, + } + + let existing_value = self.custom_properties.get(registration, &name); + match (existing_value, value) { + (None, &CustomDeclarationValue::CSSWideKeyword(CSSWideKeyword::Initial)) => { + debug_assert!(registration.inherits(), "Should've been handled earlier"); + // The initial value of a custom property without a + // guaranteed-invalid initial value is the same as it + // not existing in the map. + if registration.initial_value.is_none() { + return false; + } + }, + ( + Some(existing_value), + &CustomDeclarationValue::CSSWideKeyword(CSSWideKeyword::Initial), + ) => { + debug_assert!(registration.inherits(), "Should've been handled earlier"); + // Don't bother overwriting an existing value with the initial value specified in + // the registration. + if Some(existing_value) == registration.initial_value.as_ref() { + return false; + } + }, + (Some(_), &CustomDeclarationValue::CSSWideKeyword(CSSWideKeyword::Inherit)) => { + debug_assert!(!registration.inherits(), "Should've been handled earlier"); + // existing_value is the registered initial value. + // Don't bother adding it to self.custom_properties.non_inherited + // if the key is also absent from self.inherited.non_inherited. + if self + .computed_context + .inherited_custom_properties() + .non_inherited + .get(name) + .is_none() + { + return false; + } + }, + (Some(existing_value), &CustomDeclarationValue::Value(ref value)) => { + // Don't bother overwriting an existing value with the same + // specified value. + if existing_value == value { + return false; + } + }, + _ => {}, + } + + true + } + + /// Computes the map of applicable custom properties, as well as + /// longhand properties that are now considered invalid-at-compute time. + /// The result is saved into the computed context. + /// + /// If there was any specified property or non-inherited custom property + /// with an initial value, we've created a new map and now we + /// need to remove any potential cycles (And marking non-custom + /// properties), and wrap it in an arc. + /// + /// Some registered custom properties may require font-related properties + /// be resolved to resolve. If these properties are not resolved at this time, + /// `defer` should be set to `Yes`, which will leave such custom properties, + /// and other properties referencing them, untouched. These properties are + /// returned separately, to be resolved by `build_deferred` to fully resolve + /// all custom properties after all necessary non-custom properties are resolved. + pub fn build( + mut self, + defer: DeferFontRelativeCustomPropertyResolution, + ) -> Option { + let mut deferred_custom_properties = None; + if self.may_have_cycles { + if defer == DeferFontRelativeCustomPropertyResolution::Yes { + deferred_custom_properties = Some(ComputedCustomProperties::default()); + } + let mut invalid_non_custom_properties = LonghandIdSet::default(); + substitute_all( + &mut self.custom_properties, + deferred_custom_properties.as_mut(), + &mut invalid_non_custom_properties, + &self.seen, + &self.references_from_non_custom_properties, + self.stylist, + self.computed_context, + ); + self.computed_context.builder.invalid_non_custom_properties = invalid_non_custom_properties; + } + + self.custom_properties.shrink_to_fit(); + + // Some pages apply a lot of redundant custom properties, see e.g. + // bug 1758974 comment 5. Try to detect the case where the values + // haven't really changed, and save some memory by reusing the inherited + // map in that case. + let initial_values = self.stylist.get_custom_property_initial_values(); + self.computed_context.builder.custom_properties = ComputedCustomProperties { + inherited: if self + .computed_context + .inherited_custom_properties() + .inherited == self.custom_properties.inherited + { + self.computed_context + .inherited_custom_properties() + .inherited + .clone() + } else { + self.custom_properties.inherited + }, + non_inherited: if initial_values.non_inherited == self.custom_properties.non_inherited { + initial_values.non_inherited.clone() + } else { + self.custom_properties.non_inherited + }, + }; + + deferred_custom_properties + } + + /// Fully resolve all deferred custom properties, assuming that the incoming context + /// has necessary properties resolved. + pub fn build_deferred( + deferred: ComputedCustomProperties, + stylist: &Stylist, + computed_context: &mut computed::Context, + ) { + if deferred.is_empty() { + return; + } + // Guaranteed to not have cycles at this point. + let substitute = + |deferred: &CustomPropertiesMap, + stylist: &Stylist, + context: &computed::Context, + custom_properties: &mut ComputedCustomProperties| { + // Since `CustomPropertiesMap` preserves insertion order, we shouldn't + // have to worry about resolving in a wrong order. + for (k, v) in deferred.iter() { + let Some(v) = v else { continue }; + substitute_references_if_needed_and_apply( + k, + v, + custom_properties, + stylist, + context, + ); + } + }; + let mut custom_properties = std::mem::take(&mut computed_context.builder.custom_properties); + substitute( + &deferred.inherited, + stylist, + computed_context, + &mut custom_properties, + ); + substitute( + &deferred.non_inherited, + stylist, + computed_context, + &mut custom_properties, + ); + computed_context.builder.custom_properties = custom_properties; + } +} + +/// 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 ComputedCustomProperties, + mut deferred_properties_map: Option<&mut ComputedCustomProperties>, + invalid_non_custom_properties: &mut LonghandIdSet, + seen: &PrecomputedHashSet<&Name>, + references_from_non_custom_properties: &NonCustomReferenceMap>, + stylist: &Stylist, + computed_context: &computed::Context, +) { + // 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 + + #[derive(Clone, Eq, PartialEq, Debug)] + enum VarType { + Custom(Name), + NonCustom(SingleNonCustomReference), + } + + /// 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. + var: 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. + struct Context<'a, 'b: '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, + /// Mapping from a non-custom dependency to its order index. + non_custom_index_map: NonCustomReferenceMap, + /// 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]>, + /// References to non-custom properties in this strongly connected component. + non_custom_references: NonCustomReferences, + map: &'a mut ComputedCustomProperties, + /// The stylist is used to get registered properties, and to resolve the environment to + /// substitute `env()` variables. + stylist: &'a Stylist, + /// The computed context is used to get inherited custom + /// properties and compute registered custom properties. + computed_context: &'a computed::Context<'b>, + /// Longhand IDs that became invalid due to dependency cycle(s). + invalid_non_custom_properties: &'a mut LonghandIdSet, + /// Properties that cannot yet be substituted. + deferred_properties: Option<&'a mut ComputedCustomProperties>, + } + + /// 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, 'b>( + var: VarType, + non_custom_references: &NonCustomReferenceMap>, + context: &mut Context<'a, 'b>, + ) -> Option { + // Some shortcut checks. + let (value, should_substitute) = match var { + VarType::Custom(ref name) => { + let registration = context.stylist.get_custom_property_registration(name); + let value = context.map.get(registration, name)?; + + let non_custom_references = value + .references + .get_non_custom_dependencies(context.computed_context.is_root_element()); + let has_custom_property_reference = value.references.any_var; + // Nothing to resolve. + if !has_custom_property_reference && non_custom_references.is_empty() { + debug_assert!(!value.references.any_env, "Should've been handled earlier"); + return None; + } + + // Has this variable been visited? + match context.index_map.entry(name.clone()) { + Entry::Occupied(entry) => { + return Some(*entry.get()); + }, + Entry::Vacant(entry) => { + entry.insert(context.count); + }, + } + context.non_custom_references |= value.as_ref().references.non_custom_references; + + // Hold a strong reference to the value so that we don't + // need to keep reference to context.map. + (Some(value.clone()), has_custom_property_reference) + }, + VarType::NonCustom(ref non_custom) => { + let entry = &mut context.non_custom_index_map[*non_custom]; + if let Some(v) = entry { + return Some(*v); + } + *entry = Some(context.count); + (None, false) + }, + }; + + // 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 { + var: Some(var.clone()), + lowlink: index, + }); + context.stack.push(index); + + let mut self_ref = false; + let mut lowlink = index; + let visit_link = + |var: VarType, context: &mut Context, lowlink: &mut usize, self_ref: &mut bool| { + let next_index = match traverse(var, non_custom_references, context) { + Some(index) => index, + // There is nothing to do if the next variable has been + // fully resolved at this point. + None => { + return; + }, + }; + 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.var.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); + } + }; + if let Some(ref v) = value.as_ref() { + debug_assert!( + matches!(var, VarType::Custom(_)), + "Non-custom property has references?" + ); + + // Visit other custom properties... + // FIXME: Maybe avoid visiting the same var twice if not needed? + for next in &v.references.refs { + if !next.is_var { + continue; + } + visit_link( + VarType::Custom(next.name.clone()), + context, + &mut lowlink, + &mut self_ref, + ); + } + + // ... Then non-custom properties. + v.references.non_custom_references.for_each(|r| { + visit_link(VarType::NonCustom(r), context, &mut lowlink, &mut self_ref); + }); + } else if let VarType::NonCustom(non_custom) = var { + let entry = &non_custom_references[non_custom]; + if let Some(deps) = entry.as_ref() { + for d in deps { + // Visit any reference from this non-custom property to custom properties. + visit_link( + VarType::Custom(d.clone()), + context, + &mut lowlink, + &mut self_ref, + ); + } + } + } + + 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; + + let handle_variable_in_loop = |name: &Name, context: &mut Context<'a, 'b>| { + if context + .non_custom_references + .intersects(NonCustomReferences::FONT_UNITS | NonCustomReferences::ROOT_FONT_UNITS) + { + context + .invalid_non_custom_properties + .insert(LonghandId::FontSize); + } + if context.non_custom_references.intersects( + NonCustomReferences::LH_UNITS | + NonCustomReferences::ROOT_LH_UNITS, + ) { + context + .invalid_non_custom_properties + .insert(LonghandId::LineHeight); + } + // This variable is in loop. Resolve to invalid. + handle_invalid_at_computed_value_time( + name, + context.map, + context.computed_context.inherited_custom_properties(), + context.stylist, + context.computed_context.is_root_element(), + ); + }; + 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 + .var + .take() + .expect("Variable should not be poped from stack twice"); + if var_index == index { + name = match var_name { + VarType::Custom(name) => name, + // At the root of this component, and it's a non-custom + // reference - we have nothing to substitute, so + // it's effectively resolved. + VarType::NonCustom(..) => return None, + }; + break; + } + if let VarType::Custom(name) = var_name { + // Anything here is in a loop which can traverse to the + // variable we are handling, so it's invalid at + // computed-value time. + handle_variable_in_loop(&name, context); + } + in_loop = true; + } + // We've gotten to the root of this strongly connected component, so clear + // whether or not it involved non-custom references. + // It's fine to track it like this, because non-custom properties currently + // being tracked can only participate in any loop only once. + if in_loop { + handle_variable_in_loop(&name, context); + context.non_custom_references = NonCustomReferences::default(); + return None; + } + + if let Some(ref v) = value.as_ref() { + let registration = context.stylist.get_custom_property_registration(&name); + let registered_length_property = + registration.syntax.may_reference_font_relative_length(); + let mut defer = false; + if !context.non_custom_references.is_empty() && registered_length_property { + if let Some(deferred) = &mut context.deferred_properties { + // This property directly depends on a non-custom property, defer resolving it. + deferred.insert(registration, &name, (*v).clone()); + context.map.remove(registration, &name); + defer = true; + } + } + if should_substitute && !defer { + for reference in v.references.refs.iter() { + if !reference.is_var { + continue; + } + if let Some(deferred) = &mut context.deferred_properties { + let registration = + context.stylist.get_custom_property_registration(&reference.name); + if deferred.get(registration, &reference.name).is_some() { + // This property depends on a custom property that depends on a non-custom property, defer. + deferred.insert(registration, &name, Arc::clone(v)); + context.map.remove(registration, &name); + defer = true; + break; + } + } + } + if !defer { + substitute_references_if_needed_and_apply( + &name, + v, + &mut context.map, + context.stylist, + context.computed_context, + ); + } + } + } + context.non_custom_references = NonCustomReferences::default(); + + // All resolved, so return the signal value. + None + } + + // Note that `seen` doesn't contain names inherited from our parent, but + // those can't have variable references (since we inherit the computed + // variables) so we don't want to spend cycles traversing them anyway. + for name in seen { + let mut context = Context { + count: 0, + index_map: PrecomputedHashMap::default(), + non_custom_index_map: NonCustomReferenceMap::default(), + stack: SmallVec::new(), + var_info: SmallVec::new(), + map: custom_properties_map, + non_custom_references: NonCustomReferences::default(), + stylist, + computed_context, + invalid_non_custom_properties, + deferred_properties: deferred_properties_map.as_deref_mut(), + }; + traverse( + VarType::Custom((*name).clone()), + references_from_non_custom_properties, + &mut context, + ); + } +} + +// See https://drafts.csswg.org/css-variables-2/#invalid-at-computed-value-time +fn handle_invalid_at_computed_value_time( + name: &Name, + custom_properties: &mut ComputedCustomProperties, + inherited: &ComputedCustomProperties, + stylist: &Stylist, + is_root_element: bool, +) { + let registration = stylist.get_custom_property_registration(&name); + if !registration.syntax.is_universal() { + // For the root element, inherited maps are empty. We should just + // use the initial value if any, rather than removing the name. + if registration.inherits() && !is_root_element { + if let Some(value) = inherited.get(registration, name) { + custom_properties.insert(registration, name, Arc::clone(value)); + return; + } + } else { + if let Some(ref initial_value) = registration.initial_value { + custom_properties.insert(registration, name, Arc::clone(initial_value)); + return; + } + } + } + custom_properties.remove(registration, name); +} + +/// Replace `var()` and `env()` functions in a pre-existing variable value. +fn substitute_references_if_needed_and_apply( + name: &Name, + value: &Arc, + custom_properties: &mut ComputedCustomProperties, + stylist: &Stylist, + computed_context: &computed::Context, +) { + let registration = stylist.get_custom_property_registration(&name); + if !value.has_references() && registration.syntax.is_universal() { + // Trivial path: no references and no need to compute the value, just apply it directly. + custom_properties.insert(registration, name, Arc::clone(value)); + return; + } + + let inherited = computed_context.inherited_custom_properties(); + let value = match substitute_internal(value, custom_properties, stylist, registration, computed_context) { + Ok(v) => v, + Err(..) => { + handle_invalid_at_computed_value_time( + name, + custom_properties, + inherited, + stylist, + computed_context.is_root_element(), + ); + return; + }, + }.into_value(&value.url_data); + + // If variable fallback results in a wide keyword, deal with it now. + { + let mut input = ParserInput::new(&value.css); + let mut input = Parser::new(&mut input); + + if let Ok(kw) = input.try_parse(CSSWideKeyword::parse) { + // TODO: It's unclear what this should do for revert / revert-layer, see + // https://github.com/w3c/csswg-drafts/issues/9131. For now treating as unset + // seems fine? + match (kw, registration.inherits(), computed_context.is_root_element()) { + (CSSWideKeyword::Initial, _, _) | + (CSSWideKeyword::Revert, false, _) | + (CSSWideKeyword::RevertLayer, false, _) | + (CSSWideKeyword::Unset, false, _) | + (CSSWideKeyword::Revert, true, true) | + (CSSWideKeyword::RevertLayer, true, true) | + (CSSWideKeyword::Unset, true, true) | + (CSSWideKeyword::Inherit, _, true) => { + custom_properties.remove(registration, name); + if let Some(ref initial_value) = registration.initial_value { + custom_properties.insert(registration, name, Arc::clone(initial_value)); + } + }, + (CSSWideKeyword::Revert, true, false) | + (CSSWideKeyword::RevertLayer, true, false) | + (CSSWideKeyword::Inherit, _, false) | + (CSSWideKeyword::Unset, true, false) => { + match inherited.get(registration, name) { + Some(value) => { + custom_properties.insert(registration, name, Arc::clone(value)); + }, + None => { + custom_properties.remove(registration, name); + }, + }; + }, + } + return; + } + } + + custom_properties.insert(registration, name, Arc::new(value)); +} + +#[derive(Default)] +struct Substitution<'a> { + css: Cow<'a, str>, + first_token_type: TokenSerializationType, + last_token_type: TokenSerializationType, +} + +impl<'a> Substitution<'a> { + fn new( + css: &'a str, + first_token_type: TokenSerializationType, + last_token_type: TokenSerializationType, + ) -> Self { + Self { + css: Cow::Borrowed(css), + first_token_type, + last_token_type, + } + } + + fn from_value(v: VariableValue) -> Substitution<'static> { + debug_assert!(!v.has_references(), "Computed values shouldn't have references"); + Substitution { + css: Cow::from(v.css), + first_token_type: v.first_token_type, + last_token_type: v.last_token_type, + } + } + + fn into_value(self, url_data: &UrlExtraData) -> VariableValue { + VariableValue { + css: self.css.into_owned(), + first_token_type: self.first_token_type, + last_token_type: self.last_token_type, + url_data: url_data.clone(), + references: Default::default(), + } + } +} + +fn compute_value( + css: &str, + url_data: &UrlExtraData, + registration: &PropertyRegistrationData, + computed_context: &computed::Context, +) -> Result, ()> { + debug_assert!(!registration.syntax.is_universal()); + + let mut input = ParserInput::new(&css); + let mut input = Parser::new(&mut input); + + let value = SpecifiedRegisteredValue::compute( + &mut input, + registration, + url_data, + computed_context, + AllowComputationallyDependent::Yes, + )?; + Ok(Substitution::from_value(value)) +} + +fn do_substitute_chunk<'a>( + css: &'a str, + start: usize, + end: usize, + first_token_type: TokenSerializationType, + last_token_type: TokenSerializationType, + url_data: &UrlExtraData, + custom_properties: &'a ComputedCustomProperties, + registration: &PropertyRegistrationData, + stylist: &Stylist, + computed_context: &computed::Context, + references: &mut std::iter::Peekable>, +) -> Result, ()> { + if start == end { + // Empty string. Easy. + return Ok(Substitution::default()); + } + // Easy case: no references involved. + if references + .peek() + .map_or(true, |reference| reference.end > end) + { + let result = &css[start..end]; + if !registration.syntax.is_universal() { + return compute_value(result, url_data, registration, computed_context); + } + return Ok(Substitution::new(result, first_token_type, last_token_type)); + } + + let mut substituted = ComputedValue::empty(url_data); + let mut next_token_type = first_token_type; + let mut cur_pos = start; + while let Some(reference) = references.next_if(|reference| reference.end <= end) { + if reference.start != cur_pos { + substituted.push( + &css[cur_pos..reference.start], + next_token_type, + reference.prev_token_type, + )?; + } + + let substitution = substitute_one_reference( + css, + url_data, + custom_properties, + reference, + stylist, + computed_context, + references, + )?; + + // Optimize the property: var(--...) case to avoid allocating at all. + if reference.start == start && reference.end == end && registration.syntax.is_universal() { + return Ok(substitution); + } + + substituted.push( + &substitution.css, + substitution.first_token_type, + substitution.last_token_type, + )?; + next_token_type = reference.next_token_type; + cur_pos = reference.end; + } + // Push the rest of the value if needed. + if cur_pos != end { + substituted.push(&css[cur_pos..end], next_token_type, last_token_type)?; + } + if !registration.syntax.is_universal() { + return compute_value(&substituted.css, url_data, registration, computed_context); + } + Ok(Substitution::from_value(substituted)) +} + +fn substitute_one_reference<'a>( + css: &'a str, + url_data: &UrlExtraData, + custom_properties: &'a ComputedCustomProperties, + reference: &VarOrEnvReference, + stylist: &Stylist, + computed_context: &computed::Context, + references: &mut std::iter::Peekable>, +) -> Result, ()> { + let registration; + if reference.is_var { + registration = stylist.get_custom_property_registration(&reference.name); + if let Some(v) = custom_properties.get(registration, &reference.name) { + debug_assert!(!v.has_references(), "Should be already computed"); + if registration.syntax.is_universal() { + // Skip references that are inside the outer variable (in fallback for example). + while references + .next_if(|next_ref| next_ref.end <= reference.end) + .is_some() + {} + } else { + // We need to validate the fallback if any, since invalid fallback should + // invalidate the whole variable. + if let Some(ref fallback) = reference.fallback { + let _ = do_substitute_chunk( + css, + fallback.start.get(), + reference.end - 1, // Don't include the closing parenthesis. + fallback.first_token_type, + fallback.last_token_type, + url_data, + custom_properties, + registration, + stylist, + computed_context, + references, + )?; + } + } + return Ok(Substitution { + css: Cow::from(&v.css), + first_token_type: v.first_token_type, + last_token_type: v.last_token_type, + }); + } + } else { + registration = PropertyRegistrationData::unregistered(); + let device = stylist.device(); + if let Some(v) = device.environment().get(&reference.name, device, url_data) { + while references + .next_if(|next_ref| next_ref.end <= reference.end) + .is_some() + {} + return Ok(Substitution::from_value(v)); + } + } + + let Some(ref fallback) = reference.fallback else { return Err(()) }; + + do_substitute_chunk( + css, + fallback.start.get(), + reference.end - 1, // Skip the closing parenthesis of the reference value. + fallback.first_token_type, + fallback.last_token_type, + url_data, + custom_properties, + registration, + stylist, + computed_context, + references, + ) +} + +/// Replace `var()` and `env()` functions. Return `Err(..)` for invalid at computed time. +fn substitute_internal<'a>( + variable_value: &'a VariableValue, + custom_properties: &'a ComputedCustomProperties, + stylist: &Stylist, + registration: &PropertyRegistrationData, + computed_context: &computed::Context, +) -> Result, ()> { + let mut refs = variable_value.references.refs.iter().peekable(); + do_substitute_chunk( + &variable_value.css, + /* start = */ 0, + /* end = */ variable_value.css.len(), + variable_value.first_token_type, + variable_value.last_token_type, + &variable_value.url_data, + custom_properties, + registration, + stylist, + computed_context, + &mut refs, + ) +} + +/// Replace var() and env() functions, returning the resulting CSS string. +pub fn substitute<'a>( + variable_value: &'a VariableValue, + custom_properties: &'a ComputedCustomProperties, + stylist: &Stylist, + computed_context: &computed::Context, +) -> Result, ()> { + debug_assert!(variable_value.has_references()); + let v = substitute_internal( + variable_value, + custom_properties, + stylist, + PropertyRegistrationData::unregistered(), + computed_context, + )?; + Ok(v.css) +} diff --git a/servo/components/style/custom_properties_map.rs b/servo/components/style/custom_properties_map.rs new file mode 100644 index 0000000000..04ca8e1b3d --- /dev/null +++ b/servo/components/style/custom_properties_map.rs @@ -0,0 +1,237 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! The structure that contains the custom properties of a given element. + +use crate::custom_properties::{Name, VariableValue}; +use crate::selector_map::PrecomputedHasher; +use indexmap::IndexMap; +use servo_arc::Arc; +use std::hash::BuildHasherDefault; + +/// A map for a set of custom properties, which implements copy-on-write behavior on insertion with +/// cheap copying. +#[derive(Clone, Debug, PartialEq)] +pub struct CustomPropertiesMap(Arc); + +impl Default for CustomPropertiesMap { + fn default() -> Self { + Self(EMPTY.clone()) + } +} + +/// We use None in the value to represent a removed entry. +type OwnMap = IndexMap>, BuildHasherDefault>; + +// IndexMap equality doesn't consider ordering, which we want to account for. Also, for the same +// reason, IndexMap equality comparisons are slower than needed. +// +// See https://github.com/bluss/indexmap/issues/153. +// TODO: use as_slice when updating to indexmap 2.0. +fn maps_equal(l: &OwnMap, r: &OwnMap) -> bool { + if std::ptr::eq(l, r) { + return true; + } + if l.len() != r.len() { + return false; + } + l.iter() + .zip(r.iter()) + .all(|((k1, v1), (k2, v2))| k1 == k2 && v1 == v2) +} + +lazy_static! { + static ref EMPTY: Arc = { + Arc::new_leaked(Inner { + own_properties: Default::default(), + parent: None, + len: 0, + ancestor_count: 0, + }) + }; +} + +#[derive(Debug, Clone)] +struct Inner { + own_properties: OwnMap, + parent: Option>, + /// The number of custom properties we store. Note that this is different from the sum of our + /// own and our parent's length, since we might store duplicate entries. + len: usize, + /// The number of ancestors we have. + ancestor_count: u8, +} + +/// A not-too-large, not too small ancestor limit, to prevent creating too-big chains. +const ANCESTOR_COUNT_LIMIT: usize = 4; + +/// An iterator over the custom properties. +pub struct Iter<'a> { + current: &'a Inner, + current_iter: indexmap::map::Iter<'a, Name, Option>>, + descendants: smallvec::SmallVec<[&'a Inner; ANCESTOR_COUNT_LIMIT]>, +} + +impl<'a> Iterator for Iter<'a> { + type Item = (&'a Name, &'a Option>); + + fn next(&mut self) -> Option { + loop { + let (name, value) = match self.current_iter.next() { + Some(v) => v, + None => { + let parent = self.current.parent.as_deref()?; + self.descendants.push(self.current); + self.current = parent; + self.current_iter = parent.own_properties.iter(); + continue; + }, + }; + // If the property is overridden by a descendant we've already visited it. + for descendant in &self.descendants { + if descendant.own_properties.contains_key(name) { + continue; + } + } + return Some((name, value)); + } + } +} + +impl PartialEq for Inner { + fn eq(&self, other: &Self) -> bool { + if self.len != other.len { + return false; + } + if self.parent_ptr_eq(other) { + return maps_equal(&self.own_properties, &other.own_properties); + } + for (name, value) in self.iter() { + if other.get(name) != value.as_ref() { + return false; + } + } + return true; + } +} + +impl Inner { + fn parent_ptr_eq(&self, other: &Self) -> bool { + match (&self.parent, &other.parent) { + (Some(p1), Some(p2)) => Arc::ptr_eq(p1, p2), + (None, None) => true, + _ => false, + } + } + + fn iter(&self) -> Iter { + Iter { + current: self, + current_iter: self.own_properties.iter(), + descendants: Default::default(), + } + } + + fn is_empty(&self) -> bool { + self.len == 0 + } + + fn len(&self) -> usize { + self.len + } + + fn get(&self, name: &Name) -> Option<&Arc> { + if let Some(p) = self.own_properties.get(name) { + return p.as_ref(); + } + self.parent.as_ref()?.get(name) + } + + fn insert(&mut self, name: &Name, value: Option>) { + let new = self.own_properties.insert(name.clone(), value).is_none(); + if new && self.parent.as_ref().map_or(true, |p| p.get(name).is_none()) { + self.len += 1; + } + } + + /// Whether we should expand the chain, or just copy-on-write. + fn should_expand_chain(&self) -> bool { + const SMALL_THRESHOLD: usize = 8; + if self.own_properties.len() <= SMALL_THRESHOLD { + return false; // Just copy, to avoid very long chains. + } + self.ancestor_count < ANCESTOR_COUNT_LIMIT as u8 + } +} + +impl CustomPropertiesMap { + /// Returns whether the map has no properties in it. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Returns the amount of different properties in the map. + pub fn len(&self) -> usize { + self.0.len() + } + + /// Returns the property name and value at a given index. + pub fn get_index(&self, index: usize) -> Option<(&Name, &Option>)> { + if index >= self.len() { + return None; + } + // FIXME: This is O(n) which is a bit unfortunate. + self.0.iter().nth(index) + } + + /// Returns a given property value by name. + pub fn get(&self, name: &Name) -> Option<&Arc> { + self.0.get(name) + } + + fn do_insert(&mut self, name: &Name, value: Option>) { + if let Some(inner) = Arc::get_mut(&mut self.0) { + return inner.insert(name, value); + } + if self.get(name) == value.as_ref() { + return; + } + if !self.0.should_expand_chain() { + return Arc::make_mut(&mut self.0).insert(name, value); + } + let len = self.0.len; + let ancestor_count = self.0.ancestor_count + 1; + let mut new_inner = Inner { + own_properties: Default::default(), + // FIXME: Would be nice to avoid this clone. + parent: Some(self.0.clone()), + len, + ancestor_count, + }; + new_inner.insert(name, value); + self.0 = Arc::new(new_inner); + } + + /// Inserts an element in the map. + pub fn insert(&mut self, name: &Name, value: Arc) { + self.do_insert(name, Some(value)) + } + + /// Removes an element from the map. + pub fn remove(&mut self, name: &Name) { + self.do_insert(name, None) + } + + /// Shrinks the map as much as possible. + pub fn shrink_to_fit(&mut self) { + if let Some(inner) = Arc::get_mut(&mut self.0) { + inner.own_properties.shrink_to_fit() + } + } + + /// Return iterator to go through all properties. + pub fn iter(&self) -> Iter { + self.0.iter() + } +} diff --git a/servo/components/style/data.rs b/servo/components/style/data.rs new file mode 100644 index 0000000000..ceddc5bd20 --- /dev/null +++ b/servo/components/style/data.rs @@ -0,0 +1,545 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Per-node data used in style calculation. + +use crate::computed_value_flags::ComputedValueFlags; +use crate::context::{SharedStyleContext, StackLimitChecker}; +use crate::dom::TElement; +use crate::invalidation::element::invalidator::InvalidationResult; +use crate::invalidation::element::restyle_hints::RestyleHint; +use crate::properties::ComputedValues; +use crate::selector_parser::{PseudoElement, RestyleDamage, EAGER_PSEUDO_COUNT}; +use crate::style_resolver::{PrimaryStyle, ResolvedElementStyles, ResolvedStyle}; +#[cfg(feature = "gecko")] +use malloc_size_of::MallocSizeOfOps; +use selectors::matching::SelectorCaches; +use servo_arc::Arc; +use std::fmt; +use std::mem; +use std::ops::{Deref, DerefMut}; + +bitflags! { + /// Various flags stored on ElementData. + #[derive(Debug, Default)] + pub struct ElementDataFlags: u8 { + /// Whether the styles changed for this restyle. + const WAS_RESTYLED = 1 << 0; + /// Whether the last traversal of this element did not do + /// any style computation. This is not true during the initial + /// styling pass, nor is it true when we restyle (in which case + /// WAS_RESTYLED is set). + /// + /// This bit always corresponds to the last time the element was + /// traversed, so each traversal simply updates it with the appropriate + /// value. + const TRAVERSED_WITHOUT_STYLING = 1 << 1; + + /// Whether the primary style of this element data was reused from + /// another element via a rule node comparison. This allows us to + /// differentiate between elements that shared styles because they met + /// all the criteria of the style sharing cache, compared to elements + /// that reused style structs via rule node identity. + /// + /// The former gives us stronger transitive guarantees that allows us to + /// apply the style sharing cache to cousins. + const PRIMARY_STYLE_REUSED_VIA_RULE_NODE = 1 << 2; + } +} + +/// A lazily-allocated list of styles for eagerly-cascaded pseudo-elements. +/// +/// We use an Arc so that sharing these styles via the style sharing cache does +/// not require duplicate allocations. We leverage the copy-on-write semantics of +/// Arc::make_mut(), which is free (i.e. does not require atomic RMU operations) +/// in servo_arc. +#[derive(Clone, Debug, Default)] +pub struct EagerPseudoStyles(Option>); + +#[derive(Default)] +struct EagerPseudoArray(EagerPseudoArrayInner); +type EagerPseudoArrayInner = [Option>; EAGER_PSEUDO_COUNT]; + +impl Deref for EagerPseudoArray { + type Target = EagerPseudoArrayInner; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for EagerPseudoArray { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +// Manually implement `Clone` here because the derived impl of `Clone` for +// array types assumes the value inside is `Copy`. +impl Clone for EagerPseudoArray { + fn clone(&self) -> Self { + let mut clone = Self::default(); + for i in 0..EAGER_PSEUDO_COUNT { + clone[i] = self.0[i].clone(); + } + clone + } +} + +// Override Debug to print which pseudos we have, and substitute the rule node +// for the much-more-verbose ComputedValues stringification. +impl fmt::Debug for EagerPseudoArray { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "EagerPseudoArray {{ ")?; + for i in 0..EAGER_PSEUDO_COUNT { + if let Some(ref values) = self[i] { + write!( + f, + "{:?}: {:?}, ", + PseudoElement::from_eager_index(i), + &values.rules + )?; + } + } + write!(f, "}}") + } +} + +// Can't use [None; EAGER_PSEUDO_COUNT] here because it complains +// about Copy not being implemented for our Arc type. +#[cfg(feature = "gecko")] +const EMPTY_PSEUDO_ARRAY: &'static EagerPseudoArrayInner = &[None, None, None, None]; +#[cfg(feature = "servo")] +const EMPTY_PSEUDO_ARRAY: &'static EagerPseudoArrayInner = &[None, None, None]; + +impl EagerPseudoStyles { + /// Returns whether there are any pseudo styles. + pub fn is_empty(&self) -> bool { + self.0.is_none() + } + + /// Grabs a reference to the list of styles, if they exist. + pub fn as_optional_array(&self) -> Option<&EagerPseudoArrayInner> { + match self.0 { + None => None, + Some(ref x) => Some(&x.0), + } + } + + /// Grabs a reference to the list of styles or a list of None if + /// there are no styles to be had. + pub fn as_array(&self) -> &EagerPseudoArrayInner { + self.as_optional_array().unwrap_or(EMPTY_PSEUDO_ARRAY) + } + + /// Returns a reference to the style for a given eager pseudo, if it exists. + pub fn get(&self, pseudo: &PseudoElement) -> Option<&Arc> { + debug_assert!(pseudo.is_eager()); + self.0 + .as_ref() + .and_then(|p| p[pseudo.eager_index()].as_ref()) + } + + /// Sets the style for the eager pseudo. + pub fn set(&mut self, pseudo: &PseudoElement, value: Arc) { + if self.0.is_none() { + self.0 = Some(Arc::new(Default::default())); + } + let arr = Arc::make_mut(self.0.as_mut().unwrap()); + arr[pseudo.eager_index()] = Some(value); + } +} + +/// The styles associated with a node, including the styles for any +/// pseudo-elements. +#[derive(Clone, Default)] +pub struct ElementStyles { + /// The element's style. + pub primary: Option>, + /// A list of the styles for the element's eagerly-cascaded pseudo-elements. + pub pseudos: EagerPseudoStyles, +} + +// There's one of these per rendered elements so it better be small. +size_of_test!(ElementStyles, 16); + +/// Information on how this element uses viewport units. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum ViewportUnitUsage { + /// No viewport units are used. + None = 0, + /// There are viewport units used from regular style rules (which means we + /// should re-cascade). + FromDeclaration, + /// There are viewport units used from container queries (which means we + /// need to re-selector-match). + FromQuery, +} + +impl ElementStyles { + /// Returns the primary style. + pub fn get_primary(&self) -> Option<&Arc> { + self.primary.as_ref() + } + + /// Returns the primary style. Panic if no style available. + pub fn primary(&self) -> &Arc { + self.primary.as_ref().unwrap() + } + + /// Whether this element `display` value is `none`. + pub fn is_display_none(&self) -> bool { + self.primary().get_box().clone_display().is_none() + } + + /// Whether this element uses viewport units. + pub fn viewport_unit_usage(&self) -> ViewportUnitUsage { + fn usage_from_flags(flags: ComputedValueFlags) -> ViewportUnitUsage { + if flags.intersects(ComputedValueFlags::USES_VIEWPORT_UNITS_ON_CONTAINER_QUERIES) { + return ViewportUnitUsage::FromQuery; + } + if flags.intersects(ComputedValueFlags::USES_VIEWPORT_UNITS) { + return ViewportUnitUsage::FromDeclaration; + } + ViewportUnitUsage::None + } + + let mut usage = usage_from_flags(self.primary().flags); + for pseudo_style in self.pseudos.as_array() { + if let Some(ref pseudo_style) = pseudo_style { + usage = std::cmp::max(usage, usage_from_flags(pseudo_style.flags)); + } + } + + usage + } + + #[cfg(feature = "gecko")] + fn size_of_excluding_cvs(&self, _ops: &mut MallocSizeOfOps) -> usize { + // As the method name suggests, we don't measures the ComputedValues + // here, because they are measured on the C++ side. + + // XXX: measure the EagerPseudoArray itself, but not the ComputedValues + // within it. + + 0 + } +} + +// We manually implement Debug for ElementStyles so that we can avoid the +// verbose stringification of every property in the ComputedValues. We +// substitute the rule node instead. +impl fmt::Debug for ElementStyles { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "ElementStyles {{ primary: {:?}, pseudos: {:?} }}", + self.primary.as_ref().map(|x| &x.rules), + self.pseudos + ) + } +} + +/// Style system data associated with an Element. +/// +/// In Gecko, this hangs directly off the Element. Servo, this is embedded +/// inside of layout data, which itself hangs directly off the Element. In +/// both cases, it is wrapped inside an AtomicRefCell to ensure thread safety. +#[derive(Debug, Default)] +pub struct ElementData { + /// The styles for the element and its pseudo-elements. + pub styles: ElementStyles, + + /// The restyle damage, indicating what kind of layout changes are required + /// afte restyling. + pub damage: RestyleDamage, + + /// The restyle hint, which indicates whether selectors need to be rematched + /// for this element, its children, and its descendants. + pub hint: RestyleHint, + + /// Flags. + pub flags: ElementDataFlags, +} + +// There's one of these per rendered elements so it better be small. +size_of_test!(ElementData, 24); + +/// The kind of restyle that a single element should do. +#[derive(Debug)] +pub enum RestyleKind { + /// We need to run selector matching plus re-cascade, that is, a full + /// restyle. + MatchAndCascade, + /// We need to recascade with some replacement rule, such as the style + /// attribute, or animation rules. + CascadeWithReplacements(RestyleHint), + /// We only need to recascade, for example, because only inherited + /// properties in the parent changed. + CascadeOnly, +} + +impl ElementData { + /// Invalidates style for this element, its descendants, and later siblings, + /// based on the snapshot of the element that we took when attributes or + /// state changed. + pub fn invalidate_style_if_needed<'a, E: TElement>( + &mut self, + element: E, + shared_context: &SharedStyleContext, + stack_limit_checker: Option<&StackLimitChecker>, + selector_caches: &'a mut SelectorCaches, + ) -> 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, selector_caches); + + let invalidator = TreeStyleInvalidator::new(element, stack_limit_checker, &mut processor); + + let result = invalidator.invalidate(); + + unsafe { element.set_handled_snapshot() } + debug_assert!(element.handled_snapshot()); + + result + } + + /// Returns true if this element has styles. + #[inline] + pub fn has_styles(&self) -> bool { + self.styles.primary.is_some() + } + + /// Returns this element's styles as resolved styles to use for sharing. + pub fn share_styles(&self) -> ResolvedElementStyles { + ResolvedElementStyles { + primary: self.share_primary_style(), + pseudos: self.styles.pseudos.clone(), + } + } + + /// Returns this element's primary style as a resolved style to use for sharing. + pub fn share_primary_style(&self) -> PrimaryStyle { + let reused_via_rule_node = self + .flags + .contains(ElementDataFlags::PRIMARY_STYLE_REUSED_VIA_RULE_NODE); + + PrimaryStyle { + style: ResolvedStyle(self.styles.primary().clone()), + reused_via_rule_node, + } + } + + /// Sets a new set of styles, returning the old ones. + pub fn set_styles(&mut self, new_styles: ResolvedElementStyles) -> ElementStyles { + if new_styles.primary.reused_via_rule_node { + self.flags + .insert(ElementDataFlags::PRIMARY_STYLE_REUSED_VIA_RULE_NODE); + } else { + self.flags + .remove(ElementDataFlags::PRIMARY_STYLE_REUSED_VIA_RULE_NODE); + } + mem::replace(&mut self.styles, new_styles.into()) + } + + /// Returns the kind of restyling that we're going to need to do on this + /// element, based of the stored restyle hint. + pub fn restyle_kind(&self, shared_context: &SharedStyleContext) -> Option { + if shared_context.traversal_flags.for_animation_only() { + return self.restyle_kind_for_animation(shared_context); + } + + let style = match self.styles.primary { + Some(ref s) => s, + None => return Some(RestyleKind::MatchAndCascade), + }; + + let hint = self.hint; + if hint.is_empty() { + return None; + } + + let needs_to_match_self = hint.intersects(RestyleHint::RESTYLE_SELF) || + (hint.intersects(RestyleHint::RESTYLE_SELF_IF_PSEUDO) && style.is_pseudo_style()); + if needs_to_match_self { + return Some(RestyleKind::MatchAndCascade); + } + + if hint.has_replacements() { + debug_assert!( + !hint.has_animation_hint(), + "Animation only restyle hint should have already processed" + ); + return Some(RestyleKind::CascadeWithReplacements( + hint & RestyleHint::replacements(), + )); + } + + let needs_to_recascade_self = hint.intersects(RestyleHint::RECASCADE_SELF) || + (hint.intersects(RestyleHint::RECASCADE_SELF_IF_INHERIT_RESET_STYLE) && + style + .flags + .contains(ComputedValueFlags::INHERITS_RESET_STYLE)); + if needs_to_recascade_self { + return Some(RestyleKind::CascadeOnly); + } + + None + } + + /// Returns the kind of restyling for animation-only restyle. + fn restyle_kind_for_animation( + &self, + shared_context: &SharedStyleContext, + ) -> Option { + debug_assert!(shared_context.traversal_flags.for_animation_only()); + debug_assert!( + self.has_styles(), + "animation traversal doesn't care about unstyled elements" + ); + + // FIXME: We should ideally restyle here, but it is a hack to work around our weird + // animation-only traversal stuff: If we're display: none and the rules we could + // match could change, we consider our style up-to-date. This is because re-cascading with + // and old style doesn't guarantee returning the correct animation style (that's + // bug 1393323). So if our display changed, and it changed from display: none, we would + // incorrectly forget about it and wouldn't be able to correctly style our descendants + // later. + // XXX Figure out if this still makes sense. + let hint = self.hint; + if self.styles.is_display_none() && hint.intersects(RestyleHint::RESTYLE_SELF) { + return None; + } + + let style = self.styles.primary(); + // Return either CascadeWithReplacements or CascadeOnly in case of + // animation-only restyle. I.e. animation-only restyle never does + // selector matching. + if hint.has_animation_hint() { + return Some(RestyleKind::CascadeWithReplacements( + hint & RestyleHint::for_animations(), + )); + } + + let needs_to_recascade_self = hint.intersects(RestyleHint::RECASCADE_SELF) || + (hint.intersects(RestyleHint::RECASCADE_SELF_IF_INHERIT_RESET_STYLE) && + style + .flags + .contains(ComputedValueFlags::INHERITS_RESET_STYLE)); + if needs_to_recascade_self { + return Some(RestyleKind::CascadeOnly); + } + return None; + } + + /// Drops any restyle state from the element. + /// + /// FIXME(bholley): The only caller of this should probably just assert that + /// the hint is empty and call clear_flags_and_damage(). + #[inline] + pub fn clear_restyle_state(&mut self) { + self.hint = RestyleHint::empty(); + self.clear_restyle_flags_and_damage(); + } + + /// Drops restyle flags and damage from the element. + #[inline] + pub fn clear_restyle_flags_and_damage(&mut self) { + self.damage = RestyleDamage::empty(); + self.flags.remove(ElementDataFlags::WAS_RESTYLED); + } + + /// Mark this element as restyled, which is useful to know whether we need + /// to do a post-traversal. + pub fn set_restyled(&mut self) { + self.flags.insert(ElementDataFlags::WAS_RESTYLED); + self.flags + .remove(ElementDataFlags::TRAVERSED_WITHOUT_STYLING); + } + + /// Returns true if this element was restyled. + #[inline] + pub fn is_restyle(&self) -> bool { + self.flags.contains(ElementDataFlags::WAS_RESTYLED) + } + + /// Mark that we traversed this element without computing any style for it. + pub fn set_traversed_without_styling(&mut self) { + self.flags + .insert(ElementDataFlags::TRAVERSED_WITHOUT_STYLING); + } + + /// Returns whether this element has been part of a restyle. + #[inline] + pub fn contains_restyle_data(&self) -> bool { + self.is_restyle() || !self.hint.is_empty() || !self.damage.is_empty() + } + + /// Returns whether it is safe to perform cousin sharing based on the ComputedValues + /// identity of the primary style in this ElementData. There are a few subtle things + /// to check. + /// + /// First, if a parent element was already styled and we traversed past it without + /// restyling it, that may be because our clever invalidation logic was able to prove + /// that the styles of that element would remain unchanged despite changes to the id + /// or class attributes. However, style sharing relies on the strong guarantee that all + /// the classes and ids up the respective parent chains are identical. As such, if we + /// skipped styling for one (or both) of the parents on this traversal, we can't share + /// styles across cousins. Note that this is a somewhat conservative check. We could + /// tighten it by having the invalidation logic explicitly flag elements for which it + /// ellided styling. + /// + /// Second, we want to only consider elements whose ComputedValues match due to a hit + /// in the style sharing cache, rather than due to the rule-node-based reuse that + /// happens later in the styling pipeline. The former gives us the stronger guarantees + /// we need for style sharing, the latter does not. + pub fn safe_for_cousin_sharing(&self) -> bool { + if self.flags.intersects( + ElementDataFlags::TRAVERSED_WITHOUT_STYLING | + ElementDataFlags::PRIMARY_STYLE_REUSED_VIA_RULE_NODE, + ) { + return false; + } + if !self + .styles + .primary() + .get_box() + .clone_container_type() + .is_normal() + { + return false; + } + true + } + + /// Measures memory usage. + #[cfg(feature = "gecko")] + pub fn size_of_excluding_cvs(&self, ops: &mut MallocSizeOfOps) -> usize { + let n = self.styles.size_of_excluding_cvs(ops); + + // We may measure more fields in the future if DMD says it's worth it. + + n + } +} diff --git a/servo/components/style/dom.rs b/servo/components/style/dom.rs new file mode 100644 index 0000000000..554d79fdb3 --- /dev/null +++ b/servo/components/style/dom.rs @@ -0,0 +1,951 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Types and traits used to access the DOM from style calculation. + +#![allow(unsafe_code)] +#![deny(missing_docs)] + +use crate::applicable_declarations::ApplicableDeclarationBlock; +use crate::context::SharedStyleContext; +#[cfg(feature = "gecko")] +use crate::context::{PostAnimationTasks, UpdateAnimationsTasks}; +use crate::data::ElementData; +use crate::media_queries::Device; +use crate::properties::{AnimationDeclarations, ComputedValues, PropertyDeclarationBlock}; +use crate::selector_parser::{AttrValue, CustomState, Lang, PseudoElement, SelectorImpl}; +use crate::shared_lock::{Locked, SharedRwLock}; +use crate::stylist::CascadeData; +use crate::values::computed::Display; +use crate::values::AtomIdent; +use crate::WeakAtom; +use atomic_refcell::{AtomicRef, AtomicRefMut}; +use dom::ElementState; +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(self.scope); + self.previous + } +} + +/// The `TDocument` trait, to represent a document node. +pub trait TDocument: Sized + Copy + Clone { + /// The concrete `TNode` type. + type ConcreteNode: TNode; + + /// Get this document as a `TNode`. + fn as_node(&self) -> Self::ConcreteNode; + + /// Returns whether this document is an HTML document. + fn is_html_document(&self) -> bool; + + /// Returns the quirks mode of this document. + fn quirks_mode(&self) -> QuirksMode; + + /// Get a list of elements with a given ID in this document, sorted by + /// tree position. + /// + /// Can return an error to signal that this list is not available, or also + /// return an empty slice. + fn elements_with_id<'a>( + &self, + _id: &AtomIdent, + ) -> Result<&'a [::ConcreteElement], ()> + where + Self: 'a, + { + Err(()) + } + + /// This document's shared lock. + fn shared_lock(&self) -> &SharedRwLock; +} + +/// The `TNode` trait. This is the main generic trait over which the style +/// system can be implemented. +pub trait TNode: Sized + Copy + Clone + Debug + NodeInfo + PartialEq { + /// The concrete `TElement` type. + type ConcreteElement: TElement; + + /// The concrete `TDocument` type. + type ConcreteDocument: TDocument; + + /// The concrete `TShadowRoot` type. + type ConcreteShadowRoot: TShadowRoot; + + /// Get this node's parent node. + fn parent_node(&self) -> Option; + + /// Get this node's first child. + fn first_child(&self) -> Option; + + /// Get this node's last child. + fn last_child(&self) -> Option; + + /// Get this node's previous sibling. + fn prev_sibling(&self) -> Option; + + /// Get this node's next sibling. + fn next_sibling(&self) -> Option; + + /// Get the owner document of this node. + fn owner_doc(&self) -> Self::ConcreteDocument; + + /// Iterate over the DOM children of a node. + #[inline(always)] + fn dom_children(&self) -> DomChildren { + DomChildren(self.first_child()) + } + + /// Returns whether the node is attached to a document. + fn is_in_document(&self) -> bool; + + /// Iterate over the DOM children of a node, in preorder. + #[inline(always)] + fn dom_descendants(&self) -> DomDescendants { + DomDescendants { + previous: Some(*self), + scope: *self, + } + } + + /// Returns the next node after this one, in a pre-order tree-traversal of + /// the subtree rooted at scoped_to. + #[inline] + fn next_in_preorder(&self, scoped_to: Self) -> Option { + if let Some(c) = self.first_child() { + return Some(c); + } + + let mut current = *self; + loop { + if current == scoped_to { + return None; + } + + if let Some(s) = current.next_sibling() { + return Some(s); + } + + debug_assert!( + current.parent_node().is_some(), + "Not a descendant of the scope?" + ); + current = current.parent_node()?; + } + } + + /// Get this node's parent element from the perspective of a restyle + /// traversal. + fn traversal_parent(&self) -> Option; + + /// Get this node's parent element if present. + fn parent_element(&self) -> Option { + self.parent_node().and_then(|n| n.as_element()) + } + + /// Get this node's parent element, or shadow host if it's a shadow root. + fn parent_element_or_host(&self) -> Option { + let parent = self.parent_node()?; + if let Some(e) = parent.as_element() { + return Some(e); + } + if let Some(root) = parent.as_shadow_root() { + return Some(root.host()); + } + None + } + + /// Converts self into an `OpaqueNode`. + fn opaque(&self) -> OpaqueNode; + + /// A debug id, only useful, mm... for debugging. + fn debug_id(self) -> usize; + + /// Get this node as an element, if it's one. + fn as_element(&self) -> Option; + + /// Get this node as a document, if it's one. + fn as_document(&self) -> Option; + + /// Get this node as a ShadowRoot, if it's one. + fn as_shadow_root(&self) -> Option; +} + +/// Wrapper to output the subtree rather than the single node when formatting +/// for Debug. +pub struct ShowSubtree(pub N); +impl Debug for ShowSubtree { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!(f, "DOM Subtree:")?; + fmt_subtree(f, &|f, n| write!(f, "{:?}", n), self.0, 1) + } +} + +/// Wrapper to output the subtree along with the ElementData when formatting +/// for Debug. +pub struct ShowSubtreeData(pub N); +impl Debug for ShowSubtreeData { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!(f, "DOM Subtree:")?; + fmt_subtree(f, &|f, n| fmt_with_data(f, n), self.0, 1) + } +} + +/// Wrapper to output the subtree along with the ElementData and primary +/// ComputedValues when formatting for Debug. This is extremely verbose. +#[cfg(feature = "servo")] +pub struct ShowSubtreeDataAndPrimaryValues(pub N); +#[cfg(feature = "servo")] +impl Debug for ShowSubtreeDataAndPrimaryValues { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!(f, "DOM Subtree:")?; + fmt_subtree(f, &|f, n| fmt_with_data_and_primary_values(f, n), self.0, 1) + } +} + +fn fmt_with_data(f: &mut fmt::Formatter, n: N) -> fmt::Result { + if let Some(el) = n.as_element() { + write!( + f, + "{:?} dd={} aodd={} data={:?}", + el, + el.has_dirty_descendants(), + el.has_animation_only_dirty_descendants(), + el.borrow_data(), + ) + } else { + write!(f, "{:?}", n) + } +} + +#[cfg(feature = "servo")] +fn fmt_with_data_and_primary_values(f: &mut fmt::Formatter, n: N) -> fmt::Result { + if let Some(el) = n.as_element() { + let dd = el.has_dirty_descendants(); + let aodd = el.has_animation_only_dirty_descendants(); + let data = el.borrow_data(); + let values = data.as_ref().and_then(|d| d.styles.get_primary()); + write!( + f, + "{:?} dd={} aodd={} data={:?} values={:?}", + el, dd, aodd, &data, values + ) + } else { + write!(f, "{:?}", n) + } +} + +fn fmt_subtree(f: &mut fmt::Formatter, stringify: &F, n: N, indent: u32) -> fmt::Result +where + F: Fn(&mut fmt::Formatter, N) -> fmt::Result, +{ + for _ in 0..indent { + write!(f, " ")?; + } + stringify(f, n)?; + if let Some(e) = n.as_element() { + for kid in e.traversal_children() { + writeln!(f, "")?; + fmt_subtree(f, stringify, kid, indent + 1)?; + } + } + + Ok(()) +} + +/// The ShadowRoot trait. +pub trait TShadowRoot: Sized + Copy + Clone + Debug + PartialEq { + /// The concrete node type. + type ConcreteNode: TNode; + + /// Get this ShadowRoot as a node. + fn as_node(&self) -> Self::ConcreteNode; + + /// Get the shadow host that hosts this ShadowRoot. + fn host(&self) -> ::ConcreteElement; + + /// Get the style data for this ShadowRoot. + fn style_data<'a>(&self) -> Option<&'a CascadeData> + where + Self: 'a; + + /// Get the list of shadow parts for this shadow root. + fn parts<'a>(&self) -> &[::ConcreteElement] + where + Self: 'a, + { + &[] + } + + /// Get a list of elements with a given ID in this shadow root, sorted by + /// tree position. + /// + /// Can return an error to signal that this list is not available, or also + /// return an empty slice. + fn elements_with_id<'a>( + &self, + _id: &AtomIdent, + ) -> Result<&'a [::ConcreteElement], ()> + where + Self: 'a, + { + Err(()) + } +} + +/// The element trait, the main abstraction the style crate acts over. +pub trait TElement: + Eq + PartialEq + Debug + Hash + Sized + Copy + Clone + SelectorsElement +{ + /// The concrete node type. + type ConcreteNode: TNode; + + /// A concrete children iterator type in order to iterate over the `Node`s. + /// + /// TODO(emilio): We should eventually replace this with the `impl Trait` + /// syntax. + type TraversalChildrenIterator: Iterator; + + /// Get this element as a node. + fn as_node(&self) -> Self::ConcreteNode; + + /// A debug-only check that the device's owner doc matches the actual doc + /// we're the root of. + /// + /// Otherwise we may set document-level state incorrectly, like the root + /// font-size used for rem units. + fn owner_doc_matches_for_testing(&self, _: &Device) -> bool { + true + } + + /// Whether this element should match user and content rules. + /// + /// We use this for Native Anonymous Content in Gecko. + fn matches_user_and_content_rules(&self) -> bool { + true + } + + /// Returns the depth of this element in the DOM. + fn depth(&self) -> usize { + let mut depth = 0; + let mut curr = *self; + while let Some(parent) = curr.traversal_parent() { + depth += 1; + curr = parent; + } + + depth + } + + /// Get this node's parent element from the perspective of a restyle + /// traversal. + fn traversal_parent(&self) -> Option { + self.as_node().traversal_parent() + } + + /// Get this node's children from the perspective of a restyle traversal. + fn traversal_children(&self) -> LayoutIterator; + + /// Returns the parent element we should inherit from. + /// + /// This is pretty much always the parent element itself, except in the case + /// of Gecko's Native Anonymous Content, which uses the traversal parent + /// (i.e. the flattened tree parent) and which also may need to find the + /// closest non-NAC ancestor. + fn inheritance_parent(&self) -> Option { + self.parent_element() + } + + /// The ::before pseudo-element of this element, if it exists. + fn before_pseudo_element(&self) -> Option { + None + } + + /// The ::after pseudo-element of this element, if it exists. + fn after_pseudo_element(&self) -> Option { + None + } + + /// The ::marker pseudo-element of this element, if it exists. + fn marker_pseudo_element(&self) -> Option { + None + } + + /// Execute `f` for each anonymous content child (apart from ::before and + /// ::after) whose originating element is `self`. + fn each_anonymous_content_child(&self, _f: F) + where + F: FnMut(Self), + { + } + + /// Return whether this element is an element in the HTML namespace. + fn is_html_element(&self) -> bool; + + /// Return whether this element is an element in the MathML namespace. + fn is_mathml_element(&self) -> bool; + + /// Return whether this element is an element in the SVG namespace. + fn is_svg_element(&self) -> bool; + + /// Return whether this element is an element in the XUL namespace. + fn is_xul_element(&self) -> bool { + false + } + + /// Return the list of slotted nodes of this node. + fn slotted_nodes(&self) -> &[Self::ConcreteNode] { + &[] + } + + /// Get this element's style attribute. + fn style_attribute(&self) -> Option>>; + + /// Unset the style attribute's dirty bit. + /// Servo doesn't need to manage ditry bit for style attribute. + fn unset_dirty_style_attribute(&self) {} + + /// Get this element's SMIL override declarations. + fn smil_override(&self) -> Option>> { + None + } + + /// Get the combined animation and transition rules. + /// + /// FIXME(emilio): Is this really useful? + fn animation_declarations(&self, context: &SharedStyleContext) -> AnimationDeclarations { + if !self.may_have_animations() { + return Default::default(); + } + + AnimationDeclarations { + animations: self.animation_rule(context), + transitions: self.transition_rule(context), + } + } + + /// Get this element's animation rule. + fn animation_rule( + &self, + _: &SharedStyleContext, + ) -> Option>>; + + /// Get this element's transition rule. + fn transition_rule( + &self, + context: &SharedStyleContext, + ) -> Option>>; + + /// Get this element's state, for non-tree-structural pseudos. + fn state(&self) -> ElementState; + + /// Returns whether this element's CustomStateSet contains a given state. + fn has_custom_state(&self, _state: &CustomState) -> bool { + false + } + + /// Returns whether this element has a `part` attribute. + fn has_part_attr(&self) -> bool; + + /// Returns whether this element exports any part from its shadow tree. + fn exports_any_part(&self) -> bool; + + /// The ID for this element. + fn id(&self) -> Option<&WeakAtom>; + + /// Internal iterator for the classes of this element. + fn each_class(&self, callback: F) + where + F: FnMut(&AtomIdent); + + /// Internal iterator for the part names of this element. + fn each_part(&self, _callback: F) + where + F: FnMut(&AtomIdent), + { + } + + /// Internal iterator for the attribute names of this element. + fn each_attr_name(&self, callback: F) + where + F: FnMut(&AtomIdent); + + /// Internal iterator for the part names that this element exports for a + /// given part name. + fn each_exported_part(&self, _name: &AtomIdent, _callback: F) + where + F: FnMut(&AtomIdent), + { + } + + /// Whether a given element may generate a pseudo-element. + /// + /// This is useful to avoid computing, for example, pseudo styles for + /// `::-first-line` or `::-first-letter`, when we know it won't affect us. + /// + /// TODO(emilio, bz): actually implement the logic for it. + fn may_generate_pseudo(&self, pseudo: &PseudoElement, _primary_style: &ComputedValues) -> bool { + // ::before/::after are always supported for now, though we could try to + // optimize out leaf elements. + + // ::first-letter and ::first-line are only supported for block-inside + // things, and only in Gecko, not Servo. Unfortunately, Gecko has + // block-inside things that might have any computed display value due to + // things like fieldsets, legends, etc. Need to figure out how this + // should work. + debug_assert!( + pseudo.is_eager(), + "Someone called may_generate_pseudo with a non-eager pseudo." + ); + true + } + + /// Returns true if this element may have a descendant needing style processing. + /// + /// Note that we cannot guarantee the existence of such an element, because + /// it may have been removed from the DOM between marking it for restyle and + /// the actual restyle traversal. + fn has_dirty_descendants(&self) -> bool; + + /// Returns whether state or attributes that may change style have changed + /// on the element, and thus whether the element has been snapshotted to do + /// restyle hint computation. + fn has_snapshot(&self) -> bool; + + /// Returns whether the current snapshot if present has been handled. + fn handled_snapshot(&self) -> bool; + + /// Flags this element as having handled already its snapshot. + unsafe fn set_handled_snapshot(&self); + + /// Returns whether the element's styles are up-to-date after traversal + /// (i.e. in post traversal). + fn has_current_styles(&self, data: &ElementData) -> bool { + if self.has_snapshot() && !self.handled_snapshot() { + return false; + } + + data.has_styles() && + // TODO(hiro): When an animating element moved into subtree of + // contenteditable element, there remains animation restyle hints in + // post traversal. It's generally harmless since the hints will be + // processed in a next styling but ideally it should be processed soon. + // + // Without this, we get failures in: + // layout/style/crashtests/1383319.html + // layout/style/crashtests/1383001.html + // + // https://bugzilla.mozilla.org/show_bug.cgi?id=1389675 tracks fixing + // this. + !data.hint.has_non_animation_invalidations() + } + + /// Flag that this element has a descendant for style processing. + /// + /// Only safe to call with exclusive access to the element. + unsafe fn set_dirty_descendants(&self); + + /// Flag that this element has no descendant for style processing. + /// + /// Only safe to call with exclusive access to the element. + unsafe fn unset_dirty_descendants(&self); + + /// Similar to the dirty_descendants but for representing a descendant of + /// the element needs to be updated in animation-only traversal. + fn has_animation_only_dirty_descendants(&self) -> bool { + false + } + + /// Flag that this element has a descendant for animation-only restyle + /// processing. + /// + /// Only safe to call with exclusive access to the element. + unsafe fn set_animation_only_dirty_descendants(&self) {} + + /// Flag that this element has no descendant for animation-only restyle processing. + /// + /// Only safe to call with exclusive access to the element. + unsafe fn unset_animation_only_dirty_descendants(&self) {} + + /// Clear all bits related describing the dirtiness of descendants. + /// + /// In Gecko, this corresponds to the regular dirty descendants bit, the + /// animation-only dirty descendants bit, and the lazy frame construction + /// descendants bit. + unsafe fn clear_descendant_bits(&self) { + self.unset_dirty_descendants(); + } + + /// Returns true if this element is a visited link. + /// + /// Servo doesn't support visited styles yet. + fn is_visited_link(&self) -> bool { + false + } + + /// Returns the pseudo-element implemented by this element, if any. + /// + /// Gecko traverses pseudo-elements during the style traversal, and we need + /// to know this so we can properly grab the pseudo-element style from the + /// parent element. + /// + /// Note that we still need to compute the pseudo-elements before-hand, + /// given otherwise we don't know if we need to create an element or not. + /// + /// Servo doesn't have to deal with this. + fn implemented_pseudo_element(&self) -> Option { + None + } + + /// Atomically stores the number of children of this node that we will + /// need to process during bottom-up traversal. + fn store_children_to_process(&self, n: isize); + + /// Atomically notes that a child has been processed during bottom-up + /// traversal. Returns the number of children left to process. + fn did_process_child(&self) -> isize; + + /// Gets a reference to the ElementData container, or creates one. + /// + /// Unsafe because it can race to allocate and leak if not used with + /// exclusive access to the element. + unsafe fn ensure_data(&self) -> AtomicRefMut; + + /// Clears the element data reference, if any. + /// + /// Unsafe following the same reasoning as ensure_data. + unsafe fn clear_data(&self); + + /// Whether there is an ElementData container. + fn has_data(&self) -> bool; + + /// Immutably borrows the ElementData. + fn borrow_data(&self) -> Option>; + + /// Mutably borrows the ElementData. + fn mutate_data(&self) -> Option>; + + /// Whether we should skip any root- or item-based display property + /// blockification on this element. (This function exists so that Gecko + /// native anonymous content can opt out of this style fixup.) + fn skip_item_display_fixup(&self) -> bool; + + /// In Gecko, element has a flag that represents the element may have + /// any type of animations or not to bail out animation stuff early. + /// Whereas Servo doesn't have such flag. + fn may_have_animations(&self) -> bool; + + /// Creates a task to update various animation state on a given (pseudo-)element. + #[cfg(feature = "gecko")] + fn update_animations( + &self, + before_change_style: Option>, + tasks: UpdateAnimationsTasks, + ); + + /// Creates a task to process post animation on a given element. + #[cfg(feature = "gecko")] + fn process_post_animation(&self, tasks: PostAnimationTasks); + + /// Returns true if the element has relevant animations. Relevant + /// animations are those animations that are affecting the element's style + /// or are scheduled to do so in the future. + fn has_animations(&self, context: &SharedStyleContext) -> bool; + + /// Returns true if the element has a CSS animation. The `context` and `pseudo_element` + /// arguments are only used by Servo, since it stores animations globally and pseudo-elements + /// are not in the DOM. + fn has_css_animations( + &self, + context: &SharedStyleContext, + pseudo_element: Option, + ) -> bool; + + /// Returns true if the element has a CSS transition (including running transitions and + /// completed transitions). The `context` and `pseudo_element` arguments are only used + /// by Servo, since it stores animations globally and pseudo-elements are not in the DOM. + fn has_css_transitions( + &self, + context: &SharedStyleContext, + pseudo_element: Option, + ) -> bool; + + /// Returns true if the element has animation restyle hints. + fn has_animation_restyle_hints(&self) -> bool { + let data = match self.borrow_data() { + Some(d) => d, + None => return false, + }; + return data.hint.has_animation_hint(); + } + + /// The shadow root this element is a host of. + fn shadow_root(&self) -> Option<::ConcreteShadowRoot>; + + /// The shadow root which roots the subtree this element is contained in. + fn containing_shadow(&self) -> Option<::ConcreteShadowRoot>; + + /// Return the element which we can use to look up rules in the selector + /// maps. + /// + /// This is always the element itself, except in the case where we are an + /// element-backed pseudo-element, in which case we return the originating + /// element. + fn rule_hash_target(&self) -> Self { + if self.is_pseudo_element() { + self.pseudo_element_originating_element() + .expect("Trying to collect rules for a detached pseudo-element") + } else { + *self + } + } + + /// Executes the callback for each applicable style rule data which isn't + /// the main document's data (which stores UA / author rules). + /// + /// The element passed to the callback is the containing shadow host for the + /// data if it comes from Shadow DOM. + /// + /// Returns whether normal document author rules should apply. + /// + /// TODO(emilio): We could separate the invalidation data for elements + /// matching in other scopes to avoid over-invalidation. + fn each_applicable_non_document_style_rule_data<'a, F>(&self, mut f: F) -> bool + where + Self: 'a, + F: FnMut(&'a CascadeData, Self), + { + use crate::rule_collector::containing_shadow_ignoring_svg_use; + + let target = self.rule_hash_target(); + let matches_user_and_content_rules = target.matches_user_and_content_rules(); + let mut doc_rules_apply = matches_user_and_content_rules; + + // Use the same rules to look for the containing host as we do for rule + // collection. + if let Some(shadow) = containing_shadow_ignoring_svg_use(target) { + doc_rules_apply = false; + if let Some(data) = shadow.style_data() { + f(data, shadow.host()); + } + } + + if let Some(shadow) = target.shadow_root() { + if let Some(data) = shadow.style_data() { + f(data, shadow.host()); + } + } + + let mut current = target.assigned_slot(); + while let Some(slot) = current { + // Slots can only have assigned nodes when in a shadow tree. + let shadow = slot.containing_shadow().unwrap(); + if let Some(data) = shadow.style_data() { + if data.any_slotted_rule() { + f(data, shadow.host()); + } + } + current = slot.assigned_slot(); + } + + if target.has_part_attr() { + if let Some(mut inner_shadow) = target.containing_shadow() { + loop { + let inner_shadow_host = inner_shadow.host(); + match inner_shadow_host.containing_shadow() { + Some(shadow) => { + if let Some(data) = shadow.style_data() { + if data.any_part_rule() { + f(data, shadow.host()) + } + } + // TODO: Could be more granular. + if !inner_shadow_host.exports_any_part() { + break; + } + inner_shadow = shadow; + }, + None => { + // TODO(emilio): Should probably distinguish with + // MatchesDocumentRules::{No,Yes,IfPart} or something so that we could + // skip some work. + doc_rules_apply = matches_user_and_content_rules; + break; + }, + } + } + } + } + + doc_rules_apply + } + + /// Returns true if one of the transitions needs to be updated on this element. We check all + /// the transition properties to make sure that updating transitions is necessary. + /// This method should only be called if might_needs_transitions_update returns true when + /// passed the same parameters. + #[cfg(feature = "gecko")] + fn needs_transitions_update( + &self, + before_change_style: &ComputedValues, + after_change_style: &ComputedValues, + ) -> bool; + + /// Returns the value of the `xml:lang=""` attribute (or, if appropriate, + /// the `lang=""` attribute) on this element. + fn lang_attr(&self) -> Option; + + /// Returns whether this element's language matches the language tag + /// `value`. If `override_lang` is not `None`, it specifies the value + /// of the `xml:lang=""` or `lang=""` attribute to use in place of + /// looking at the element and its ancestors. (This argument is used + /// to implement matching of `:lang()` against snapshots.) + fn match_element_lang(&self, override_lang: Option>, value: &Lang) -> bool; + + /// Returns whether this element is the main body element of the HTML + /// document it is on. + fn is_html_document_body_element(&self) -> bool; + + /// Generate the proper applicable declarations due to presentational hints, + /// and insert them into `hints`. + fn synthesize_presentational_hints_for_legacy_attributes( + &self, + visited_handling: VisitedHandlingMode, + hints: &mut V, + ) where + V: Push; + + /// Returns element's local name. + fn local_name(&self) -> &::BorrowedLocalName; + + /// Returns element's namespace. + fn namespace(&self) + -> &::BorrowedNamespaceUrl; + + /// Returns the size of the element to be used in container size queries. + /// This will usually be the size of the content area of the primary box, + /// but can be None if there is no box or if some axis lacks size containment. + fn query_container_size( + &self, + display: &Display, + ) -> euclid::default::Size2D>; + + /// Returns true if the element has all of specified selector flags. + fn has_selector_flags(&self, flags: ElementSelectorFlags) -> bool; + + /// Returns the search direction for relative selector invalidation, if it is on the search path. + fn relative_selector_search_direction(&self) -> Option; +} + +/// 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..cdc106e1ad --- /dev/null +++ b/servo/components/style/dom_apis.rs @@ -0,0 +1,814 @@ +/* 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, SiblingTraversalMap, +}; +use crate::invalidation::element::invalidator::{InvalidationProcessor, InvalidationVector}; +use crate::selector_parser::SelectorImpl; +use crate::values::AtomIdent; +use selectors::attr::CaseSensitivity; +use selectors::attr::{AttrSelectorOperation, NamespaceConstraint}; +use selectors::matching::{ + self, MatchingContext, MatchingForInvalidation, MatchingMode, NeedsSelectorFlags, + SelectorCaches, +}; +use selectors::parser::{Combinator, Component, LocalName}; +use selectors::{Element, SelectorList}; +use smallvec::SmallVec; + +/// +pub fn element_matches( + element: &E, + selector_list: &SelectorList, + quirks_mode: QuirksMode, +) -> bool +where + E: Element, +{ + let mut selector_caches = SelectorCaches::default(); + + let mut context = MatchingContext::new( + MatchingMode::Normal, + None, + &mut selector_caches, + quirks_mode, + NeedsSelectorFlags::No, + MatchingForInvalidation::No, + ); + context.scope_element = Some(element.opaque()); + context.current_host = element.containing_shadow_host().map(|e| e.opaque()); + matching::matches_selector_list(selector_list, element, &mut context) +} + +/// +pub fn element_closest( + element: E, + selector_list: &SelectorList, + quirks_mode: QuirksMode, +) -> Option +where + E: Element, +{ + let mut selector_caches = SelectorCaches::default(); + + let mut context = MatchingContext::new( + MatchingMode::Normal, + None, + &mut selector_caches, + quirks_mode, + NeedsSelectorFlags::No, + MatchingForInvalidation::No, + ); + context.scope_element = Some(element.opaque()); + context.current_host = element.containing_shadow_host().map(|e| e.opaque()); + + let mut current = Some(element); + while let Some(element) = current.take() { + if matching::matches_selector_list(selector_list, &element, &mut context) { + return Some(element); + } + current = element.parent_element(); + } + + return None; +} + +/// A selector query abstraction, in order to be generic over QuerySelector and +/// QuerySelectorAll. +pub trait SelectorQuery { + /// The output of the query. + type Output; + + /// Whether the query should stop after the first element has been matched. + fn should_stop_after_first_match() -> bool; + + /// Append an element matching after the first query. + fn append_element(output: &mut Self::Output, element: E); + + /// Returns true if the output is empty. + fn is_empty(output: &Self::Output) -> bool; +} + +/// The result of a querySelectorAll call. +pub type QuerySelectorAllResult = SmallVec<[E; 128]>; + +/// A query for all the elements in a subtree. +pub struct QueryAll; + +impl SelectorQuery for QueryAll { + type Output = QuerySelectorAllResult; + + fn should_stop_after_first_match() -> bool { + false + } + + fn append_element(output: &mut Self::Output, element: E) { + output.push(element); + } + + fn is_empty(output: &Self::Output) -> bool { + output.is_empty() + } +} + +/// A query for the first in-tree match of all the elements in a subtree. +pub struct QueryFirst; + +impl SelectorQuery for QueryFirst { + type Output = Option; + + fn should_stop_after_first_match() -> bool { + true + } + + fn append_element(output: &mut Self::Output, element: E) { + if output.is_none() { + *output = Some(element) + } + } + + fn is_empty(output: &Self::Output) -> bool { + output.is_none() + } +} + +struct QuerySelectorProcessor<'a, 'b, E, Q> +where + E: TElement + 'a, + Q: SelectorQuery, + Q::Output: 'a, +{ + results: &'a mut Q::Output, + matching_context: MatchingContext<'b, E::Impl>, + traversal_map: SiblingTraversalMap, + dependencies: &'a [Dependency], +} + +impl<'a, 'b, E, Q> InvalidationProcessor<'a, 'b, E> for QuerySelectorProcessor<'a, 'b, 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<'b, E::Impl> { + &mut self.matching_context + } + + fn sibling_traversal_map(&self) -> &SiblingTraversalMap { + &self.traversal_map + } + + fn should_process_descendants(&mut self, _: E) -> bool { + if Q::should_stop_after_first_match() { + return Q::is_empty(&self.results); + } + + true + } + + fn invalidated_self(&mut self, e: E) { + Q::append_element(self.results, e); + } + + fn invalidated_sibling(&mut self, e: E, _of: E) { + Q::append_element(self.results, e); + } + + fn recursion_limit_exceeded(&mut self, _e: E) {} + fn invalidated_descendants(&mut self, _e: E, _child: E) {} +} + +fn collect_all_elements(root: E::ConcreteNode, results: &mut Q::Output, mut filter: F) +where + E: TElement, + Q: SelectorQuery, + F: FnMut(E) -> bool, +{ + for node in root.dom_descendants() { + let element = match node.as_element() { + Some(e) => e, + None => continue, + }; + + if !filter(element) { + continue; + } + + Q::append_element(results, element); + if Q::should_stop_after_first_match() { + return; + } + } +} + +/// Returns whether a given element connected to `root` is descendant of `root`. +/// +/// NOTE(emilio): if root == element, this returns false. +fn connected_element_is_descendant_of(element: E, root: E::ConcreteNode) -> bool +where + E: TElement, +{ + // Optimize for when the root is a document or a shadow root and the element + // is connected to that root. + if root.as_document().is_some() { + debug_assert!(element.as_node().is_in_document(), "Not connected?"); + debug_assert_eq!( + root, + root.owner_doc().as_node(), + "Where did this element come from?", + ); + return true; + } + + if root.as_shadow_root().is_some() { + debug_assert_eq!( + element.containing_shadow().unwrap().as_node(), + root, + "Not connected?" + ); + return true; + } + + let mut current = element.as_node().parent_node(); + while let Some(n) = current.take() { + if n == root { + return true; + } + + current = n.parent_node(); + } + false +} + +/// Fast path for iterating over every element with a given id in the document +/// or shadow root that `root` is connected to. +fn fast_connected_elements_with_id<'a, N>( + root: N, + id: &AtomIdent, + case_sensitivity: CaseSensitivity, +) -> Result<&'a [N::ConcreteElement], ()> +where + N: TNode + 'a, +{ + 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, + class_and_id_case_sensitivity: CaseSensitivity, + mut filter: F, +) where + E: TElement, + Q: SelectorQuery, + F: FnMut(E) -> bool, +{ + let elements = match fast_connected_elements_with_id(root, id, class_and_id_case_sensitivity) { + Ok(elements) => elements, + Err(()) => { + collect_all_elements::(root, results, |e| { + e.has_id(id, class_and_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; + } + } +} + +fn has_attr(element: E, local_name: &AtomIdent) -> bool +where + E: TElement, +{ + let mut found = false; + element.each_attr_name(|name| found |= name == local_name); + found +} + +#[inline(always)] +fn local_name_matches(element: E, local_name: &LocalName) -> bool +where + E: TElement, +{ + let LocalName { + ref name, + ref lower_name, + } = *local_name; + + let chosen_name = if name == lower_name || element.is_html_element_in_html_document() { + lower_name + } else { + name + }; + + element.local_name() == &**chosen_name +} + +fn get_attr_name(component: &Component) -> Option<&AtomIdent> { + let (name, name_lower) = match component { + Component::AttributeInNoNamespace { ref local_name, .. } => return Some(local_name), + Component::AttributeInNoNamespaceExists { + ref local_name, + ref local_name_lower, + .. + } => (local_name, local_name_lower), + Component::AttributeOther(ref attr) => (&attr.local_name, &attr.local_name_lower), + _ => return None, + }; + if name != name_lower { + return None; // TODO: Maybe optimize this? + } + Some(name) +} + +fn get_id(component: &Component) -> Option<&AtomIdent> { + use selectors::attr::AttrSelectorOperator; + Some(match component { + Component::ID(ref id) => id, + Component::AttributeInNoNamespace { + ref operator, + ref local_name, + ref value, + .. + } => { + if *local_name != local_name!("id") { + return None; + } + if *operator != AttrSelectorOperator::Equal { + return None; + } + AtomIdent::cast(&value.0) + }, + _ => return None, + }) +} + +/// Fast paths for querySelector with a single simple selector. +fn query_selector_single_query( + root: E::ConcreteNode, + component: &Component, + results: &mut Q::Output, + class_and_id_case_sensitivity: CaseSensitivity, +) -> Result<(), ()> +where + E: TElement, + Q: SelectorQuery, +{ + match *component { + Component::ExplicitUniversalType => { + collect_all_elements::(root, results, |_| true) + }, + Component::Class(ref class) => collect_all_elements::(root, results, |element| { + element.has_class(class, class_and_id_case_sensitivity) + }), + Component::LocalName(ref local_name) => { + collect_all_elements::(root, results, |element| { + local_name_matches(element, local_name) + }) + }, + Component::AttributeInNoNamespaceExists { + ref local_name, + ref local_name_lower, + } => collect_all_elements::(root, results, |element| { + element.has_attr_in_no_namespace(matching::select_name( + &element, + local_name, + local_name_lower, + )) + }), + Component::AttributeInNoNamespace { + ref local_name, + ref value, + operator, + case_sensitivity, + } => { + let empty_namespace = selectors::parser::namespace_empty_string::(); + let namespace_constraint = NamespaceConstraint::Specific(&empty_namespace); + collect_all_elements::(root, results, |element| { + element.attr_matches( + &namespace_constraint, + local_name, + &AttrSelectorOperation::WithValue { + operator, + case_sensitivity: matching::to_unconditional_case_sensitivity( + case_sensitivity, + &element, + ), + value, + }, + ) + }) + }, + ref other => { + let id = match get_id(other) { + Some(id) => id, + // TODO(emilio): More fast paths? + None => return Err(()), + }; + collect_elements_with_id::( + root, + id, + results, + class_and_id_case_sensitivity, + |_| true, + ); + }, + } + + Ok(()) +} + +enum SimpleFilter<'a> { + Class(&'a AtomIdent), + Attr(&'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.len() > 1 { + return Err(()); + } + + let selector = &selector_list.slice()[0]; + let class_and_id_case_sensitivity = matching_context.classes_and_ids_case_sensitivity(); + // Let's just care about the easy cases for now. + if selector.len() == 1 { + if query_selector_single_query::( + root, + selector.iter().next().unwrap(), + results, + class_and_id_case_sensitivity, + ) + .is_ok() + { + return Ok(()); + } + } + + let mut iter = selector.iter(); + let mut combinator: Option = None; + + // We want to optimize some cases where there's no id involved whatsoever, + // like `.foo .bar`, but we don't want to make `#foo .bar` slower because of + // that. + let mut simple_filter = None; + + 'selector_loop: loop { + debug_assert!(combinator.map_or(true, |c| !c.is_sibling())); + + 'component_loop: for component in &mut iter { + match *component { + Component::Class(ref class) => { + if combinator.is_none() { + simple_filter = Some(SimpleFilter::Class(class)); + } + }, + Component::LocalName(ref local_name) => { + if combinator.is_none() { + // Prefer to look at class rather than local-name if + // both are present. + if let Some(SimpleFilter::Class(..)) = simple_filter { + continue; + } + simple_filter = Some(SimpleFilter::LocalName(local_name)); + } + }, + ref other => { + if let Some(id) = get_id(other) { + if combinator.is_none() { + // In the rightmost compound, just find descendants of root that match + // the selector list with that id. + collect_elements_with_id::( + root, + id, + results, + class_and_id_case_sensitivity, + |e| { + matching::matches_selector_list( + selector_list, + &e, + matching_context, + ) + }, + ); + return Ok(()); + } + + let elements = fast_connected_elements_with_id( + root, + id, + class_and_id_case_sensitivity, + )?; + 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(()); + } + if combinator.is_none() && simple_filter.is_none() { + if let Some(attr_name) = get_attr_name(other) { + simple_filter = Some(SimpleFilter::Attr(attr_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) => { + collect_all_elements::(root, results, |element| { + element.has_class(class, class_and_id_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) + }); + }, + SimpleFilter::Attr(ref local_name) => { + collect_all_elements::(root, results, |element| { + has_attr(element, local_name) && + matching::matches_selector_list(selector_list, &element, matching_context) + }); + }, + } + + Ok(()) +} + +// Slow path for a given selector query. +fn query_selector_slow( + root: E::ConcreteNode, + selector_list: &SelectorList, + results: &mut Q::Output, + matching_context: &mut MatchingContext, +) where + E: TElement, + Q: SelectorQuery, +{ + collect_all_elements::(root, results, |element| { + matching::matches_selector_list(selector_list, &element, matching_context) + }); +} + +/// Whether the invalidation machinery should be used for this query. +#[derive(PartialEq)] +pub enum MayUseInvalidation { + /// We may use it if we deem it useful. + Yes, + /// Don't use it. + No, +} + +/// +pub fn query_selector( + root: E::ConcreteNode, + selector_list: &SelectorList, + results: &mut Q::Output, + may_use_invalidation: MayUseInvalidation, +) where + E: TElement, + Q: SelectorQuery, +{ + use crate::invalidation::element::invalidator::TreeStyleInvalidator; + + let mut selector_caches = SelectorCaches::default(); + let quirks_mode = root.owner_doc().quirks_mode(); + + let mut matching_context = MatchingContext::new( + MatchingMode::Normal, + None, + &mut selector_caches, + quirks_mode, + NeedsSelectorFlags::No, + MatchingForInvalidation::No, + ); + let root_element = root.as_element(); + matching_context.scope_element = root_element.map(|e| e.opaque()); + matching_context.current_host = match root_element { + Some(root) => root.containing_shadow_host().map(|host| host.opaque()), + None => root.as_shadow_root().map(|root| root.host().opaque()), + }; + + let fast_result = + query_selector_fast::(root, selector_list, results, &mut matching_context); + + if fast_result.is_ok() { + return; + } + + // Slow path: Use the invalidation machinery if we're a root, and tree + // traversal otherwise. + // + // See the comment in collect_invalidations to see why only if we're a root. + // + // The invalidation mechanism is only useful in presence of combinators. + // + // We could do that check properly here, though checking the length of the + // selectors is a good heuristic. + // + // A selector with a combinator needs to have a length of at least 3: A + // simple selector, a combinator, and another simple selector. + let invalidation_may_be_useful = may_use_invalidation == MayUseInvalidation::Yes && + selector_list.slice().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 + .slice() + .iter() + .map(|selector| Dependency::for_full_selector_invalidation(selector.clone())) + .collect::>(); + let mut processor = QuerySelectorProcessor:: { + results, + matching_context, + traversal_map: SiblingTraversalMap::default(), + 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..95447ce08e --- /dev/null +++ b/servo/components/style/driver.rs @@ -0,0 +1,164 @@ +/* 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::scoped_tls::ScopedTLS; +use crate::traversal::{DomTraversal, PerLevelTraversalData, PreTraverseToken}; +use rayon; +use std::collections::VecDeque; +use time; + +#[cfg(feature = "servo")] +fn should_report_statistics() -> bool { + false +} + +#[cfg(feature = "gecko")] +fn should_report_statistics() -> bool { + unsafe { crate::gecko_bindings::structs::ServoTraversalStatistics_sActive } +} + +#[cfg(feature = "servo")] +fn report_statistics(_stats: &PerThreadTraversalStatistics) { + unreachable!("Servo never report stats"); +} + +#[cfg(feature = "gecko")] +fn report_statistics(stats: &PerThreadTraversalStatistics) { + // This should only be called in the main thread, or it may be racy + // to update the statistics in a global variable. + debug_assert!(unsafe { crate::gecko_bindings::bindings::Gecko_IsMainThread() }); + let gecko_stats = + unsafe { &mut crate::gecko_bindings::structs::ServoTraversalStatistics_sSingleton }; + gecko_stats.mElementsTraversed += stats.elements_traversed; + gecko_stats.mElementsStyled += stats.elements_styled; + gecko_stats.mElementsMatched += stats.elements_matched; + gecko_stats.mStylesShared += stats.styles_shared; + gecko_stats.mStylesReused += stats.styles_reused; +} + +fn with_pool_in_place_scope<'scope, R>( + work_unit_max: usize, + pool: Option<&rayon::ThreadPool>, + closure: impl FnOnce(Option<&rayon::ScopeFifo<'scope>>) -> R, +) -> R { + if work_unit_max == 0 || pool.is_none() { + closure(None) + } else { + pool.unwrap() + .in_place_scope_fifo(|scope| closure(Some(scope))) + } +} + +/// See documentation of the pref for performance characteristics. +fn work_unit_max() -> usize { + static_prefs::pref!("layout.css.stylo-work-unit-size") as usize +} + +/// Do a DOM traversal for top-down and (optionally) bottom-up processing, generic over `D`. +/// +/// We use an adaptive traversal strategy. We start out with simple sequential processing, until we +/// arrive at a wide enough level in the DOM that the parallel traversal would parallelize it. +/// If a thread pool is provided, we then transfer control over to the parallel traversal. +/// +/// Returns true if the traversal was parallel, and also returns the statistics object containing +/// information on nodes traversed (on nightly only). Not all of its fields will be initialized +/// since we don't call finish(). +pub fn traverse_dom( + traversal: &D, + token: PreTraverseToken, + pool: Option<&rayon::ThreadPool>, +) -> E +where + E: TElement, + D: DomTraversal, +{ + let root = token + .traversal_root() + .expect("Should've ensured we needed to traverse"); + + let report_stats = should_report_statistics(); + let dump_stats = traversal.shared_context().options.dump_style_statistics; + let start_time = if dump_stats { + Some(time::precise_time_s()) + } else { + None + }; + + // Declare the main-thread context, as well as the worker-thread contexts, + // which we may or may not instantiate. It's important to declare the worker- + // thread contexts first, so that they get dropped second. This matters because: + // * ThreadLocalContexts borrow AtomicRefCells in TLS. + // * Dropping a ThreadLocalContext can run SequentialTasks. + // * Sequential tasks may call into functions like + // Servo_StyleSet_GetBaseComputedValuesForElement, which instantiate a + // ThreadLocalStyleContext on the main thread. If the main thread + // ThreadLocalStyleContext has not released its TLS borrow by that point, + // we'll panic on double-borrow. + let mut scoped_tls = ScopedTLS::>::new(pool); + // Process the nodes breadth-first. This helps keep similar traversal characteristics for the + // style sharing cache. + let work_unit_max = work_unit_max(); + with_pool_in_place_scope(work_unit_max, pool, |maybe_scope| { + let mut tlc = scoped_tls.ensure(parallel::create_thread_local_context); + let mut context = StyleContext { + shared: traversal.shared_context(), + thread_local: &mut tlc, + }; + + debug_assert_eq!( + scoped_tls.current_thread_index(), + 0, + "Main thread should be the first thread" + ); + + let mut discovered = VecDeque::with_capacity(work_unit_max * 2); + discovered.push_back(unsafe { SendNode::new(root.as_node()) }); + parallel::style_trees( + &mut context, + discovered, + root.as_node().opaque(), + work_unit_max, + PerLevelTraversalData { + current_dom_depth: root.depth(), + }, + maybe_scope, + traversal, + &scoped_tls, + ); + }); + + // Collect statistics from thread-locals if requested. + if dump_stats || report_stats { + let mut aggregate = PerThreadTraversalStatistics::default(); + for slot in scoped_tls.slots() { + 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 parallel = pool.is_some(); + let stats = + TraversalStatistics::new(aggregate, traversal, parallel, start_time.unwrap()); + if stats.is_large { + println!("{}", stats); + } + } + } + + root +} diff --git a/servo/components/style/encoding_support.rs b/servo/components/style/encoding_support.rs new file mode 100644 index 0000000000..c144ad0b3b --- /dev/null +++ b/servo/components/style/encoding_support.rs @@ -0,0 +1,105 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Parsing stylesheets from bytes (not `&str`). + +use crate::context::QuirksMode; +use crate::error_reporting::ParseErrorReporter; +use crate::media_queries::MediaList; +use crate::shared_lock::SharedRwLock; +use crate::stylesheets::{AllowImportRules, Origin, Stylesheet, StylesheetLoader, UrlExtraData}; +use cssparser::{stylesheet_encoding, EncodingSupport}; +use servo_arc::Arc; +use std::borrow::Cow; +use std::str; + +struct EncodingRs; + +impl EncodingSupport for EncodingRs { + type Encoding = &'static encoding_rs::Encoding; + + fn utf8() -> Self::Encoding { + encoding_rs::UTF_8 + } + + fn is_utf16_be_or_le(encoding: &Self::Encoding) -> bool { + *encoding == encoding_rs::UTF_16LE || *encoding == encoding_rs::UTF_16BE + } + + fn from_label(ascii_label: &[u8]) -> Option { + encoding_rs::Encoding::for_label(ascii_label) + } +} + +fn decode_stylesheet_bytes<'a>( + css: &'a [u8], + protocol_encoding_label: Option<&str>, + environment_encoding: Option<&'static encoding_rs::Encoding>, +) -> Cow<'a, str> { + let fallback_encoding = stylesheet_encoding::( + css, + protocol_encoding_label.map(str::as_bytes), + environment_encoding, + ); + let (result, _used_encoding, _) = fallback_encoding.decode(&css); + // FIXME record used encoding for environment encoding of @import + result +} + +impl Stylesheet { + /// Parse a stylesheet from a set of bytes, potentially received over the + /// network. + /// + /// Takes care of decoding the network bytes and forwards the resulting + /// string to `Stylesheet::from_str`. + pub fn from_bytes( + bytes: &[u8], + url_data: UrlExtraData, + protocol_encoding_label: Option<&str>, + environment_encoding: Option<&'static encoding_rs::Encoding>, + origin: Origin, + media: MediaList, + shared_lock: SharedRwLock, + stylesheet_loader: Option<&dyn StylesheetLoader>, + error_reporter: Option<&dyn ParseErrorReporter>, + quirks_mode: QuirksMode, + ) -> Stylesheet { + let string = decode_stylesheet_bytes(bytes, protocol_encoding_label, environment_encoding); + Stylesheet::from_str( + &string, + url_data, + origin, + Arc::new(shared_lock.wrap(media)), + shared_lock, + stylesheet_loader, + error_reporter, + quirks_mode, + 0, + AllowImportRules::Yes, + ) + } + + /// Updates an empty stylesheet with a set of bytes that reached over the + /// network. + pub fn update_from_bytes( + existing: &Stylesheet, + bytes: &[u8], + protocol_encoding_label: Option<&str>, + environment_encoding: Option<&'static encoding_rs::Encoding>, + url_data: UrlExtraData, + stylesheet_loader: Option<&dyn StylesheetLoader>, + error_reporter: Option<&dyn ParseErrorReporter>, + ) { + let string = decode_stylesheet_bytes(bytes, protocol_encoding_label, environment_encoding); + Self::update_from_str( + existing, + &string, + url_data, + stylesheet_loader, + error_reporter, + 0, + AllowImportRules::Yes, + ) + } +} diff --git a/servo/components/style/error_reporting.rs b/servo/components/style/error_reporting.rs new file mode 100644 index 0000000000..6db4c18e27 --- /dev/null +++ b/servo/components/style/error_reporting.rs @@ -0,0 +1,454 @@ +/* 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::parser::{Component, RelativeSelector, Selector}; +use selectors::visitor::{SelectorListKind, SelectorVisitor}; +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>, &'a [SelectorList]), + /// A property descriptor was not recognized. + UnsupportedPropertyDescriptor(&'a str, ParseError<'a>), + /// A font face descriptor was not recognized. + UnsupportedFontFaceDescriptor(&'a str, ParseError<'a>), + /// A font feature values descriptor was not recognized. + UnsupportedFontFeatureValuesDescriptor(&'a str, ParseError<'a>), + /// A font palette values descriptor was not recognized. + UnsupportedFontPaletteValuesDescriptor(&'a str, ParseError<'a>), + /// A keyframe rule was not valid. + InvalidKeyframeRule(&'a str, ParseError<'a>), + /// A font feature values rule was not valid. + InvalidFontFeatureValuesRule(&'a str, ParseError<'a>), + /// A keyframe property declaration was not recognized. + UnsupportedKeyframePropertyDeclaration(&'a str, ParseError<'a>), + /// A rule was invalid for some reason. + InvalidRule(&'a str, ParseError<'a>), + /// A rule was not recognized. + UnsupportedRule(&'a str, ParseError<'a>), + /// A viewport descriptor declaration was not recognized. + UnsupportedViewportDescriptorDeclaration(&'a str, ParseError<'a>), + /// A counter style descriptor declaration was not recognized. + UnsupportedCounterStyleDescriptorDeclaration(&'a str, ParseError<'a>), + /// A counter style rule had no symbols. + InvalidCounterStyleWithoutSymbols(String), + /// A counter style rule had less than two symbols. + InvalidCounterStyleNotEnoughSymbols(String), + /// A counter style rule did not have additive-symbols. + InvalidCounterStyleWithoutAdditiveSymbols, + /// A counter style rule had extends with symbols. + InvalidCounterStyleExtendsWithSymbols, + /// A counter style rule had extends with additive-symbols. + InvalidCounterStyleExtendsWithAdditiveSymbols, + /// A media rule was invalid for some reason. + InvalidMediaRule(&'a str, ParseError<'a>), + /// A value was not recognized. + UnsupportedValue(&'a str, ParseError<'a>), + /// A never-matching `:host` selector was found. + NeverMatchingHostSelector(String), +} + +impl<'a> fmt::Display for ContextualParseError<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn token_to_str(t: &Token, f: &mut fmt::Formatter) -> fmt::Result { + match *t { + Token::Ident(ref i) => write!(f, "identifier {}", i), + Token::AtKeyword(ref kw) => write!(f, "keyword @{}", kw), + Token::Hash(ref h) => write!(f, "hash #{}", h), + Token::IDHash(ref h) => write!(f, "id selector #{}", h), + Token::QuotedString(ref s) => write!(f, "quoted string \"{}\"", s), + Token::UnquotedUrl(ref u) => write!(f, "url {}", u), + Token::Delim(ref d) => write!(f, "delimiter {}", d), + Token::Number { + int_value: Some(i), .. + } => write!(f, "number {}", i), + Token::Number { value, .. } => write!(f, "number {}", value), + Token::Percentage { + int_value: Some(i), .. + } => write!(f, "percentage {}", i), + Token::Percentage { unit_value, .. } => { + write!(f, "percentage {}", unit_value * 100.) + }, + Token::Dimension { + value, ref unit, .. + } => write!(f, "dimension {}{}", value, unit), + Token::WhiteSpace(_) => write!(f, "whitespace"), + Token::Comment(_) => write!(f, "comment"), + Token::Colon => write!(f, "colon (:)"), + Token::Semicolon => write!(f, "semicolon (;)"), + Token::Comma => write!(f, "comma (,)"), + Token::IncludeMatch => write!(f, "include match (~=)"), + Token::DashMatch => write!(f, "dash match (|=)"), + Token::PrefixMatch => write!(f, "prefix match (^=)"), + Token::SuffixMatch => write!(f, "suffix match ($=)"), + Token::SubstringMatch => write!(f, "substring match (*=)"), + Token::CDO => write!(f, "CDO ()"), + Token::Function(ref name) => write!(f, "function {}", name), + Token::ParenthesisBlock => write!(f, "parenthesis ("), + Token::SquareBracketBlock => write!(f, "square bracket ["), + Token::CurlyBracketBlock => write!(f, "curly bracket {{"), + Token::BadUrl(ref _u) => write!(f, "bad url parse error"), + Token::BadString(ref _s) => write!(f, "bad string parse error"), + Token::CloseParenthesis => write!(f, "unmatched close parenthesis"), + Token::CloseSquareBracket => write!(f, "unmatched close square bracket"), + Token::CloseCurlyBracket => write!(f, "unmatched close curly bracket"), + } + } + + fn parse_error_to_str(err: &ParseError, f: &mut fmt::Formatter) -> fmt::Result { + match err.kind { + ParseErrorKind::Basic(BasicParseErrorKind::UnexpectedToken(ref t)) => { + write!(f, "found unexpected ")?; + token_to_str(t, f) + }, + ParseErrorKind::Basic(BasicParseErrorKind::EndOfInput) => { + write!(f, "unexpected end of input") + }, + ParseErrorKind::Basic(BasicParseErrorKind::AtRuleInvalid(ref i)) => { + write!(f, "@ rule invalid: {}", i) + }, + ParseErrorKind::Basic(BasicParseErrorKind::AtRuleBodyInvalid) => { + write!(f, "@ rule invalid") + }, + ParseErrorKind::Basic(BasicParseErrorKind::QualifiedRuleInvalid) => { + write!(f, "qualified rule invalid") + }, + ParseErrorKind::Custom(ref err) => write!(f, "{:?}", err), + } + } + + match *self { + ContextualParseError::UnsupportedPropertyDeclaration(decl, ref err, _selectors) => { + write!(f, "Unsupported property declaration: '{}', ", decl)?; + parse_error_to_str(err, f) + }, + ContextualParseError::UnsupportedPropertyDescriptor(decl, ref err) => { + write!( + f, + "Unsupported @property descriptor declaration: '{}', ", + decl + )?; + parse_error_to_str(err, f) + }, + ContextualParseError::UnsupportedFontFaceDescriptor(decl, ref err) => { + write!( + f, + "Unsupported @font-face descriptor declaration: '{}', ", + decl + )?; + parse_error_to_str(err, f) + }, + ContextualParseError::UnsupportedFontFeatureValuesDescriptor(decl, ref err) => { + write!( + f, + "Unsupported @font-feature-values descriptor declaration: '{}', ", + decl + )?; + parse_error_to_str(err, f) + }, + ContextualParseError::UnsupportedFontPaletteValuesDescriptor(decl, ref err) => { + write!( + f, + "Unsupported @font-palette-values descriptor declaration: '{}', ", + decl + )?; + parse_error_to_str(err, f) + }, + ContextualParseError::InvalidKeyframeRule(rule, ref err) => { + write!(f, "Invalid keyframe rule: '{}', ", rule)?; + parse_error_to_str(err, f) + }, + ContextualParseError::InvalidFontFeatureValuesRule(rule, ref err) => { + write!(f, "Invalid font feature value rule: '{}', ", rule)?; + parse_error_to_str(err, f) + }, + ContextualParseError::UnsupportedKeyframePropertyDeclaration(decl, ref err) => { + write!(f, "Unsupported keyframe property declaration: '{}', ", decl)?; + parse_error_to_str(err, f) + }, + ContextualParseError::InvalidRule(rule, ref err) => { + write!(f, "Invalid rule: '{}', ", rule)?; + parse_error_to_str(err, f) + }, + ContextualParseError::UnsupportedRule(rule, ref err) => { + write!(f, "Unsupported rule: '{}', ", rule)?; + parse_error_to_str(err, f) + }, + ContextualParseError::UnsupportedViewportDescriptorDeclaration(decl, ref err) => { + write!( + f, + "Unsupported @viewport descriptor declaration: '{}', ", + decl + )?; + parse_error_to_str(err, f) + }, + ContextualParseError::UnsupportedCounterStyleDescriptorDeclaration(decl, ref err) => { + write!( + f, + "Unsupported @counter-style descriptor declaration: '{}', ", + decl + )?; + parse_error_to_str(err, f) + }, + ContextualParseError::InvalidCounterStyleWithoutSymbols(ref system) => write!( + f, + "Invalid @counter-style rule: 'system: {}' without 'symbols'", + system + ), + ContextualParseError::InvalidCounterStyleNotEnoughSymbols(ref system) => write!( + f, + "Invalid @counter-style rule: 'system: {}' less than two 'symbols'", + system + ), + ContextualParseError::InvalidCounterStyleWithoutAdditiveSymbols => write!( + f, + "Invalid @counter-style rule: 'system: additive' without 'additive-symbols'" + ), + ContextualParseError::InvalidCounterStyleExtendsWithSymbols => write!( + f, + "Invalid @counter-style rule: 'system: extends …' with 'symbols'" + ), + ContextualParseError::InvalidCounterStyleExtendsWithAdditiveSymbols => write!( + f, + "Invalid @counter-style rule: 'system: extends …' with 'additive-symbols'" + ), + ContextualParseError::InvalidMediaRule(media_rule, ref err) => { + write!(f, "Invalid media rule: {}, ", media_rule)?; + parse_error_to_str(err, f) + }, + ContextualParseError::UnsupportedValue(_value, ref err) => parse_error_to_str(err, f), + ContextualParseError::NeverMatchingHostSelector(ref selector) => { + write!(f, ":host selector is not featureless: {}", selector) + }, + } + } +} + +/// A generic trait for an error reporter. +pub trait ParseErrorReporter { + /// Called when the style engine detects an error. + /// + /// Returns the current input being parsed, the source location it was + /// reported from, and a message. + fn report_error( + &self, + url: &UrlExtraData, + location: SourceLocation, + error: ContextualParseError, + ); +} + +/// An error reporter that uses [the `log` crate](https://github.com/rust-lang-nursery/log) +/// at `info` level. +/// +/// This logging is silent by default, and can be enabled with a `RUST_LOG=style=info` +/// environment variable. +/// (See [`env_logger`](https://rust-lang-nursery.github.io/log/env_logger/).) +#[cfg(feature = "servo")] +pub struct RustLogReporter; + +#[cfg(feature = "servo")] +impl ParseErrorReporter for RustLogReporter { + fn report_error( + &self, + url: &UrlExtraData, + location: SourceLocation, + error: ContextualParseError, + ) { + if log_enabled!(log::Level::Info) { + info!( + "Url:\t{}\n{}:{} {}", + url.as_str(), + location.line, + location.column, + error + ) + } + } +} + +/// Any warning a selector may generate. +/// TODO(dshin): Bug 1860634 - Merge with never matching host selector warning, which is part of the rule parser. +#[repr(u8)] +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum SelectorWarningKind { + /// Relative Selector with not enough constraint, either outside or inside the selector. e.g. `*:has(.a)`, `.a:has(*)`. + /// May cause expensive invalidations for every element inserted and/or removed. + UnconstraintedRelativeSelector, +} + +impl SelectorWarningKind { + /// Get all warnings for this selector. + pub fn from_selector(selector: &Selector) -> Vec { + let mut result = vec![]; + if UnconstrainedRelativeSelectorVisitor::has_warning(selector, 0, false) { + result.push(SelectorWarningKind::UnconstraintedRelativeSelector); + } + result + } +} + +/// Per-compound state for finding unconstrained relative selectors. +struct PerCompoundState { + /// Is there a relative selector in this compound? + relative_selector_found: bool, + /// Is this compound constrained in any way? + constrained: bool, + /// Nested below, or inside relative selector? + in_relative_selector: bool, +} + +impl PerCompoundState { + fn new(in_relative_selector: bool) -> Self { + Self { + relative_selector_found: false, + constrained: false, + in_relative_selector, + } + } +} + +/// Visitor to check if there's any unconstrained relative selector. +struct UnconstrainedRelativeSelectorVisitor { + compound_state: PerCompoundState, +} + +impl UnconstrainedRelativeSelectorVisitor { + fn new(in_relative_selector: bool) -> Self { + Self { + compound_state: PerCompoundState::new(in_relative_selector), + } + } + + fn has_warning( + selector: &Selector, + offset: usize, + in_relative_selector: bool, + ) -> bool { + let relative_selector = matches!( + selector.iter_raw_parse_order_from(0).next().unwrap(), + Component::RelativeSelectorAnchor + ); + debug_assert!( + !relative_selector || offset == 0, + "Checking relative selector from non-rightmost?" + ); + let mut visitor = Self::new(in_relative_selector); + let mut iter = if relative_selector { + selector.iter_skip_relative_selector_anchor() + } else { + selector.iter_from(offset) + }; + loop { + visitor.compound_state = PerCompoundState::new(in_relative_selector); + + for s in &mut iter { + s.visit(&mut visitor); + } + + if (visitor.compound_state.relative_selector_found || + visitor.compound_state.in_relative_selector) && + !visitor.compound_state.constrained + { + return true; + } + + if iter.next_sequence().is_none() { + break; + } + } + false + } +} + +impl SelectorVisitor for UnconstrainedRelativeSelectorVisitor { + type Impl = SelectorImpl; + + fn visit_simple_selector(&mut self, c: &Component) -> bool { + match c { + // Deferred to visit_selector_list + Component::Is(..) | + Component::Where(..) | + Component::Negation(..) | + Component::Has(..) => (), + Component::ExplicitUniversalType => (), + _ => self.compound_state.constrained |= true, + }; + true + } + + fn visit_selector_list( + &mut self, + _list_kind: SelectorListKind, + list: &[Selector], + ) -> bool { + let mut all_constrained = true; + for s in list { + let mut offset = 0; + // First, check the rightmost compound for constraint at this level. + if !self.compound_state.in_relative_selector { + let mut nested = Self::new(false); + let mut iter = s.iter(); + loop { + for c in &mut iter { + c.visit(&mut nested); + offset += 1; + } + + let c = iter.next_sequence(); + offset += 1; + if c.map_or(true, |c| !c.is_pseudo_element()) { + break; + } + } + // Every single selector in the list must be constrained. + all_constrained &= nested.compound_state.constrained; + } + + if offset >= s.len() { + continue; + } + + // Then, recurse in to check at the deeper level. + if Self::has_warning(s, offset, self.compound_state.in_relative_selector) { + self.compound_state.constrained = false; + if !self.compound_state.in_relative_selector { + self.compound_state.relative_selector_found = true; + } + return false; + } + } + self.compound_state.constrained |= all_constrained; + true + } + + fn visit_relative_selector_list(&mut self, list: &[RelativeSelector]) -> bool { + debug_assert!( + !self.compound_state.in_relative_selector, + "Nested relative selector" + ); + self.compound_state.relative_selector_found = true; + + for rs in list { + // If the inside is unconstrained, we are unconstrained no matter what. + if Self::has_warning(&rs.selector, 0, true) { + self.compound_state.constrained = false; + return false; + } + } + true + } +} diff --git a/servo/components/style/font_face.rs b/servo/components/style/font_face.rs new file mode 100644 index 0000000000..5f1efdd266 --- /dev/null +++ b/servo/components/style/font_face.rs @@ -0,0 +1,807 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! The [`@font-face`][ff] at-rule. +//! +//! [ff]: https://drafts.csswg.org/css-fonts/#at-font-face-rule + +use crate::error_reporting::ContextualParseError; +use crate::parser::{Parse, ParserContext}; +#[cfg(feature = "gecko")] +use crate::properties::longhands::font_language_override; +use crate::shared_lock::{SharedRwLockReadGuard, ToCssWithGuard}; +use crate::str::CssStringWriter; +use crate::values::computed::font::{FamilyName, FontStretch}; +use crate::values::generics::font::FontStyle as GenericFontStyle; +use crate::values::specified::font::SpecifiedFontStyle; +use crate::values::specified::font::{ + AbsoluteFontWeight, FontStretch as SpecifiedFontStretch, MetricsOverride, +}; +#[cfg(feature = "gecko")] +use crate::values::specified::font::{FontFeatureSettings, FontVariationSettings}; +use crate::values::specified::url::SpecifiedUrl; +use crate::values::specified::{Angle, NonNegativePercentage}; +#[cfg(feature = "gecko")] +use cssparser::UnicodeRange; +use cssparser::{ + AtRuleParser, CowRcStr, DeclarationParser, Parser, QualifiedRuleParser, RuleBodyItemParser, + RuleBodyParser, SourceLocation, +}; +use selectors::parser::SelectorParseErrorKind; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError}; +use style_traits::{StyleParseErrorKind, ToCss}; + +/// A source for a font-face rule. +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive(Clone, Debug, Eq, PartialEq, ToCss, ToShmem)] +pub enum Source { + /// A `url()` source. + Url(UrlSource), + /// A `local()` source. + #[css(function)] + Local(FamilyName), +} + +/// A list of sources for the font-face src descriptor. +#[derive(Clone, Debug, Eq, PartialEq, ToCss, ToShmem)] +#[css(comma)] +pub struct SourceList(#[css(iterable)] pub Vec); + +// We can't just use OneOrMoreSeparated to derive Parse for the Source list, +// because we want to filter out components that parsed as None, then fail if no +// valid components remain. So we provide our own implementation here. +impl Parse for SourceList { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + // Parse the comma-separated list, then let filter_map discard any None items. + let list = input + .parse_comma_separated(|input| { + let s = input.parse_entirely(|input| Source::parse(context, input)); + while input.next().is_ok() {} + Ok(s.ok()) + })? + .into_iter() + .filter_map(|s| s) + .collect::>(); + if list.is_empty() { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } else { + Ok(SourceList(list)) + } + } +} + +/// Keywords for the font-face src descriptor's format() function. +/// ('None' and 'Unknown' are for internal use in gfx, not exposed to CSS.) +#[derive(Clone, Copy, Debug, Eq, Parse, PartialEq, ToCss, ToShmem)] +#[repr(u8)] +#[allow(missing_docs)] +pub enum FontFaceSourceFormatKeyword { + #[css(skip)] + None, + Collection, + EmbeddedOpentype, + Opentype, + Svg, + Truetype, + Woff, + Woff2, + #[css(skip)] + Unknown, +} + +/// Flags for the @font-face tech() function, indicating font technologies +/// required by the resource. +#[derive(Clone, Copy, Debug, Eq, PartialEq, ToShmem)] +#[repr(C)] +pub struct FontFaceSourceTechFlags(u16); +bitflags! { + impl FontFaceSourceTechFlags: u16 { + /// Font requires OpenType feature support. + const FEATURES_OPENTYPE = 1 << 0; + /// Font requires Apple Advanced Typography support. + const FEATURES_AAT = 1 << 1; + /// Font requires Graphite shaping support. + const FEATURES_GRAPHITE = 1 << 2; + /// Font requires COLRv0 rendering support (simple list of colored layers). + const COLOR_COLRV0 = 1 << 3; + /// Font requires COLRv1 rendering support (graph of paint operations). + const COLOR_COLRV1 = 1 << 4; + /// Font requires SVG glyph rendering support. + const COLOR_SVG = 1 << 5; + /// Font has bitmap glyphs in 'sbix' format. + const COLOR_SBIX = 1 << 6; + /// Font has bitmap glyphs in 'CBDT' format. + const COLOR_CBDT = 1 << 7; + /// Font requires OpenType Variations support. + const VARIATIONS = 1 << 8; + /// Font requires CPAL palette selection support. + const PALETTES = 1 << 9; + /// Font requires support for incremental downloading. + const INCREMENTAL = 1 << 10; + } +} + +impl FontFaceSourceTechFlags { + /// Parse a single font-technology keyword and return its flag. + pub fn parse_one<'i, 't>(input: &mut Parser<'i, 't>) -> Result> { + Ok(try_match_ident_ignore_ascii_case! { input, + "features-opentype" => Self::FEATURES_OPENTYPE, + "features-aat" => Self::FEATURES_AAT, + "features-graphite" => Self::FEATURES_GRAPHITE, + "color-colrv0" => Self::COLOR_COLRV0, + "color-colrv1" => Self::COLOR_COLRV1, + "color-svg" => Self::COLOR_SVG, + "color-sbix" => Self::COLOR_SBIX, + "color-cbdt" => Self::COLOR_CBDT, + "variations" => Self::VARIATIONS, + "palettes" => Self::PALETTES, + "incremental" => Self::INCREMENTAL, + }) + } +} + +impl Parse for FontFaceSourceTechFlags { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + let location = input.current_source_location(); + // We don't actually care about the return value of parse_comma_separated, + // because we insert the flags into result as we go. + let mut result = Self::empty(); + input.parse_comma_separated(|input| { + let flag = Self::parse_one(input)?; + result.insert(flag); + Ok(()) + })?; + if !result.is_empty() { + Ok(result) + } else { + Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } +} + +#[allow(unused_assignments)] +impl ToCss for FontFaceSourceTechFlags { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: fmt::Write, + { + let mut first = true; + + macro_rules! write_if_flag { + ($s:expr => $f:ident) => { + if self.contains(Self::$f) { + if first { + first = false; + } else { + dest.write_str(", ")?; + } + dest.write_str($s)?; + } + }; + } + + write_if_flag!("features-opentype" => FEATURES_OPENTYPE); + write_if_flag!("features-aat" => FEATURES_AAT); + write_if_flag!("features-graphite" => FEATURES_GRAPHITE); + write_if_flag!("color-colrv0" => COLOR_COLRV0); + write_if_flag!("color-colrv1" => COLOR_COLRV1); + write_if_flag!("color-svg" => COLOR_SVG); + write_if_flag!("color-sbix" => COLOR_SBIX); + write_if_flag!("color-cbdt" => COLOR_CBDT); + write_if_flag!("variations" => VARIATIONS); + write_if_flag!("palettes" => PALETTES); + write_if_flag!("incremental" => INCREMENTAL); + + Ok(()) + } +} + +/// A POD representation for Gecko. All pointers here are non-owned and as such +/// can't outlive the rule they came from, but we can't enforce that via C++. +/// +/// All the strings are of course utf8. +#[cfg(feature = "gecko")] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(u8)] +#[allow(missing_docs)] +pub enum FontFaceSourceListComponent { + Url(*const crate::gecko::url::CssUrl), + Local(*mut crate::gecko_bindings::structs::nsAtom), + FormatHintKeyword(FontFaceSourceFormatKeyword), + FormatHintString { + length: usize, + utf8_bytes: *const u8, + }, + TechFlags(FontFaceSourceTechFlags), +} + +#[derive(Clone, Debug, Eq, PartialEq, ToCss, ToShmem)] +#[repr(u8)] +#[allow(missing_docs)] +pub enum FontFaceSourceFormat { + Keyword(FontFaceSourceFormatKeyword), + String(String), +} + +/// A `UrlSource` represents a font-face source that has been specified with a +/// `url()` function. +/// +/// +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive(Clone, Debug, Eq, PartialEq, ToShmem)] +pub struct UrlSource { + /// The specified url. + pub url: SpecifiedUrl, + /// The format hint specified with the `format()` function, if present. + pub format_hint: Option, + /// The font technology flags specified with the `tech()` function, if any. + pub tech_flags: FontFaceSourceTechFlags, +} + +impl ToCss for UrlSource { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: fmt::Write, + { + self.url.to_css(dest)?; + if let Some(hint) = &self.format_hint { + dest.write_str(" format(")?; + hint.to_css(dest)?; + dest.write_char(')')?; + } + if !self.tech_flags.is_empty() { + dest.write_str(" tech(")?; + self.tech_flags.to_css(dest)?; + dest.write_char(')')?; + } + Ok(()) + } +} + +/// A font-display value for a @font-face rule. +/// The font-display descriptor determines how a font face is displayed based +/// on whether and when it is downloaded and ready to use. +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, ToComputedValue, ToCss, ToShmem, +)] +#[repr(u8)] +pub enum FontDisplay { + Auto, + Block, + Swap, + Fallback, + Optional, +} + +macro_rules! impl_range { + ($range:ident, $component:ident) => { + impl Parse for $range { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + let first = $component::parse(context, input)?; + let second = input + .try_parse(|input| $component::parse(context, input)) + .unwrap_or_else(|_| first.clone()); + Ok($range(first, second)) + } + } + impl ToCss for $range { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: fmt::Write, + { + self.0.to_css(dest)?; + if self.0 != self.1 { + dest.write_char(' ')?; + self.1.to_css(dest)?; + } + Ok(()) + } + } + }; +} + +/// The font-weight descriptor: +/// +/// https://drafts.csswg.org/css-fonts-4/#descdef-font-face-font-weight +#[derive(Clone, Debug, PartialEq, ToShmem)] +pub struct FontWeightRange(pub AbsoluteFontWeight, pub AbsoluteFontWeight); +impl_range!(FontWeightRange, AbsoluteFontWeight); + +/// The computed representation of the above so Gecko can read them easily. +/// +/// This one is needed because cbindgen doesn't know how to generate +/// specified::Number. +#[repr(C)] +#[allow(missing_docs)] +pub struct ComputedFontWeightRange(f32, f32); + +#[inline] +fn sort_range(a: T, b: T) -> (T, T) { + if a > b { + (b, a) + } else { + (a, b) + } +} + +impl FontWeightRange { + /// Returns a computed font-stretch range. + pub fn compute(&self) -> ComputedFontWeightRange { + let (min, max) = sort_range(self.0.compute().value(), self.1.compute().value()); + ComputedFontWeightRange(min, max) + } +} + +/// The font-stretch descriptor: +/// +/// https://drafts.csswg.org/css-fonts-4/#descdef-font-face-font-stretch +#[derive(Clone, Debug, PartialEq, ToShmem)] +pub struct FontStretchRange(pub SpecifiedFontStretch, pub SpecifiedFontStretch); +impl_range!(FontStretchRange, SpecifiedFontStretch); + +/// The computed representation of the above, so that Gecko can read them +/// easily. +#[repr(C)] +#[allow(missing_docs)] +pub struct ComputedFontStretchRange(FontStretch, FontStretch); + +impl FontStretchRange { + /// Returns a computed font-stretch range. + pub fn compute(&self) -> ComputedFontStretchRange { + fn compute_stretch(s: &SpecifiedFontStretch) -> FontStretch { + match *s { + SpecifiedFontStretch::Keyword(ref kw) => kw.compute(), + SpecifiedFontStretch::Stretch(ref p) => FontStretch::from_percentage(p.0.get()), + SpecifiedFontStretch::System(..) => unreachable!(), + } + } + + let (min, max) = sort_range(compute_stretch(&self.0), compute_stretch(&self.1)); + ComputedFontStretchRange(min, max) + } +} + +/// The font-style descriptor: +/// +/// https://drafts.csswg.org/css-fonts-4/#descdef-font-face-font-style +#[derive(Clone, Debug, PartialEq, ToShmem)] +#[allow(missing_docs)] +pub enum FontStyle { + Normal, + Italic, + Oblique(Angle, Angle), +} + +/// The computed representation of the above, with angles in degrees, so that +/// Gecko can read them easily. +#[repr(u8)] +#[allow(missing_docs)] +pub enum ComputedFontStyleDescriptor { + Normal, + Italic, + Oblique(f32, f32), +} + +impl Parse for FontStyle { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + let style = SpecifiedFontStyle::parse(context, input)?; + Ok(match style { + GenericFontStyle::Normal => FontStyle::Normal, + GenericFontStyle::Italic => FontStyle::Italic, + GenericFontStyle::Oblique(angle) => { + let second_angle = input + .try_parse(|input| SpecifiedFontStyle::parse_angle(context, input)) + .unwrap_or_else(|_| angle.clone()); + + FontStyle::Oblique(angle, second_angle) + }, + }) + } +} + +impl ToCss for FontStyle { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: fmt::Write, + { + match *self { + FontStyle::Normal => dest.write_str("normal"), + FontStyle::Italic => dest.write_str("italic"), + FontStyle::Oblique(ref first, ref second) => { + dest.write_str("oblique")?; + if *first != SpecifiedFontStyle::default_angle() || first != second { + dest.write_char(' ')?; + first.to_css(dest)?; + } + if first != second { + dest.write_char(' ')?; + second.to_css(dest)?; + } + Ok(()) + }, + } + } +} + +impl FontStyle { + /// Returns a computed font-style descriptor. + pub fn compute(&self) -> ComputedFontStyleDescriptor { + match *self { + FontStyle::Normal => ComputedFontStyleDescriptor::Normal, + FontStyle::Italic => ComputedFontStyleDescriptor::Italic, + FontStyle::Oblique(ref first, ref second) => { + let (min, max) = sort_range( + SpecifiedFontStyle::compute_angle_degrees(first), + SpecifiedFontStyle::compute_angle_degrees(second), + ); + ComputedFontStyleDescriptor::Oblique(min, max) + }, + } + } +} + +/// Parse the block inside a `@font-face` rule. +/// +/// Note that the prelude parsing code lives in the `stylesheets` module. +pub fn parse_font_face_block( + context: &ParserContext, + input: &mut Parser, + location: SourceLocation, +) -> FontFaceRuleData { + let mut rule = FontFaceRuleData::empty(location); + { + let mut parser = FontFaceRuleParser { + context, + rule: &mut rule, + }; + let mut iter = RuleBodyParser::new(input, &mut parser); + while let Some(declaration) = iter.next() { + if let Err((error, slice)) = declaration { + let location = error.location; + let error = ContextualParseError::UnsupportedFontFaceDescriptor(slice, error); + context.log_css_error(location, error) + } + } + } + rule +} + +/// A @font-face rule that is known to have font-family and src declarations. +#[cfg(feature = "servo")] +pub struct FontFace<'a>(&'a FontFaceRuleData); + +/// A list of effective sources that we send over through IPC to the font cache. +#[cfg(feature = "servo")] +#[derive(Clone, Debug)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +pub struct EffectiveSources(SourceList); + +#[cfg(feature = "servo")] +impl<'a> FontFace<'a> { + /// Returns the list of effective sources for that font-face, that is the + /// sources which don't list any format hint, or the ones which list at + /// least "truetype" or "opentype". + pub fn effective_sources(&self) -> EffectiveSources { + EffectiveSources( + self.sources() + .iter() + .rev() + .filter(|source| { + if let Source::Url(ref url_source) = **source { + // We support only opentype fonts and truetype is an alias for + // that format. Sources without format hints need to be + // downloaded in case we support them. + url_source.format_hint.as_ref().map_or(true, |hint| { + hint == "truetype" || hint == "opentype" || hint == "woff" + }) + } else { + true + } + }) + .cloned() + .collect(), + ) + } +} + +#[cfg(feature = "servo")] +impl Iterator for EffectiveSources { + type Item = Source; + fn next(&mut self) -> Option { + self.0.pop() + } + + fn size_hint(&self) -> (usize, Option) { + (self.0.len(), Some(self.0.len())) + } +} + +struct FontFaceRuleParser<'a, 'b: 'a> { + context: &'a ParserContext<'b>, + rule: &'a mut FontFaceRuleData, +} + +/// Default methods reject all at rules. +impl<'a, 'b, 'i> AtRuleParser<'i> for FontFaceRuleParser<'a, 'b> { + type Prelude = (); + type AtRule = (); + type Error = StyleParseErrorKind<'i>; +} + +impl<'a, 'b, 'i> QualifiedRuleParser<'i> for FontFaceRuleParser<'a, 'b> { + type Prelude = (); + type QualifiedRule = (); + type Error = StyleParseErrorKind<'i>; +} + +impl<'a, 'b, 'i> RuleBodyItemParser<'i, (), StyleParseErrorKind<'i>> + for FontFaceRuleParser<'a, 'b> +{ + fn parse_qualified(&self) -> bool { + false + } + fn parse_declarations(&self) -> bool { + true + } +} + +impl Parse for Source { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + if input + .try_parse(|input| input.expect_function_matching("local")) + .is_ok() + { + return input + .parse_nested_block(|input| FamilyName::parse(context, input)) + .map(Source::Local); + } + + let url = SpecifiedUrl::parse(context, input)?; + + // Parsing optional format() + let format_hint = if input + .try_parse(|input| input.expect_function_matching("format")) + .is_ok() + { + input.parse_nested_block(|input| { + if let Ok(kw) = input.try_parse(FontFaceSourceFormatKeyword::parse) { + Ok(Some(FontFaceSourceFormat::Keyword(kw))) + } else { + let s = input.expect_string()?.as_ref().to_owned(); + Ok(Some(FontFaceSourceFormat::String(s))) + } + })? + } else { + None + }; + + // Parse optional tech() + let tech_flags = if static_prefs::pref!("layout.css.font-tech.enabled") && + input + .try_parse(|input| input.expect_function_matching("tech")) + .is_ok() + { + input.parse_nested_block(|input| FontFaceSourceTechFlags::parse(context, input))? + } else { + FontFaceSourceTechFlags::empty() + }; + + Ok(Source::Url(UrlSource { + url, + format_hint, + tech_flags, + })) + } +} + +macro_rules! is_descriptor_enabled { + ("font-variation-settings") => { + static_prefs::pref!("layout.css.font-variations.enabled") + }; + ("size-adjust") => { + static_prefs::pref!("layout.css.size-adjust.enabled") + }; + ($name:tt) => { + true + }; +} + +macro_rules! font_face_descriptors_common { + ( + $( #[$doc: meta] $name: tt $ident: ident / $gecko_ident: ident: $ty: ty, )* + ) => { + /// Data inside a `@font-face` rule. + /// + /// + #[derive(Clone, Debug, PartialEq, ToShmem)] + pub struct FontFaceRuleData { + $( + #[$doc] + pub $ident: Option<$ty>, + )* + /// Line and column of the @font-face rule source code. + pub source_location: SourceLocation, + } + + impl FontFaceRuleData { + /// Create an empty font-face rule + pub fn empty(location: SourceLocation) -> Self { + FontFaceRuleData { + $( + $ident: None, + )* + source_location: location, + } + } + + /// Serialization of declarations in the FontFaceRule + pub fn decl_to_css(&self, dest: &mut CssStringWriter) -> fmt::Result { + $( + if let Some(ref value) = self.$ident { + dest.write_str(concat!($name, ": "))?; + value.to_css(&mut CssWriter::new(dest))?; + dest.write_str("; ")?; + } + )* + Ok(()) + } + } + + impl<'a, 'b, 'i> DeclarationParser<'i> for FontFaceRuleParser<'a, 'b> { + type Declaration = (); + type Error = StyleParseErrorKind<'i>; + + fn parse_value<'t>( + &mut self, + name: CowRcStr<'i>, + input: &mut Parser<'i, 't>, + ) -> Result<(), ParseError<'i>> { + match_ignore_ascii_case! { &*name, + $( + $name if is_descriptor_enabled!($name) => { + // DeclarationParser also calls parse_entirely + // so we’d normally not need to, + // but in this case we do because we set the value as a side effect + // rather than returning it. + let value = input.parse_entirely(|i| Parse::parse(self.context, i))?; + self.rule.$ident = Some(value) + }, + )* + _ => return Err(input.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))), + } + Ok(()) + } + } + } +} + +impl ToCssWithGuard for FontFaceRuleData { + // Serialization of FontFaceRule is not specced. + fn to_css(&self, _guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result { + dest.write_str("@font-face { ")?; + self.decl_to_css(dest)?; + dest.write_char('}') + } +} + +macro_rules! font_face_descriptors { + ( + mandatory descriptors = [ + $( #[$m_doc: meta] $m_name: tt $m_ident: ident / $m_gecko_ident: ident: $m_ty: ty, )* + ] + optional descriptors = [ + $( #[$o_doc: meta] $o_name: tt $o_ident: ident / $o_gecko_ident: ident: $o_ty: ty, )* + ] + ) => { + font_face_descriptors_common! { + $( #[$m_doc] $m_name $m_ident / $m_gecko_ident: $m_ty, )* + $( #[$o_doc] $o_name $o_ident / $o_gecko_ident: $o_ty, )* + } + + impl FontFaceRuleData { + /// Per https://github.com/w3c/csswg-drafts/issues/1133 an @font-face rule + /// is valid as far as the CSS parser is concerned even if it doesn’t have + /// a font-family or src declaration. + /// + /// However both are required for the rule to represent an actual font face. + #[cfg(feature = "servo")] + pub fn font_face(&self) -> Option { + if $( self.$m_ident.is_some() )&&* { + Some(FontFace(self)) + } else { + None + } + } + } + + #[cfg(feature = "servo")] + impl<'a> FontFace<'a> { + $( + #[$m_doc] + pub fn $m_ident(&self) -> &$m_ty { + self.0 .$m_ident.as_ref().unwrap() + } + )* + } + } +} + +#[cfg(feature = "gecko")] +font_face_descriptors! { + mandatory descriptors = [ + /// The name of this font face + "font-family" family / mFamily: FamilyName, + + /// The alternative sources for this font face. + "src" sources / mSrc: SourceList, + ] + optional descriptors = [ + /// The style of this font face. + "font-style" style / mStyle: FontStyle, + + /// The weight of this font face. + "font-weight" weight / mWeight: FontWeightRange, + + /// The stretch of this font face. + "font-stretch" stretch / mStretch: FontStretchRange, + + /// The display of this font face. + "font-display" display / mDisplay: FontDisplay, + + /// The ranges of code points outside of which this font face should not be used. + "unicode-range" unicode_range / mUnicodeRange: Vec, + + /// The feature settings of this font face. + "font-feature-settings" feature_settings / mFontFeatureSettings: FontFeatureSettings, + + /// The variation settings of this font face. + "font-variation-settings" variation_settings / mFontVariationSettings: FontVariationSettings, + + /// The language override of this font face. + "font-language-override" language_override / mFontLanguageOverride: font_language_override::SpecifiedValue, + + /// The ascent override for this font face. + "ascent-override" ascent_override / mAscentOverride: MetricsOverride, + + /// The descent override for this font face. + "descent-override" descent_override / mDescentOverride: MetricsOverride, + + /// The line-gap override for this font face. + "line-gap-override" line_gap_override / mLineGapOverride: MetricsOverride, + + /// The size adjustment for this font face. + "size-adjust" size_adjust / mSizeAdjust: NonNegativePercentage, + ] +} + +#[cfg(feature = "servo")] +font_face_descriptors! { + mandatory descriptors = [ + /// The name of this font face + "font-family" family / mFamily: FamilyName, + + /// The alternative sources for this font face. + "src" sources / mSrc: SourceList, + ] + optional descriptors = [ + ] +} diff --git a/servo/components/style/font_metrics.rs b/servo/components/style/font_metrics.rs new file mode 100644 index 0000000000..391d3653ee --- /dev/null +++ b/servo/components/style/font_metrics.rs @@ -0,0 +1,58 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Access to font metrics from the style system. + +#![deny(missing_docs)] + +use crate::values::computed::Length; + +/// Represents the font metrics that style needs from a font to compute the +/// value of certain CSS units like `ex`. +#[derive(Clone, Debug, PartialEq)] +pub struct FontMetrics { + /// The x-height of the font. + pub x_height: Option, + /// The zero advance. This is usually writing mode dependent + pub zero_advance_measure: Option, + /// The cap-height of the font. + pub cap_height: Option, + /// The ideographic-width of the font. + pub ic_width: Option, + /// The ascent of the font (a value is always available for this). + pub ascent: Length, + /// Script scale down factor for math-depth 1. + /// https://w3c.github.io/mathml-core/#dfn-scriptpercentscaledown + pub script_percent_scale_down: Option, + /// Script scale down factor for math-depth 2. + /// https://w3c.github.io/mathml-core/#dfn-scriptscriptpercentscaledown + pub script_script_percent_scale_down: Option, +} + +impl Default for FontMetrics { + fn default() -> Self { + FontMetrics { + x_height: None, + zero_advance_measure: None, + cap_height: None, + ic_width: None, + ascent: Length::new(0.0), + script_percent_scale_down: None, + script_script_percent_scale_down: None, + } + } +} + +/// Type of font metrics to retrieve. +#[derive(Clone, Debug, PartialEq)] +pub enum FontMetricsOrientation { + /// Get metrics for horizontal or vertical according to the Context's + /// writing mode, using horizontal metrics for vertical/mixed + MatchContextPreferHorizontal, + /// Get metrics for horizontal or vertical according to the Context's + /// writing mode, using vertical metrics for vertical/mixed + MatchContextPreferVertical, + /// Force getting horizontal metrics. + Horizontal, +} diff --git a/servo/components/style/gecko/arc_types.rs b/servo/components/style/gecko/arc_types.rs new file mode 100644 index 0000000000..24bf22d69a --- /dev/null +++ b/servo/components/style/gecko/arc_types.rs @@ -0,0 +1,171 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! This file lists all arc FFI types and defines corresponding addref and release functions. This +//! list loosely corresponds to ServoLockedArcTypeList.h file in Gecko. + +#![allow(non_snake_case, missing_docs)] + +use crate::gecko::url::CssUrlData; +use crate::media_queries::MediaList; +use crate::properties::animated_properties::AnimationValue; +use crate::properties::{ComputedValues, PropertyDeclarationBlock}; +use crate::shared_lock::Locked; +use crate::stylesheets::keyframes_rule::Keyframe; +use crate::stylesheets::{ + ContainerRule, CounterStyleRule, CssRules, DocumentRule, FontFaceRule, FontFeatureValuesRule, + FontPaletteValuesRule, ImportRule, KeyframesRule, LayerBlockRule, LayerStatementRule, + MediaRule, NamespaceRule, PageRule, PropertyRule, StyleRule, StylesheetContents, SupportsRule, +}; +use servo_arc::Arc; + +macro_rules! impl_simple_arc_ffi { + ($ty:ty, $addref:ident, $release:ident) => { + #[no_mangle] + pub unsafe extern "C" fn $addref(obj: *const $ty) { + std::mem::forget(Arc::from_raw_addrefed(obj)); + } + + #[no_mangle] + pub unsafe extern "C" fn $release(obj: *const $ty) { + let _ = Arc::from_raw(obj); + } + }; +} + +macro_rules! impl_locked_arc_ffi { + ($servo_type:ty, $alias:ident, $addref:ident, $release:ident) => { + /// A simple alias for a locked type. + pub type $alias = Locked<$servo_type>; + impl_simple_arc_ffi!($alias, $addref, $release); + }; +} + +impl_locked_arc_ffi!( + CssRules, + LockedCssRules, + Servo_CssRules_AddRef, + Servo_CssRules_Release +); +impl_locked_arc_ffi!( + PropertyDeclarationBlock, + LockedDeclarationBlock, + Servo_DeclarationBlock_AddRef, + Servo_DeclarationBlock_Release +); +impl_locked_arc_ffi!( + StyleRule, + LockedStyleRule, + Servo_StyleRule_AddRef, + Servo_StyleRule_Release +); +impl_locked_arc_ffi!( + ImportRule, + LockedImportRule, + Servo_ImportRule_AddRef, + Servo_ImportRule_Release +); +impl_locked_arc_ffi!( + Keyframe, + LockedKeyframe, + Servo_Keyframe_AddRef, + Servo_Keyframe_Release +); +impl_locked_arc_ffi!( + KeyframesRule, + LockedKeyframesRule, + Servo_KeyframesRule_AddRef, + Servo_KeyframesRule_Release +); +impl_simple_arc_ffi!( + LayerBlockRule, + Servo_LayerBlockRule_AddRef, + Servo_LayerBlockRule_Release +); +impl_simple_arc_ffi!( + LayerStatementRule, + Servo_LayerStatementRule_AddRef, + Servo_LayerStatementRule_Release +); +impl_locked_arc_ffi!( + MediaList, + LockedMediaList, + Servo_MediaList_AddRef, + Servo_MediaList_Release +); +impl_simple_arc_ffi!(MediaRule, Servo_MediaRule_AddRef, Servo_MediaRule_Release); +impl_simple_arc_ffi!( + NamespaceRule, + Servo_NamespaceRule_AddRef, + Servo_NamespaceRule_Release +); +impl_locked_arc_ffi!( + PageRule, + LockedPageRule, + Servo_PageRule_AddRef, + Servo_PageRule_Release +); +impl_simple_arc_ffi!( + PropertyRule, + Servo_PropertyRule_AddRef, + Servo_PropertyRule_Release +); +impl_simple_arc_ffi!( + SupportsRule, + Servo_SupportsRule_AddRef, + Servo_SupportsRule_Release +); +impl_simple_arc_ffi!( + ContainerRule, + Servo_ContainerRule_AddRef, + Servo_ContainerRule_Release +); +impl_simple_arc_ffi!( + DocumentRule, + Servo_DocumentRule_AddRef, + Servo_DocumentRule_Release +); +impl_simple_arc_ffi!( + FontFeatureValuesRule, + Servo_FontFeatureValuesRule_AddRef, + Servo_FontFeatureValuesRule_Release +); +impl_simple_arc_ffi!( + FontPaletteValuesRule, + Servo_FontPaletteValuesRule_AddRef, + Servo_FontPaletteValuesRule_Release +); +impl_locked_arc_ffi!( + FontFaceRule, + LockedFontFaceRule, + Servo_FontFaceRule_AddRef, + Servo_FontFaceRule_Release +); +impl_locked_arc_ffi!( + CounterStyleRule, + LockedCounterStyleRule, + Servo_CounterStyleRule_AddRef, + Servo_CounterStyleRule_Release +); + +impl_simple_arc_ffi!( + StylesheetContents, + Servo_StyleSheetContents_AddRef, + Servo_StyleSheetContents_Release +); +impl_simple_arc_ffi!( + CssUrlData, + Servo_CssUrlData_AddRef, + Servo_CssUrlData_Release +); +impl_simple_arc_ffi!( + ComputedValues, + Servo_ComputedStyle_AddRef, + Servo_ComputedStyle_Release +); +impl_simple_arc_ffi!( + AnimationValue, + Servo_AnimationValue_AddRef, + Servo_AnimationValue_Release +); diff --git a/servo/components/style/gecko/conversions.rs b/servo/components/style/gecko/conversions.rs new file mode 100644 index 0000000000..ea3700a323 --- /dev/null +++ b/servo/components/style/gecko/conversions.rs @@ -0,0 +1,59 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! This module contains conversion helpers between Servo and Gecko types +//! Ideally, it would be in geckolib itself, but coherence +//! forces us to keep the traits and implementations here +//! +//! FIXME(emilio): This file should generally just die. + +#![allow(unsafe_code)] + +use crate::gecko_bindings::structs::{nsresult, Matrix4x4Components}; +use crate::stylesheets::RulesMutateError; +use crate::values::computed::transform::Matrix3D; + +impl From for nsresult { + fn from(other: RulesMutateError) -> Self { + match other { + RulesMutateError::Syntax => nsresult::NS_ERROR_DOM_SYNTAX_ERR, + RulesMutateError::IndexSize => nsresult::NS_ERROR_DOM_INDEX_SIZE_ERR, + RulesMutateError::HierarchyRequest => nsresult::NS_ERROR_DOM_HIERARCHY_REQUEST_ERR, + RulesMutateError::InvalidState => nsresult::NS_ERROR_DOM_INVALID_STATE_ERR, + } + } +} + +impl<'a> From<&'a Matrix4x4Components> for Matrix3D { + fn from(m: &'a Matrix4x4Components) -> Matrix3D { + Matrix3D { + m11: m[0], + m12: m[1], + m13: m[2], + m14: m[3], + m21: m[4], + m22: m[5], + m23: m[6], + m24: m[7], + m31: m[8], + m32: m[9], + m33: m[10], + m34: m[11], + m41: m[12], + m42: m[13], + m43: m[14], + m44: m[15], + } + } +} + +impl From for Matrix4x4Components { + fn from(matrix: Matrix3D) -> Self { + [ + matrix.m11, matrix.m12, matrix.m13, matrix.m14, matrix.m21, matrix.m22, matrix.m23, + matrix.m24, matrix.m31, matrix.m32, matrix.m33, matrix.m34, matrix.m41, matrix.m42, + matrix.m43, matrix.m44, + ] + } +} diff --git a/servo/components/style/gecko/data.rs b/servo/components/style/gecko/data.rs new file mode 100644 index 0000000000..c4a5554c5e --- /dev/null +++ b/servo/components/style/gecko/data.rs @@ -0,0 +1,198 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Data needed to style a Gecko document. + +use crate::dom::TElement; +use crate::gecko_bindings::bindings; +use crate::gecko_bindings::structs::{ + self, ServoStyleSetSizes, StyleSheet as DomStyleSheet, StyleSheetInfo, +}; +use crate::invalidation::media_queries::{MediaListKey, ToMediaListKey}; +use crate::media_queries::{Device, MediaList}; +use crate::properties::ComputedValues; +use crate::selector_parser::SnapshotMap; +use crate::shared_lock::{SharedRwLockReadGuard, StylesheetGuards}; +use crate::stylesheets::{StylesheetContents, StylesheetInDocument}; +use crate::stylist::Stylist; +use atomic_refcell::{AtomicRef, AtomicRefCell, AtomicRefMut}; +use malloc_size_of::MallocSizeOfOps; +use servo_arc::Arc; +use std::fmt; + +/// Little wrapper to a Gecko style sheet. +#[derive(Eq, PartialEq)] +pub struct GeckoStyleSheet(*const DomStyleSheet); + +// NOTE(emilio): These are kind of a lie. We allow to make these Send + Sync so that other data +// structures can also be Send and Sync, but Gecko's stylesheets are main-thread-reference-counted. +// +// We assert that we reference-count in the right thread (in the Addref/Release implementations). +// Sending these to a different thread can't really happen (it could theoretically really happen if +// we allowed @import rules inside a nested style rule, but that can't happen per spec and would be +// a parser bug, caught by the asserts). +unsafe impl Send for GeckoStyleSheet {} +unsafe impl Sync for GeckoStyleSheet {} + +impl fmt::Debug for GeckoStyleSheet { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + let contents = self.contents(); + formatter + .debug_struct("GeckoStyleSheet") + .field("origin", &contents.origin) + .field("url_data", &*contents.url_data.read()) + .finish() + } +} + +impl ToMediaListKey for crate::gecko::data::GeckoStyleSheet { + fn to_media_list_key(&self) -> MediaListKey { + use std::mem; + unsafe { MediaListKey::from_raw(mem::transmute(self.0)) } + } +} + +impl GeckoStyleSheet { + /// Create a `GeckoStyleSheet` from a raw `DomStyleSheet` pointer. + #[inline] + pub unsafe fn new(s: *const DomStyleSheet) -> Self { + debug_assert!(!s.is_null()); + bindings::Gecko_StyleSheet_AddRef(s); + Self::from_addrefed(s) + } + + /// Create a `GeckoStyleSheet` from a raw `DomStyleSheet` pointer that + /// already holds a strong reference. + #[inline] + pub unsafe fn from_addrefed(s: *const DomStyleSheet) -> Self { + assert!(!s.is_null()); + GeckoStyleSheet(s) + } + + /// HACK(emilio): This is so that we can avoid crashing release due to + /// bug 1719963 and can hopefully get a useful report from fuzzers. + #[inline] + pub fn hack_is_null(&self) -> bool { + self.0.is_null() + } + + /// Get the raw `StyleSheet` that we're wrapping. + pub fn raw(&self) -> &DomStyleSheet { + unsafe { &*self.0 } + } + + fn inner(&self) -> &StyleSheetInfo { + unsafe { &*(self.raw().mInner as *const StyleSheetInfo) } + } +} + +impl Drop for GeckoStyleSheet { + fn drop(&mut self) { + unsafe { bindings::Gecko_StyleSheet_Release(self.0) }; + } +} + +impl Clone for GeckoStyleSheet { + fn clone(&self) -> Self { + unsafe { bindings::Gecko_StyleSheet_AddRef(self.0) }; + GeckoStyleSheet(self.0) + } +} + +impl StylesheetInDocument for GeckoStyleSheet { + fn media<'a>(&'a self, guard: &'a SharedRwLockReadGuard) -> Option<&'a MediaList> { + use crate::gecko_bindings::structs::mozilla::dom::MediaList as DomMediaList; + unsafe { + let dom_media_list = self.raw().mMedia.mRawPtr as *const DomMediaList; + if dom_media_list.is_null() { + return None; + } + let list = &*(*dom_media_list).mRawList.mRawPtr; + Some(list.read_with(guard)) + } + } + + // All the stylesheets Servo knows about are enabled, because that state is + // handled externally by Gecko. + #[inline] + fn enabled(&self) -> bool { + true + } + + #[inline] + fn contents(&self) -> &StylesheetContents { + debug_assert!(!self.inner().mContents.mRawPtr.is_null()); + unsafe { &*self.inner().mContents.mRawPtr } + } +} + +/// The container for data that a Servo-backed Gecko document needs to style +/// itself. +pub struct PerDocumentStyleDataImpl { + /// Rule processor. + pub stylist: Stylist, + + /// A cache from element to resolved style. + pub undisplayed_style_cache: crate::traversal::UndisplayedStyleCache, + + /// The generation for which our cache is valid. + pub undisplayed_style_cache_generation: u64, +} + +/// The data itself is an `AtomicRefCell`, which guarantees the proper semantics +/// and unexpected races while trying to mutate it. +pub struct PerDocumentStyleData(AtomicRefCell); + +impl PerDocumentStyleData { + /// Create a `PerDocumentStyleData`. + pub fn new(document: *const structs::Document) -> Self { + let device = Device::new(document); + let quirks_mode = device.document().mCompatMode; + + PerDocumentStyleData(AtomicRefCell::new(PerDocumentStyleDataImpl { + stylist: Stylist::new(device, quirks_mode.into()), + undisplayed_style_cache: Default::default(), + undisplayed_style_cache_generation: 0, + })) + } + + /// Get an immutable reference to this style data. + pub fn borrow(&self) -> AtomicRef { + self.0.borrow() + } + + /// Get an mutable reference to this style data. + pub fn borrow_mut(&self) -> AtomicRefMut { + self.0.borrow_mut() + } +} + +impl PerDocumentStyleDataImpl { + /// Recreate the style data if the stylesheets have changed. + pub fn flush_stylesheets( + &mut self, + guard: &SharedRwLockReadGuard, + document_element: Option, + snapshots: Option<&SnapshotMap>, + ) -> bool + where + E: TElement, + { + self.stylist + .flush(&StylesheetGuards::same(guard), document_element, snapshots) + } + + /// Get the default computed values for this document. + pub fn default_computed_values(&self) -> &Arc { + self.stylist.device().default_computed_values_arc() + } + + /// Measure heap usage. + pub fn add_size_of(&self, ops: &mut MallocSizeOfOps, sizes: &mut ServoStyleSetSizes) { + self.stylist.add_size_of(ops, sizes); + } +} + +/// The gecko-specific AuthorStyles instantiation. +pub type AuthorStyles = crate::author_styles::AuthorStyles; diff --git a/servo/components/style/gecko/media_features.rs b/servo/components/style/gecko/media_features.rs new file mode 100644 index 0000000000..c9ad30b28b --- /dev/null +++ b/servo/components/style/gecko/media_features.rs @@ -0,0 +1,1003 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Gecko's media feature list and evaluator. + +use crate::gecko_bindings::bindings; +use crate::gecko_bindings::structs; +use crate::gecko_bindings::structs::ScreenColorGamut; +use crate::media_queries::{Device, MediaType}; +use crate::queries::condition::KleeneValue; +use crate::queries::feature::{AllowsRanges, Evaluator, FeatureFlags, QueryFeatureDescription}; +use crate::queries::values::Orientation; +use crate::values::computed::{CSSPixelLength, Context, Ratio, Resolution}; +use crate::values::AtomString; +use app_units::Au; +use euclid::default::Size2D; + +fn device_size(device: &Device) -> Size2D { + let mut width = 0; + let mut height = 0; + unsafe { + bindings::Gecko_MediaFeatures_GetDeviceSize(device.document(), &mut width, &mut height); + } + Size2D::new(Au(width), Au(height)) +} + +/// https://drafts.csswg.org/mediaqueries-4/#width +fn eval_width(context: &Context) -> CSSPixelLength { + CSSPixelLength::new(context.device().au_viewport_size().width.to_f32_px()) +} + +/// https://drafts.csswg.org/mediaqueries-4/#device-width +fn eval_device_width(context: &Context) -> CSSPixelLength { + CSSPixelLength::new(device_size(context.device()).width.to_f32_px()) +} + +/// https://drafts.csswg.org/mediaqueries-4/#height +fn eval_height(context: &Context) -> CSSPixelLength { + CSSPixelLength::new(context.device().au_viewport_size().height.to_f32_px()) +} + +/// https://drafts.csswg.org/mediaqueries-4/#device-height +fn eval_device_height(context: &Context) -> CSSPixelLength { + CSSPixelLength::new(device_size(context.device()).height.to_f32_px()) +} + +fn eval_aspect_ratio_for(context: &Context, get_size: F) -> Ratio +where + F: FnOnce(&Device) -> Size2D, +{ + let size = get_size(context.device()); + Ratio::new(size.width.0 as f32, size.height.0 as f32) +} + +/// https://drafts.csswg.org/mediaqueries-4/#aspect-ratio +fn eval_aspect_ratio(context: &Context) -> Ratio { + eval_aspect_ratio_for(context, Device::au_viewport_size) +} + +/// https://drafts.csswg.org/mediaqueries-4/#device-aspect-ratio +fn eval_device_aspect_ratio(context: &Context) -> Ratio { + eval_aspect_ratio_for(context, device_size) +} + +/// https://compat.spec.whatwg.org/#css-media-queries-webkit-device-pixel-ratio +fn eval_device_pixel_ratio(context: &Context) -> f32 { + eval_resolution(context).dppx() +} + +/// https://drafts.csswg.org/mediaqueries-4/#orientation +fn eval_orientation(context: &Context, value: Option) -> bool { + Orientation::eval(context.device().au_viewport_size(), value) +} + +/// FIXME: There's no spec for `-moz-device-orientation`. +fn eval_device_orientation(context: &Context, value: Option) -> bool { + Orientation::eval(device_size(context.device()), value) +} + +/// Values for the display-mode media feature. +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, PartialEq, ToCss)] +#[repr(u8)] +#[allow(missing_docs)] +pub enum DisplayMode { + Browser = 0, + MinimalUi, + Standalone, + Fullscreen, +} + +/// https://w3c.github.io/manifest/#the-display-mode-media-feature +fn eval_display_mode(context: &Context, query_value: Option) -> bool { + match query_value { + Some(v) => { + v == unsafe { + bindings::Gecko_MediaFeatures_GetDisplayMode(context.device().document()) + } + }, + None => true, + } +} + +/// https://drafts.csswg.org/mediaqueries-4/#grid +fn eval_grid(_: &Context) -> bool { + // Gecko doesn't support grid devices (e.g., ttys), so the 'grid' feature + // is always 0. + false +} + +/// https://compat.spec.whatwg.org/#css-media-queries-webkit-transform-3d +fn eval_transform_3d(_: &Context) -> bool { + true +} + +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] +#[repr(u8)] +enum Scan { + Progressive, + Interlace, +} + +/// https://drafts.csswg.org/mediaqueries-4/#scan +fn eval_scan(_: &Context, _: Option) -> bool { + // Since Gecko doesn't support the 'tv' media type, the 'scan' feature never + // matches. + false +} + +/// https://drafts.csswg.org/mediaqueries-4/#color +fn eval_color(context: &Context) -> i32 { + unsafe { bindings::Gecko_MediaFeatures_GetColorDepth(context.device().document()) } +} + +/// https://drafts.csswg.org/mediaqueries-4/#color-index +fn eval_color_index(_: &Context) -> i32 { + // We should return zero if the device does not use a color lookup table. + 0 +} + +/// https://drafts.csswg.org/mediaqueries-4/#monochrome +fn eval_monochrome(context: &Context) -> i32 { + // For color devices we should return 0. + unsafe { bindings::Gecko_MediaFeatures_GetMonochromeBitsPerPixel(context.device().document()) } +} + +/// Values for the color-gamut media feature. +/// This implements PartialOrd so that lower values will correctly match +/// higher capabilities. +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, PartialEq, PartialOrd, ToCss)] +#[repr(u8)] +enum ColorGamut { + /// The sRGB gamut. + Srgb, + /// The gamut specified by the Display P3 Color Space. + P3, + /// The gamut specified by the ITU-R Recommendation BT.2020 Color Space. + Rec2020, +} + +/// https://drafts.csswg.org/mediaqueries-4/#color-gamut +fn eval_color_gamut(context: &Context, query_value: Option) -> bool { + let query_value = match query_value { + Some(v) => v, + None => return false, + }; + let color_gamut = + unsafe { bindings::Gecko_MediaFeatures_ColorGamut(context.device().document()) }; + // Match if our color gamut is at least as wide as the query value + query_value <= + match color_gamut { + // EndGuard_ is not a valid color gamut, so the default color-gamut is used. + ScreenColorGamut::Srgb | ScreenColorGamut::EndGuard_ => ColorGamut::Srgb, + ScreenColorGamut::P3 => ColorGamut::P3, + ScreenColorGamut::Rec2020 => ColorGamut::Rec2020, + } +} + +/// https://drafts.csswg.org/mediaqueries-4/#resolution +fn eval_resolution(context: &Context) -> Resolution { + let resolution_dppx = + unsafe { bindings::Gecko_MediaFeatures_GetResolution(context.device().document()) }; + Resolution::from_dppx(resolution_dppx) +} + +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] +#[repr(u8)] +enum PrefersReducedMotion { + NoPreference, + Reduce, +} + +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] +#[repr(u8)] +enum PrefersReducedTransparency { + NoPreference, + Reduce, +} + +/// Values for the prefers-color-scheme media feature. +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, PartialEq, ToCss)] +#[repr(u8)] +#[allow(missing_docs)] +pub enum PrefersColorScheme { + Light, + Dark, +} + +/// Values for the dynamic-range and video-dynamic-range media features. +/// https://drafts.csswg.org/mediaqueries-5/#dynamic-range +/// This implements PartialOrd so that lower values will correctly match +/// higher capabilities. +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, PartialEq, PartialOrd, ToCss)] +#[repr(u8)] +#[allow(missing_docs)] +pub enum DynamicRange { + Standard, + High, +} + +/// https://drafts.csswg.org/mediaqueries-5/#prefers-reduced-motion +fn eval_prefers_reduced_motion( + context: &Context, + query_value: Option, +) -> bool { + let prefers_reduced = + unsafe { bindings::Gecko_MediaFeatures_PrefersReducedMotion(context.device().document()) }; + let query_value = match query_value { + Some(v) => v, + None => return prefers_reduced, + }; + + match query_value { + PrefersReducedMotion::NoPreference => !prefers_reduced, + PrefersReducedMotion::Reduce => prefers_reduced, + } +} + +/// https://drafts.csswg.org/mediaqueries-5/#prefers-reduced-transparency +fn eval_prefers_reduced_transparency( + context: &Context, + query_value: Option, +) -> bool { + let prefers_reduced = unsafe { + bindings::Gecko_MediaFeatures_PrefersReducedTransparency(context.device().document()) + }; + let query_value = match query_value { + Some(v) => v, + None => return prefers_reduced, + }; + + match query_value { + PrefersReducedTransparency::NoPreference => !prefers_reduced, + PrefersReducedTransparency::Reduce => prefers_reduced, + } +} + +/// Possible values for prefers-contrast media query. +/// https://drafts.csswg.org/mediaqueries-5/#prefers-contrast +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, PartialEq, ToCss)] +#[repr(u8)] +pub enum PrefersContrast { + /// More contrast is preferred. + More, + /// Low contrast is preferred. + Less, + /// Custom (not more, not less). + Custom, + /// The default value if neither high or low contrast is enabled. + NoPreference, +} + +/// https://drafts.csswg.org/mediaqueries-5/#prefers-contrast +fn eval_prefers_contrast(context: &Context, query_value: Option) -> bool { + let prefers_contrast = + unsafe { bindings::Gecko_MediaFeatures_PrefersContrast(context.device().document()) }; + match query_value { + Some(v) => v == prefers_contrast, + None => prefers_contrast != PrefersContrast::NoPreference, + } +} + +/// Possible values for the forced-colors media query. +/// https://drafts.csswg.org/mediaqueries-5/#forced-colors +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, PartialEq, ToCss)] +#[repr(u8)] +pub enum ForcedColors { + /// Page colors are not being forced. + None, + /// Page colors are being forced. + Active, +} + +/// https://drafts.csswg.org/mediaqueries-5/#forced-colors +fn eval_forced_colors(context: &Context, query_value: Option) -> bool { + let forced = !context.device().use_document_colors(); + match query_value { + Some(query_value) => forced == (query_value == ForcedColors::Active), + None => forced, + } +} + +/// Possible values for the inverted-colors media query. +/// https://drafts.csswg.org/mediaqueries-5/#inverted +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] +#[repr(u8)] +enum InvertedColors { + /// Colors are displayed normally. + None, + /// All pixels within the displayed area have been inverted. + Inverted, +} + +/// https://drafts.csswg.org/mediaqueries-5/#inverted +fn eval_inverted_colors(context: &Context, query_value: Option) -> bool { + let inverted_colors = + unsafe { bindings::Gecko_MediaFeatures_InvertedColors(context.device().document()) }; + let query_value = match query_value { + Some(v) => v, + None => return inverted_colors, + }; + + match query_value { + InvertedColors::None => !inverted_colors, + InvertedColors::Inverted => inverted_colors, + } +} + +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] +#[repr(u8)] +enum OverflowBlock { + None, + Scroll, + Paged, +} + +/// https://drafts.csswg.org/mediaqueries-4/#mf-overflow-block +fn eval_overflow_block(context: &Context, query_value: Option) -> bool { + // For the time being, assume that printing (including previews) + // is the only time when we paginate, and we are otherwise always + // scrolling. This is true at the moment in Firefox, but may need + // updating in the future (e.g., ebook readers built with Stylo, a + // billboard mode that doesn't support overflow at all). + // + // If this ever changes, don't forget to change eval_overflow_inline too. + let scrolling = context.device().media_type() != MediaType::print(); + let query_value = match query_value { + Some(v) => v, + None => return true, + }; + + match query_value { + OverflowBlock::None => false, + OverflowBlock::Scroll => scrolling, + OverflowBlock::Paged => !scrolling, + } +} + +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] +#[repr(u8)] +enum OverflowInline { + None, + Scroll, +} + +/// https://drafts.csswg.org/mediaqueries-4/#mf-overflow-inline +fn eval_overflow_inline(context: &Context, query_value: Option) -> bool { + // See the note in eval_overflow_block. + let scrolling = context.device().media_type() != MediaType::print(); + let query_value = match query_value { + Some(v) => v, + None => return scrolling, + }; + + match query_value { + OverflowInline::None => !scrolling, + OverflowInline::Scroll => scrolling, + } +} + +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] +#[repr(u8)] +enum Update { + None, + Slow, + Fast, +} + +/// https://drafts.csswg.org/mediaqueries-4/#update +fn eval_update(context: &Context, query_value: Option) -> bool { + // This has similar caveats to those described in eval_overflow_block. + // For now, we report that print (incl. print media simulation, + // which can in fact update but is limited to the developer tools) + // is `update: none` and that all other contexts are `update: fast`, + // which may not be true for future platforms, like e-ink devices. + let can_update = context.device().media_type() != MediaType::print(); + let query_value = match query_value { + Some(v) => v, + None => return can_update, + }; + + match query_value { + Update::None => !can_update, + Update::Slow => false, + Update::Fast => can_update, + } +} + +fn do_eval_prefers_color_scheme( + context: &Context, + use_content: bool, + query_value: Option, +) -> bool { + let prefers_color_scheme = unsafe { + bindings::Gecko_MediaFeatures_PrefersColorScheme(context.device().document(), use_content) + }; + match query_value { + Some(v) => prefers_color_scheme == v, + None => true, + } +} + +/// https://drafts.csswg.org/mediaqueries-5/#prefers-color-scheme +fn eval_prefers_color_scheme(context: &Context, query_value: Option) -> bool { + do_eval_prefers_color_scheme(context, /* use_content = */ false, query_value) +} + +fn eval_content_prefers_color_scheme( + context: &Context, + query_value: Option, +) -> bool { + do_eval_prefers_color_scheme(context, /* use_content = */ true, query_value) +} + +/// https://drafts.csswg.org/mediaqueries-5/#dynamic-range +fn eval_dynamic_range(context: &Context, query_value: Option) -> bool { + let dynamic_range = + unsafe { bindings::Gecko_MediaFeatures_DynamicRange(context.device().document()) }; + match query_value { + Some(v) => dynamic_range >= v, + None => false, + } +} +/// https://drafts.csswg.org/mediaqueries-5/#video-dynamic-range +fn eval_video_dynamic_range(context: &Context, query_value: Option) -> bool { + let dynamic_range = + unsafe { bindings::Gecko_MediaFeatures_VideoDynamicRange(context.device().document()) }; + match query_value { + Some(v) => dynamic_range >= v, + None => false, + } +} + +bitflags! { + /// https://drafts.csswg.org/mediaqueries-4/#mf-interaction + struct PointerCapabilities: u8 { + const COARSE = structs::PointerCapabilities_Coarse; + const FINE = structs::PointerCapabilities_Fine; + const HOVER = structs::PointerCapabilities_Hover; + } +} + +fn primary_pointer_capabilities(context: &Context) -> PointerCapabilities { + PointerCapabilities::from_bits_truncate(unsafe { + bindings::Gecko_MediaFeatures_PrimaryPointerCapabilities(context.device().document()) + }) +} + +fn all_pointer_capabilities(context: &Context) -> PointerCapabilities { + PointerCapabilities::from_bits_truncate(unsafe { + bindings::Gecko_MediaFeatures_AllPointerCapabilities(context.device().document()) + }) +} + +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] +#[repr(u8)] +enum Pointer { + None, + Coarse, + Fine, +} + +fn eval_pointer_capabilities( + query_value: Option, + pointer_capabilities: PointerCapabilities, +) -> bool { + let query_value = match query_value { + Some(v) => v, + None => return !pointer_capabilities.is_empty(), + }; + + match query_value { + Pointer::None => pointer_capabilities.is_empty(), + Pointer::Coarse => pointer_capabilities.intersects(PointerCapabilities::COARSE), + Pointer::Fine => pointer_capabilities.intersects(PointerCapabilities::FINE), + } +} + +/// https://drafts.csswg.org/mediaqueries-4/#pointer +fn eval_pointer(context: &Context, query_value: Option) -> bool { + eval_pointer_capabilities(query_value, primary_pointer_capabilities(context)) +} + +/// https://drafts.csswg.org/mediaqueries-4/#descdef-media-any-pointer +fn eval_any_pointer(context: &Context, query_value: Option) -> bool { + eval_pointer_capabilities(query_value, all_pointer_capabilities(context)) +} + +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] +#[repr(u8)] +enum Hover { + None, + Hover, +} + +fn eval_hover_capabilities( + query_value: Option, + pointer_capabilities: PointerCapabilities, +) -> bool { + let can_hover = pointer_capabilities.intersects(PointerCapabilities::HOVER); + let query_value = match query_value { + Some(v) => v, + None => return can_hover, + }; + + match query_value { + Hover::None => !can_hover, + Hover::Hover => can_hover, + } +} + +/// https://drafts.csswg.org/mediaqueries-4/#hover +fn eval_hover(context: &Context, query_value: Option) -> bool { + eval_hover_capabilities(query_value, primary_pointer_capabilities(context)) +} + +/// https://drafts.csswg.org/mediaqueries-4/#descdef-media-any-hover +fn eval_any_hover(context: &Context, query_value: Option) -> bool { + eval_hover_capabilities(query_value, all_pointer_capabilities(context)) +} + +fn eval_moz_is_glyph(context: &Context) -> bool { + context.device().document().mIsSVGGlyphsDocument() +} + +fn eval_moz_print_preview(context: &Context) -> bool { + let is_print_preview = context.device().is_print_preview(); + if is_print_preview { + debug_assert_eq!(context.device().media_type(), MediaType::print()); + } + is_print_preview +} + +fn eval_moz_is_resource_document(context: &Context) -> bool { + unsafe { bindings::Gecko_MediaFeatures_IsResourceDocument(context.device().document()) } +} + +/// Allows front-end CSS to discern platform via media queries. +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] +#[repr(u8)] +pub enum Platform { + /// Matches any Android version. + Android, + /// For our purposes here, "linux" is just "gtk" (so unix-but-not-mac). + /// There's no need for our front-end code to differentiate between those + /// platforms and they already use the "linux" string elsewhere (e.g., + /// toolkit/themes/linux). + Linux, + /// Matches any macOS version. + Macos, + /// Matches any Windows version. + Windows, +} + +fn eval_moz_platform(_: &Context, query_value: Option) -> bool { + let query_value = match query_value { + Some(v) => v, + None => return false, + }; + + unsafe { bindings::Gecko_MediaFeatures_MatchesPlatform(query_value) } +} + +/// Allows front-end CSS to discern gtk theme via media queries. +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, PartialEq, ToCss)] +#[repr(u8)] +pub enum GtkThemeFamily { + /// Unknown theme family. + Unknown = 0, + /// Adwaita, the default GTK theme. + Adwaita, + /// Breeze, the default KDE theme. + Breeze, + /// Yaru, the default Ubuntu theme. + Yaru, +} + +fn eval_gtk_theme_family(_: &Context, query_value: Option) -> bool { + let family = unsafe { bindings::Gecko_MediaFeatures_GtkThemeFamily() }; + match query_value { + Some(v) => v == family, + None => return family != GtkThemeFamily::Unknown, + } +} + +/// Values for the scripting media feature. +/// https://drafts.csswg.org/mediaqueries-5/#scripting +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, PartialEq, ToCss)] +#[repr(u8)] +pub enum Scripting { + /// Scripting is not supported or not enabled + None, + /// Scripting is supported and enabled, but only for initial page load + /// We will never match this value as it is intended for non-browser user agents, + /// but it is part of the spec so we should still parse it. + /// See: https://github.com/w3c/csswg-drafts/issues/8621 + InitialOnly, + /// Scripting is supported and enabled + Enabled, +} + +/// https://drafts.csswg.org/mediaqueries-5/#scripting +fn eval_scripting(context: &Context, query_value: Option) -> bool { + let scripting = unsafe { bindings::Gecko_MediaFeatures_Scripting(context.device().document()) }; + match query_value { + Some(v) => v == scripting, + None => scripting != Scripting::None, + } +} + +fn eval_moz_overlay_scrollbars(context: &Context) -> bool { + unsafe { bindings::Gecko_MediaFeatures_UseOverlayScrollbars(context.device().document()) } +} + +fn eval_moz_bool_pref(_: &Context, pref: Option<&AtomString>) -> KleeneValue { + let Some(pref) = pref else { + return KleeneValue::False; + }; + KleeneValue::from(unsafe { bindings::Gecko_ComputeBoolPrefMediaQuery(pref.as_ptr()) }) +} + +fn get_lnf_int(int_id: i32) -> i32 { + unsafe { bindings::Gecko_GetLookAndFeelInt(int_id) } +} + +fn get_lnf_int_as_bool(int_id: i32) -> bool { + get_lnf_int(int_id) != 0 +} + +fn get_scrollbar_start_backward(int_id: i32) -> bool { + (get_lnf_int(int_id) & bindings::LookAndFeel_eScrollArrow_StartBackward as i32) != 0 +} + +fn get_scrollbar_start_forward(int_id: i32) -> bool { + (get_lnf_int(int_id) & bindings::LookAndFeel_eScrollArrow_StartForward as i32) != 0 +} + +fn get_scrollbar_end_backward(int_id: i32) -> bool { + (get_lnf_int(int_id) & bindings::LookAndFeel_eScrollArrow_EndBackward as i32) != 0 +} + +fn get_scrollbar_end_forward(int_id: i32) -> bool { + (get_lnf_int(int_id) & bindings::LookAndFeel_eScrollArrow_EndForward as i32) != 0 +} + +macro_rules! lnf_int_feature { + ($feature_name:expr, $int_id:ident, $get_value:ident) => {{ + fn __eval(_: &Context) -> bool { + $get_value(bindings::LookAndFeel_IntID::$int_id as i32) + } + + feature!( + $feature_name, + AllowsRanges::No, + Evaluator::BoolInteger(__eval), + FeatureFlags::CHROME_AND_UA_ONLY, + ) + }}; + ($feature_name:expr, $int_id:ident) => {{ + lnf_int_feature!($feature_name, $int_id, get_lnf_int_as_bool) + }}; +} + +/// Adding new media features requires (1) adding the new feature to this +/// array, with appropriate entries (and potentially any new code needed +/// to support new types in these entries and (2) ensuring that either +/// nsPresContext::MediaFeatureValuesChanged is called when the value that +/// would be returned by the evaluator function could change. +pub static MEDIA_FEATURES: [QueryFeatureDescription; 59] = [ + feature!( + atom!("width"), + AllowsRanges::Yes, + Evaluator::Length(eval_width), + FeatureFlags::VIEWPORT_DEPENDENT, + ), + feature!( + atom!("height"), + AllowsRanges::Yes, + Evaluator::Length(eval_height), + FeatureFlags::VIEWPORT_DEPENDENT, + ), + feature!( + atom!("aspect-ratio"), + AllowsRanges::Yes, + Evaluator::NumberRatio(eval_aspect_ratio), + FeatureFlags::VIEWPORT_DEPENDENT, + ), + feature!( + atom!("orientation"), + AllowsRanges::No, + keyword_evaluator!(eval_orientation, Orientation), + FeatureFlags::VIEWPORT_DEPENDENT, + ), + feature!( + atom!("device-width"), + AllowsRanges::Yes, + Evaluator::Length(eval_device_width), + FeatureFlags::empty(), + ), + feature!( + atom!("device-height"), + AllowsRanges::Yes, + Evaluator::Length(eval_device_height), + FeatureFlags::empty(), + ), + feature!( + atom!("device-aspect-ratio"), + AllowsRanges::Yes, + Evaluator::NumberRatio(eval_device_aspect_ratio), + FeatureFlags::empty(), + ), + feature!( + atom!("-moz-device-orientation"), + AllowsRanges::No, + keyword_evaluator!(eval_device_orientation, Orientation), + FeatureFlags::empty(), + ), + // Webkit extensions that we support for de-facto web compatibility. + // -webkit-{min|max}-device-pixel-ratio (controlled with its own pref): + feature!( + atom!("device-pixel-ratio"), + AllowsRanges::Yes, + Evaluator::Float(eval_device_pixel_ratio), + FeatureFlags::WEBKIT_PREFIX, + ), + // -webkit-transform-3d. + feature!( + atom!("transform-3d"), + AllowsRanges::No, + Evaluator::BoolInteger(eval_transform_3d), + FeatureFlags::WEBKIT_PREFIX, + ), + feature!( + atom!("-moz-device-pixel-ratio"), + AllowsRanges::Yes, + Evaluator::Float(eval_device_pixel_ratio), + FeatureFlags::empty(), + ), + feature!( + atom!("resolution"), + AllowsRanges::Yes, + Evaluator::Resolution(eval_resolution), + FeatureFlags::empty(), + ), + feature!( + atom!("display-mode"), + AllowsRanges::No, + keyword_evaluator!(eval_display_mode, DisplayMode), + FeatureFlags::empty(), + ), + feature!( + atom!("grid"), + AllowsRanges::No, + Evaluator::BoolInteger(eval_grid), + FeatureFlags::empty(), + ), + feature!( + atom!("scan"), + AllowsRanges::No, + keyword_evaluator!(eval_scan, Scan), + FeatureFlags::empty(), + ), + feature!( + atom!("color"), + AllowsRanges::Yes, + Evaluator::Integer(eval_color), + FeatureFlags::empty(), + ), + feature!( + atom!("color-index"), + AllowsRanges::Yes, + Evaluator::Integer(eval_color_index), + FeatureFlags::empty(), + ), + feature!( + atom!("monochrome"), + AllowsRanges::Yes, + Evaluator::Integer(eval_monochrome), + FeatureFlags::empty(), + ), + feature!( + atom!("color-gamut"), + AllowsRanges::No, + keyword_evaluator!(eval_color_gamut, ColorGamut), + FeatureFlags::empty(), + ), + feature!( + atom!("prefers-reduced-motion"), + AllowsRanges::No, + keyword_evaluator!(eval_prefers_reduced_motion, PrefersReducedMotion), + FeatureFlags::empty(), + ), + feature!( + atom!("prefers-reduced-transparency"), + AllowsRanges::No, + keyword_evaluator!( + eval_prefers_reduced_transparency, + PrefersReducedTransparency + ), + FeatureFlags::empty(), + ), + feature!( + atom!("prefers-contrast"), + AllowsRanges::No, + keyword_evaluator!(eval_prefers_contrast, PrefersContrast), + // Note: by default this is only enabled in browser chrome and + // ua. It can be enabled on the web via the + // layout.css.prefers-contrast.enabled preference. See + // disabed_by_pref in media_feature_expression.rs for how that + // is done. + FeatureFlags::empty(), + ), + feature!( + atom!("forced-colors"), + AllowsRanges::No, + keyword_evaluator!(eval_forced_colors, ForcedColors), + FeatureFlags::empty(), + ), + feature!( + atom!("inverted-colors"), + AllowsRanges::No, + keyword_evaluator!(eval_inverted_colors, InvertedColors), + FeatureFlags::empty(), + ), + feature!( + atom!("overflow-block"), + AllowsRanges::No, + keyword_evaluator!(eval_overflow_block, OverflowBlock), + FeatureFlags::empty(), + ), + feature!( + atom!("overflow-inline"), + AllowsRanges::No, + keyword_evaluator!(eval_overflow_inline, OverflowInline), + FeatureFlags::empty(), + ), + feature!( + atom!("update"), + AllowsRanges::No, + keyword_evaluator!(eval_update, Update), + FeatureFlags::empty(), + ), + feature!( + atom!("prefers-color-scheme"), + AllowsRanges::No, + keyword_evaluator!(eval_prefers_color_scheme, PrefersColorScheme), + FeatureFlags::empty(), + ), + feature!( + atom!("dynamic-range"), + AllowsRanges::No, + keyword_evaluator!(eval_dynamic_range, DynamicRange), + FeatureFlags::empty(), + ), + feature!( + atom!("video-dynamic-range"), + AllowsRanges::No, + keyword_evaluator!(eval_video_dynamic_range, DynamicRange), + FeatureFlags::empty(), + ), + feature!( + atom!("scripting"), + AllowsRanges::No, + keyword_evaluator!(eval_scripting, Scripting), + FeatureFlags::empty(), + ), + // Evaluates to the preferred color scheme for content. Only useful in + // chrome context, where the chrome color-scheme and the content + // color-scheme might differ. + feature!( + atom!("-moz-content-prefers-color-scheme"), + AllowsRanges::No, + keyword_evaluator!(eval_content_prefers_color_scheme, PrefersColorScheme), + FeatureFlags::CHROME_AND_UA_ONLY, + ), + feature!( + atom!("pointer"), + AllowsRanges::No, + keyword_evaluator!(eval_pointer, Pointer), + FeatureFlags::empty(), + ), + feature!( + atom!("any-pointer"), + AllowsRanges::No, + keyword_evaluator!(eval_any_pointer, Pointer), + FeatureFlags::empty(), + ), + feature!( + atom!("hover"), + AllowsRanges::No, + keyword_evaluator!(eval_hover, Hover), + FeatureFlags::empty(), + ), + feature!( + atom!("any-hover"), + AllowsRanges::No, + keyword_evaluator!(eval_any_hover, Hover), + FeatureFlags::empty(), + ), + // Internal -moz-is-glyph media feature: applies only inside SVG glyphs. + // Internal because it is really only useful in the user agent anyway + // and therefore not worth standardizing. + feature!( + atom!("-moz-is-glyph"), + AllowsRanges::No, + Evaluator::BoolInteger(eval_moz_is_glyph), + FeatureFlags::CHROME_AND_UA_ONLY, + ), + feature!( + atom!("-moz-is-resource-document"), + AllowsRanges::No, + Evaluator::BoolInteger(eval_moz_is_resource_document), + FeatureFlags::CHROME_AND_UA_ONLY, + ), + feature!( + atom!("-moz-platform"), + AllowsRanges::No, + keyword_evaluator!(eval_moz_platform, Platform), + FeatureFlags::CHROME_AND_UA_ONLY, + ), + feature!( + atom!("-moz-gtk-theme-family"), + AllowsRanges::No, + keyword_evaluator!(eval_gtk_theme_family, GtkThemeFamily), + FeatureFlags::CHROME_AND_UA_ONLY, + ), + feature!( + atom!("-moz-print-preview"), + AllowsRanges::No, + Evaluator::BoolInteger(eval_moz_print_preview), + FeatureFlags::CHROME_AND_UA_ONLY, + ), + feature!( + atom!("-moz-overlay-scrollbars"), + AllowsRanges::No, + Evaluator::BoolInteger(eval_moz_overlay_scrollbars), + FeatureFlags::CHROME_AND_UA_ONLY, + ), + feature!( + atom!("-moz-bool-pref"), + AllowsRanges::No, + Evaluator::String(eval_moz_bool_pref), + FeatureFlags::CHROME_AND_UA_ONLY, + ), + lnf_int_feature!( + atom!("-moz-scrollbar-start-backward"), + ScrollArrowStyle, + get_scrollbar_start_backward + ), + lnf_int_feature!( + atom!("-moz-scrollbar-start-forward"), + ScrollArrowStyle, + get_scrollbar_start_forward + ), + lnf_int_feature!( + atom!("-moz-scrollbar-end-backward"), + ScrollArrowStyle, + get_scrollbar_end_backward + ), + lnf_int_feature!( + atom!("-moz-scrollbar-end-forward"), + ScrollArrowStyle, + get_scrollbar_end_forward + ), + lnf_int_feature!(atom!("-moz-menubar-drag"), MenuBarDrag), + lnf_int_feature!(atom!("-moz-mac-big-sur-theme"), MacBigSurTheme), + lnf_int_feature!(atom!("-moz-mac-rtl"), MacRTL), + lnf_int_feature!( + atom!("-moz-windows-accent-color-in-titlebar"), + WindowsAccentColorInTitlebar + ), + lnf_int_feature!(atom!("-moz-swipe-animation-enabled"), SwipeAnimationEnabled), + lnf_int_feature!(atom!("-moz-gtk-csd-available"), GTKCSDAvailable), + lnf_int_feature!(atom!("-moz-gtk-csd-minimize-button"), GTKCSDMinimizeButton), + lnf_int_feature!(atom!("-moz-gtk-csd-maximize-button"), GTKCSDMaximizeButton), + lnf_int_feature!(atom!("-moz-gtk-csd-close-button"), GTKCSDCloseButton), + lnf_int_feature!( + atom!("-moz-gtk-csd-reversed-placement"), + GTKCSDReversedPlacement + ), + lnf_int_feature!(atom!("-moz-system-dark-theme"), SystemUsesDarkTheme), + lnf_int_feature!(atom!("-moz-panel-animations"), PanelAnimations), +]; diff --git a/servo/components/style/gecko/media_queries.rs b/servo/components/style/gecko/media_queries.rs new file mode 100644 index 0000000000..ef156ab380 --- /dev/null +++ b/servo/components/style/gecko/media_queries.rs @@ -0,0 +1,593 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Gecko's media-query device and expression representation. + +use crate::color::AbsoluteColor; +use crate::context::QuirksMode; +use crate::custom_properties::CssEnvironment; +use crate::font_metrics::FontMetrics; +use crate::gecko::values::{convert_absolute_color_to_nscolor, convert_nscolor_to_absolute_color}; +use crate::gecko_bindings::bindings; +use crate::gecko_bindings::structs; +use crate::logical_geometry::WritingMode; +use crate::media_queries::MediaType; +use crate::properties::ComputedValues; +use crate::string_cache::Atom; +use crate::values::computed::font::GenericFontFamily; +use crate::values::computed::{ColorScheme, Length, NonNegativeLength}; +use crate::values::specified::color::SystemColor; +use crate::values::specified::font::{FONT_MEDIUM_LINE_HEIGHT_PX, FONT_MEDIUM_PX}; +use crate::values::specified::ViewportVariant; +use crate::values::{CustomIdent, KeyframesName}; +use app_units::{Au, AU_PER_PX}; +use euclid::default::Size2D; +use euclid::{Scale, SideOffsets2D}; +use servo_arc::Arc; +use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering}; +use std::{cmp, fmt}; +use style_traits::{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, + /// Line height of the root element, used for rlh units in other elements. + root_line_height: 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 root line-height + /// by using rlh units. + used_root_line_height: AtomicBool, + /// Whether any styles computed in the document relied on font metrics. + used_font_metrics: AtomicBool, + /// Whether any styles computed in the document relied on the viewport size + /// by using vw/vh/vmin/vmax units. + used_viewport_size: AtomicBool, + /// Whether any styles computed in the document relied on the viewport size + /// by using dvw/dvh/dvmin/dvmax units. + used_dynamic_viewport_size: AtomicBool, + /// The CssEnvironment object responsible of getting CSS environment + /// variables. + environment: CssEnvironment, +} + +impl fmt::Debug for Device { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use nsstring::nsCString; + + let mut doc_uri = nsCString::new(); + unsafe { + bindings::Gecko_nsIURI_Debug((*self.document()).mDocumentURI.raw(), &mut doc_uri) + }; + + f.debug_struct("Device") + .field("document_url", &doc_uri) + .finish() + } +} + +unsafe impl Sync for Device {} +unsafe impl Send for Device {} + +impl Device { + /// Trivially constructs a new `Device`. + pub fn new(document: *const structs::Document) -> Self { + assert!(!document.is_null()); + let doc = unsafe { &*document }; + let prefs = unsafe { &*bindings::Gecko_GetPrefSheetPrefs(doc) }; + Device { + document, + default_values: ComputedValues::default_values(doc), + root_font_size: AtomicU32::new(FONT_MEDIUM_PX.to_bits()), + root_line_height: AtomicU32::new(FONT_MEDIUM_LINE_HEIGHT_PX.to_bits()), + // This gets updated when we see the , so it doesn't really + // matter which color-scheme we look at here. + body_text_color: AtomicUsize::new(prefs.mLightColors.mDefault as usize), + used_root_font_size: AtomicBool::new(false), + used_root_line_height: AtomicBool::new(false), + used_font_metrics: AtomicBool::new(false), + used_viewport_size: AtomicBool::new(false), + used_dynamic_viewport_size: AtomicBool::new(false), + environment: CssEnvironment, + } + } + + /// Get the relevant environment to resolve `env()` functions. + #[inline] + pub fn environment(&self) -> &CssEnvironment { + &self.environment + } + + /// Returns the computed line-height for the font in a given computed values instance. + /// + /// If you pass down an element, then the used line-height is returned. + pub fn calc_line_height( + &self, + font: &crate::properties::style_structs::Font, + writing_mode: WritingMode, + element: Option, + ) -> NonNegativeLength { + let pres_context = self.pres_context(); + let line_height = font.clone_line_height(); + let au = Au(unsafe { + bindings::Gecko_CalcLineHeight( + &line_height, + pres_context.map_or(std::ptr::null(), |pc| pc), + writing_mode.is_text_vertical(), + &**font, + element.map_or(std::ptr::null(), |e| e.0), + ) + }); + NonNegativeLength::new(au.to_f32_px()) + } + + /// 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) + } + + /// Get the line height of the root element (for rlh) + pub fn root_line_height(&self) -> Length { + self.used_root_line_height.store(true, Ordering::Relaxed); + Length::new(f32::from_bits( + self.root_line_height.load(Ordering::Relaxed), + )) + } + + /// Set the line height of the root element (for rlh) + pub fn set_root_line_height(&self, size: Length) { + self.root_line_height + .store(size.px().to_bits(), Ordering::Relaxed); + } + + /// The quirks mode of the document. + pub fn quirks_mode(&self) -> QuirksMode { + self.document().mCompatMode.into() + } + + /// Sets the body text color for the "inherit color from body" quirk. + /// + /// + pub fn set_body_text_color(&self, color: AbsoluteColor) { + self.body_text_color.store( + convert_absolute_color_to_nscolor(&color) as usize, + Ordering::Relaxed, + ) + } + + /// Gets the base size given a generic font family and a language. + pub fn base_size_for_generic(&self, language: &Atom, generic: GenericFontFamily) -> Length { + unsafe { bindings::Gecko_GetBaseSize(self.document(), language.as_ptr(), generic) } + } + + /// Gets the size of the scrollbar in CSS pixels. + pub fn scrollbar_inline_size(&self) -> Length { + let pc = match self.pres_context() { + Some(pc) => pc, + // XXX: we could have a more reasonable default perhaps. + None => return Length::new(0.0), + }; + Length::new(unsafe { bindings::Gecko_GetScrollbarInlineSize(pc) }) + } + + /// Queries font metrics + pub fn query_font_metrics( + &self, + vertical: bool, + font: &crate::properties::style_structs::Font, + base_size: Length, + in_media_query: bool, + retrieve_math_scales: bool, + ) -> FontMetrics { + self.used_font_metrics.store(true, Ordering::Relaxed); + let pc = match self.pres_context() { + Some(pc) => pc, + None => return Default::default(), + }; + let gecko_metrics = unsafe { + bindings::Gecko_GetFontMetrics( + pc, + vertical, + &**font, + base_size, + // we don't use the user font set in a media query + !in_media_query, + retrieve_math_scales, + ) + }; + FontMetrics { + x_height: Some(gecko_metrics.mXSize), + zero_advance_measure: if gecko_metrics.mChSize.px() >= 0. { + Some(gecko_metrics.mChSize) + } else { + None + }, + cap_height: if gecko_metrics.mCapHeight.px() >= 0. { + Some(gecko_metrics.mCapHeight) + } else { + None + }, + ic_width: if gecko_metrics.mIcWidth.px() >= 0. { + Some(gecko_metrics.mIcWidth) + } else { + None + }, + ascent: gecko_metrics.mAscent, + script_percent_scale_down: if gecko_metrics.mScriptPercentScaleDown > 0. { + Some(gecko_metrics.mScriptPercentScaleDown) + } else { + None + }, + script_script_percent_scale_down: if gecko_metrics.mScriptScriptPercentScaleDown > 0. { + Some(gecko_metrics.mScriptScriptPercentScaleDown) + } else { + None + }, + } + } + + /// Returns the body text color. + pub fn body_text_color(&self) -> AbsoluteColor { + convert_nscolor_to_absolute_color(self.body_text_color.load(Ordering::Relaxed) as u32) + } + + /// Gets the document pointer. + #[inline] + pub fn document(&self) -> &structs::Document { + unsafe { &*self.document } + } + + /// Gets the pres context associated with this document. + #[inline] + pub fn pres_context(&self) -> Option<&structs::nsPresContext> { + unsafe { + self.document() + .mPresShell + .as_ref()? + .mPresContext + .mRawPtr + .as_ref() + } + } + + /// Gets the preference stylesheet prefs for our document. + #[inline] + pub fn pref_sheet_prefs(&self) -> &structs::PreferenceSheet_Prefs { + unsafe { &*bindings::Gecko_GetPrefSheetPrefs(self.document()) } + } + + /// Recreates the default computed values. + pub fn reset_computed_values(&mut self) { + self.default_values = ComputedValues::default_values(self.document()); + } + + /// Rebuild all the cached data. + pub fn rebuild_cached_data(&mut self) { + self.reset_computed_values(); + self.used_root_font_size.store(false, Ordering::Relaxed); + self.used_root_line_height.store(false, Ordering::Relaxed); + self.used_font_metrics.store(false, Ordering::Relaxed); + self.used_viewport_size.store(false, Ordering::Relaxed); + self.used_dynamic_viewport_size + .store(false, Ordering::Relaxed); + } + + /// Returns whether we ever looked up the root font size of the device. + pub fn used_root_font_size(&self) -> bool { + self.used_root_font_size.load(Ordering::Relaxed) + } + + /// Returns whether we ever looked up the root line-height of the device. + pub fn used_root_line_height(&self) -> bool { + self.used_root_line_height.load(Ordering::Relaxed) + } + + /// Recreates all the temporary state that the `Device` stores. + /// + /// This includes the viewport override from `@viewport` rules, and also the + /// default computed values. + pub fn reset(&mut self) { + self.reset_computed_values(); + } + + /// Returns whether this document is in print preview. + pub fn is_print_preview(&self) -> bool { + let pc = match self.pres_context() { + Some(pc) => pc, + None => return false, + }; + pc.mType == structs::nsPresContext_nsPresContextType_eContext_PrintPreview + } + + /// Returns the current media type of the device. + pub fn media_type(&self) -> MediaType { + let pc = match self.pres_context() { + Some(pc) => pc, + None => return MediaType::screen(), + }; + + // Gecko allows emulating random media with mMediaEmulationData.mMedium. + let medium_to_use = if !pc.mMediaEmulationData.mMedium.mRawPtr.is_null() { + pc.mMediaEmulationData.mMedium.mRawPtr + } else { + pc.mMedium as *const structs::nsAtom as *mut _ + }; + + MediaType(CustomIdent(unsafe { Atom::from_raw(medium_to_use) })) + } + + // It may make sense to account for @page rule margins here somehow, however + // it's not clear how that'd work, see: + // https://github.com/w3c/csswg-drafts/issues/5437 + fn page_size_minus_default_margin(&self, pc: &structs::nsPresContext) -> Size2D { + debug_assert!(pc.mIsRootPaginatedDocument() != 0); + let area = &pc.mPageSize; + let margin = &pc.mDefaultPageMargin; + let width = area.width - margin.left - margin.right; + let height = area.height - margin.top - margin.bottom; + Size2D::new(Au(cmp::max(width, 0)), Au(cmp::max(height, 0))) + } + + /// Returns the current viewport size in app units. + pub fn au_viewport_size(&self) -> Size2D { + let pc = match self.pres_context() { + Some(pc) => pc, + None => return Size2D::new(Au(0), Au(0)), + }; + + if pc.mIsRootPaginatedDocument() != 0 { + return self.page_size_minus_default_margin(pc); + } + + let area = &pc.mVisibleArea; + Size2D::new(Au(area.width), Au(area.height)) + } + + /// Returns the current viewport size in app units, recording that it's been + /// used for viewport unit resolution. + pub fn au_viewport_size_for_viewport_unit_resolution( + &self, + variant: ViewportVariant, + ) -> Size2D { + self.used_viewport_size.store(true, Ordering::Relaxed); + let pc = match self.pres_context() { + Some(pc) => pc, + None => return Size2D::new(Au(0), Au(0)), + }; + + if pc.mIsRootPaginatedDocument() != 0 { + return self.page_size_minus_default_margin(pc); + } + + match variant { + ViewportVariant::UADefault => { + let size = &pc.mSizeForViewportUnits; + Size2D::new(Au(size.width), Au(size.height)) + }, + ViewportVariant::Small => { + let size = &pc.mVisibleArea; + Size2D::new(Au(size.width), Au(size.height)) + }, + ViewportVariant::Large => { + let size = &pc.mVisibleArea; + // Looks like IntCoordTyped is treated as if it's u32 in Rust. + debug_assert!( + /* pc.mDynamicToolbarMaxHeight >=0 && */ + pc.mDynamicToolbarMaxHeight < i32::MAX as u32 + ); + Size2D::new( + Au(size.width), + Au(size.height + + pc.mDynamicToolbarMaxHeight as i32 * pc.mCurAppUnitsPerDevPixel), + ) + }, + ViewportVariant::Dynamic => { + self.used_dynamic_viewport_size + .store(true, Ordering::Relaxed); + let size = &pc.mVisibleArea; + // Looks like IntCoordTyped is treated as if it's u32 in Rust. + debug_assert!( + /* pc.mDynamicToolbarHeight >=0 && */ + pc.mDynamicToolbarHeight < i32::MAX as u32 + ); + Size2D::new( + Au(size.width), + Au(size.height + + (pc.mDynamicToolbarMaxHeight - pc.mDynamicToolbarHeight) as i32 * + pc.mCurAppUnitsPerDevPixel), + ) + }, + } + } + + /// Returns whether we ever looked up the viewport size of the Device. + pub fn used_viewport_size(&self) -> bool { + self.used_viewport_size.load(Ordering::Relaxed) + } + + /// Returns whether we ever looked up the dynamic viewport size of the Device. + pub fn used_dynamic_viewport_size(&self) -> bool { + self.used_dynamic_viewport_size.load(Ordering::Relaxed) + } + + /// Returns whether font metrics have been queried. + pub fn used_font_metrics(&self) -> bool { + self.used_font_metrics.load(Ordering::Relaxed) + } + + /// Returns whether visited styles are enabled. + pub fn visited_styles_enabled(&self) -> bool { + unsafe { bindings::Gecko_VisitedStylesEnabled(self.document()) } + } + + /// Returns the number of app units per device pixel we're using currently. + pub fn app_units_per_device_pixel(&self) -> i32 { + match self.pres_context() { + Some(pc) => pc.mCurAppUnitsPerDevPixel, + None => AU_PER_PX, + } + } + + /// Returns the device pixel ratio. + pub fn device_pixel_ratio(&self) -> Scale { + let pc = match self.pres_context() { + Some(pc) => pc, + None => return Scale::new(1.), + }; + + if pc.mMediaEmulationData.mDPPX > 0.0 { + return Scale::new(pc.mMediaEmulationData.mDPPX); + } + + let au_per_dpx = pc.mCurAppUnitsPerDevPixel as f32; + let au_per_px = AU_PER_PX as f32; + Scale::new(au_per_px / au_per_dpx) + } + + /// Returns whether document colors are enabled. + #[inline] + pub fn use_document_colors(&self) -> bool { + let doc = self.document(); + if doc.mIsBeingUsedAsImage() { + return true; + } + self.pref_sheet_prefs().mUseDocumentColors + } + + /// Computes a system color and returns it as an nscolor. + pub(crate) fn system_nscolor( + &self, + system_color: SystemColor, + color_scheme: &ColorScheme, + ) -> u32 { + unsafe { bindings::Gecko_ComputeSystemColor(system_color, self.document(), color_scheme) } + } + + /// Returns whether the used color-scheme for `color-scheme` should be dark. + pub(crate) fn is_dark_color_scheme(&self, color_scheme: &ColorScheme) -> bool { + unsafe { bindings::Gecko_IsDarkColorScheme(self.document(), color_scheme) } + } + + /// Returns the default background color. + /// + /// This is only for forced-colors/high-contrast, so looking at light colors + /// is ok. + pub fn default_background_color(&self) -> AbsoluteColor { + let normal = ColorScheme::normal(); + convert_nscolor_to_absolute_color(self.system_nscolor(SystemColor::Canvas, &normal)) + } + + /// Returns the default foreground color. + /// + /// See above for looking at light colors only. + pub fn default_color(&self) -> AbsoluteColor { + let normal = ColorScheme::normal(); + convert_nscolor_to_absolute_color(self.system_nscolor(SystemColor::Canvastext, &normal)) + } + + /// Returns the current effective text zoom. + #[inline] + fn text_zoom(&self) -> f32 { + let pc = match self.pres_context() { + Some(pc) => pc, + None => return 1., + }; + pc.mTextZoom + } + + /// Applies text zoom to a font-size or line-height value (see nsStyleFont::ZoomText). + #[inline] + pub fn zoom_text(&self, size: Length) -> Length { + size.scale_by(self.text_zoom()) + } + + /// Un-apply text zoom. + #[inline] + pub fn unzoom_text(&self, size: Length) -> Length { + size.scale_by(1. / self.text_zoom()) + } + + /// Returns safe area insets + pub fn safe_area_insets(&self) -> SideOffsets2D { + let pc = match self.pres_context() { + Some(pc) => pc, + None => return SideOffsets2D::zero(), + }; + let mut top = 0.0; + let mut right = 0.0; + let mut bottom = 0.0; + let mut left = 0.0; + unsafe { + bindings::Gecko_GetSafeAreaInsets(pc, &mut top, &mut right, &mut bottom, &mut left) + }; + SideOffsets2D::new(top, right, bottom, left) + } + + /// Returns true if the given MIME type is supported + pub fn is_supported_mime_type(&self, mime_type: &str) -> bool { + unsafe { + bindings::Gecko_IsSupportedImageMimeType(mime_type.as_ptr(), mime_type.len() as u32) + } + } + + /// Return whether the document is a chrome document. + /// + /// This check is consistent with how we enable chrome rules for chrome:// and resource:// + /// stylesheets (and thus chrome:// documents). + #[inline] + pub fn chrome_rules_enabled_for_document(&self) -> bool { + self.document().mChromeRulesEnabled() + } +} diff --git a/servo/components/style/gecko/mod.rs b/servo/components/style/gecko/mod.rs new file mode 100644 index 0000000000..c32ded14f3 --- /dev/null +++ b/servo/components/style/gecko/mod.rs @@ -0,0 +1,23 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Gecko-specific style-system bits. + +#[macro_use] +mod non_ts_pseudo_class_list; + +pub mod arc_types; +pub mod conversions; +pub mod data; +pub mod media_features; +pub mod media_queries; +pub mod pseudo_element; +pub mod restyle_damage; +pub mod selector_parser; +pub mod snapshot; +pub mod snapshot_helpers; +pub mod traversal; +pub mod url; +pub mod values; +pub mod wrapper; diff --git a/servo/components/style/gecko/non_ts_pseudo_class_list.rs b/servo/components/style/gecko/non_ts_pseudo_class_list.rs new file mode 100644 index 0000000000..cc7495dd9c --- /dev/null +++ b/servo/components/style/gecko/non_ts_pseudo_class_list.rs @@ -0,0 +1,106 @@ +/* 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-select-list-box", MozSelectListBox, _, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), + ("link", Link, UNVISITED, _), + ("any-link", AnyLink, VISITED_OR_UNVISITED, _), + ("visited", Visited, VISITED, _), + ("active", Active, ACTIVE, _), + ("autofill", Autofill, AUTOFILL, _), + ("checked", Checked, CHECKED, _), + ("defined", Defined, DEFINED, _), + ("disabled", Disabled, DISABLED, _), + ("enabled", Enabled, ENABLED, _), + ("focus", Focus, FOCUS, _), + ("focus-within", FocusWithin, FOCUS_WITHIN, _), + ("focus-visible", FocusVisible, FOCUSRING, _), + ("hover", Hover, HOVER, _), + ("-moz-drag-over", MozDragOver, DRAGOVER, _), + ("target", Target, URLTARGET, _), + ("indeterminate", Indeterminate, INDETERMINATE, _), + ("-moz-inert", MozInert, INERT, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), + ("-moz-devtools-highlighted", MozDevtoolsHighlighted, DEVTOOLS_HIGHLIGHTED, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), + ("-moz-styleeditor-transitioning", MozStyleeditorTransitioning, STYLEEDITOR_TRANSITIONING, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), + ("fullscreen", Fullscreen, FULLSCREEN, _), + ("modal", Modal, MODAL, _), + ("-moz-topmost-modal", MozTopmostModal, TOPMOST_MODAL, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), + ("-moz-broken", MozBroken, BROKEN, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS_AND_CHROME), + ("-moz-has-dir-attr", MozHasDirAttr, HAS_DIR_ATTR, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), + ("-moz-dir-attr-ltr", MozDirAttrLTR, HAS_DIR_ATTR_LTR, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), + ("-moz-dir-attr-rtl", MozDirAttrRTL, HAS_DIR_ATTR_RTL, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), + ("-moz-dir-attr-like-auto", MozDirAttrLikeAuto, HAS_DIR_ATTR_LIKE_AUTO, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), + + ("-moz-autofill-preview", MozAutofillPreview, AUTOFILL_PREVIEW, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS_AND_CHROME), + ("-moz-value-empty", MozValueEmpty, VALUE_EMPTY, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), + ("-moz-revealed", MozRevealed, REVEALED, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), + + ("-moz-math-increment-script-level", MozMathIncrementScriptLevel, INCREMENT_SCRIPT_LEVEL, _), + + ("required", Required, REQUIRED, _), + ("popover-open", PopoverOpen, POPOVER_OPEN, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS_AND_CHROME), + ("optional", Optional, OPTIONAL_, _), + ("valid", Valid, VALID, _), + ("invalid", Invalid, INVALID, _), + ("in-range", InRange, INRANGE, _), + ("out-of-range", OutOfRange, OUTOFRANGE, _), + ("default", Default, DEFAULT, _), + ("placeholder-shown", PlaceholderShown, PLACEHOLDER_SHOWN, _), + ("read-only", ReadOnly, READONLY, _), + ("read-write", ReadWrite, READWRITE, _), + ("user-valid", UserValid, USER_VALID, _), + ("user-invalid", UserInvalid, USER_INVALID, _), + ("-moz-meter-optimum", MozMeterOptimum, OPTIMUM, _), + ("-moz-meter-sub-optimum", MozMeterSubOptimum, SUB_OPTIMUM, _), + ("-moz-meter-sub-sub-optimum", MozMeterSubSubOptimum, SUB_SUB_OPTIMUM, _), + + ("-moz-first-node", MozFirstNode, _, _), + ("-moz-last-node", MozLastNode, _, _), + ("-moz-only-whitespace", MozOnlyWhitespace, _, _), + ("-moz-native-anonymous", MozNativeAnonymous, _, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), + ("-moz-placeholder", MozPlaceholder, _, _), + + // NOTE(emilio): Pseudo-classes below only depend on document state, and thus + // conceptually they should probably be media queries instead. + // + // However that has a set of trade-offs that might not be worth making. In + // particular, such media queries would prevent documents that match them from + // sharing user-agent stylesheets with documents that don't. Also, changes between + // media query results are more expensive than document state changes. So for now + // making them pseudo-classes is probably the right trade-off. + ("-moz-is-html", MozIsHTML, _, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), + ("-moz-lwtheme", MozLWTheme, _, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS_AND_CHROME), + ("-moz-window-inactive", MozWindowInactive, _, _), + ] + } + } +} diff --git a/servo/components/style/gecko/pseudo_element.rs b/servo/components/style/gecko/pseudo_element.rs new file mode 100644 index 0000000000..3bcd873455 --- /dev/null +++ b/servo/components/style/gecko/pseudo_element.rs @@ -0,0 +1,233 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Gecko's definition of a pseudo-element. +//! +//! Note that a few autogenerated bits of this live in +//! `pseudo_element_definition.mako.rs`. If you touch that file, you probably +//! need to update the checked-in files for Servo. + +use crate::gecko_bindings::structs::{self, PseudoStyleType}; +use crate::properties::longhands::display::computed_value::T as Display; +use crate::properties::{ComputedValues, PropertyFlags}; +use crate::selector_parser::{PseudoElementCascadeType, SelectorImpl}; +use crate::str::{starts_with_ignore_ascii_case, string_as_ascii_lowercase}; +use crate::string_cache::Atom; +use crate::values::serialize_atom_identifier; +use crate::values::AtomIdent; +use cssparser::ToCss; +use static_prefs::pref; +use std::fmt; + +include!(concat!( + env!("OUT_DIR"), + "/gecko/pseudo_element_definition.rs" +)); + +impl ::selectors::parser::PseudoElement for PseudoElement { + type Impl = SelectorImpl; + + // ::slotted() should support all tree-abiding pseudo-elements, see + // https://drafts.csswg.org/css-scoping/#slotted-pseudo + // https://drafts.csswg.org/css-pseudo-4/#treelike + #[inline] + fn valid_after_slotted(&self) -> bool { + matches!( + *self, + Self::Before | + Self::After | + Self::Marker | + Self::Placeholder | + Self::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 + } + + /// Gets the canonical index of this eagerly-cascaded pseudo-element. + #[inline] + pub fn eager_index(&self) -> usize { + EAGER_PSEUDOS + .iter() + .position(|p| p == self) + .expect("Not an eager pseudo") + } + + /// Creates a pseudo-element from an eager index. + #[inline] + pub fn from_eager_index(i: usize) -> Self { + EAGER_PSEUDOS[i].clone() + } + + /// Whether animations for the current pseudo element are stored in the + /// parent element. + #[inline] + pub fn animations_stored_in_parent(&self) -> bool { + matches!(*self, Self::Before | Self::After | Self::Marker) + } + + /// Whether the current pseudo element is ::before or ::after. + #[inline] + pub fn is_before_or_after(&self) -> bool { + matches!(*self, Self::Before | Self::After) + } + + /// Whether this pseudo-element is the ::before pseudo. + #[inline] + pub fn is_before(&self) -> bool { + *self == PseudoElement::Before + } + + /// Whether this pseudo-element is the ::after pseudo. + #[inline] + pub fn is_after(&self) -> bool { + *self == PseudoElement::After + } + + /// Whether this pseudo-element is the ::marker pseudo. + #[inline] + pub fn is_marker(&self) -> bool { + *self == PseudoElement::Marker + } + + /// Whether this pseudo-element is the ::selection pseudo. + #[inline] + pub fn is_selection(&self) -> bool { + *self == PseudoElement::Selection + } + + /// Whether this pseudo-element is ::first-letter. + #[inline] + pub fn is_first_letter(&self) -> bool { + *self == PseudoElement::FirstLetter + } + + /// Whether this pseudo-element is ::first-line. + #[inline] + pub fn is_first_line(&self) -> bool { + *self == PseudoElement::FirstLine + } + + /// Whether this pseudo-element is the ::-moz-color-swatch pseudo. + #[inline] + pub fn is_color_swatch(&self) -> bool { + *self == PseudoElement::MozColorSwatch + } + + /// Whether this pseudo-element is lazily-cascaded. + #[inline] + pub fn is_lazy(&self) -> bool { + !self.is_eager() && !self.is_precomputed() + } + + /// The identifier of the highlight this pseudo-element represents. + pub fn highlight_name(&self) -> Option<&AtomIdent> { + match *self { + Self::Highlight(ref name) => Some(name), + _ => None, + } + } + + /// Whether this pseudo-element is the ::highlight pseudo. + pub fn is_highlight(&self) -> bool { + matches!(*self, Self::Highlight(_)) + } + + /// Whether this pseudo-element supports user action selectors. + pub fn supports_user_action_state(&self) -> bool { + (self.flags() & structs::CSS_PSEUDO_ELEMENT_SUPPORTS_USER_ACTION_STATE) != 0 + } + + /// Whether this pseudo-element is enabled for all content. + pub fn enabled_in_content(&self) -> bool { + match *self { + Self::Highlight(..) => pref!("dom.customHighlightAPI.enabled"), + Self::SliderFill | Self::SliderTrack | Self::SliderThumb => { + pref!("layout.css.modern-range-pseudos.enabled") + }, + // If it's not explicitly enabled in UA sheets or chrome, then we're enabled for + // content. + _ => (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..48f0618502 --- /dev/null +++ b/servo/components/style/gecko/pseudo_element_definition.mako.rs @@ -0,0 +1,278 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +/// Gecko's pseudo-element definition. +/// +/// We intentionally double-box legacy ::-moz-tree pseudo-elements to keep the +/// size of PseudoElement (and thus selector components) small. +#[derive(Clone, Debug, Eq, Hash, MallocSizeOf, PartialEq, ToShmem)] +pub enum PseudoElement { + % for pseudo in PSEUDOS: + /// ${pseudo.value} + % if pseudo.is_tree_pseudo_element(): + ${pseudo.capitalized_pseudo()}(thin_vec::ThinVec), + % elif pseudo.pseudo_ident == "highlight": + ${pseudo.capitalized_pseudo()}(AtomIdent), + % else: + ${pseudo.capitalized_pseudo()}, + % endif + % endfor + /// ::-webkit-* that we don't recognize + /// https://github.com/whatwg/compat/issues/103 + UnknownWebkit(Atom), +} + +/// Important: If you change this, you should also update Gecko's +/// nsCSSPseudoElements::IsEagerlyCascadedInServo. +<% EAGER_PSEUDOS = ["Before", "After", "FirstLine", "FirstLetter"] %> +<% TREE_PSEUDOS = [pseudo for pseudo in PSEUDOS if pseudo.is_tree_pseudo_element()] %> +<% SIMPLE_PSEUDOS = [pseudo for pseudo in PSEUDOS if pseudo.is_simple_pseudo_element()] %> + +/// The number of eager pseudo-elements. +pub const EAGER_PSEUDO_COUNT: usize = ${len(EAGER_PSEUDOS)}; + +/// The number of non-functional pseudo-elements. +pub const SIMPLE_PSEUDO_COUNT: usize = ${len(SIMPLE_PSEUDOS)}; + +/// The number of tree pseudo-elements. +pub const TREE_PSEUDO_COUNT: usize = ${len(TREE_PSEUDOS)}; + +/// The number of all pseudo-elements. +pub const PSEUDO_COUNT: usize = ${len(PSEUDOS)}; + +/// The list of eager pseudos. +pub const EAGER_PSEUDOS: [PseudoElement; EAGER_PSEUDO_COUNT] = [ + % for eager_pseudo_name in EAGER_PSEUDOS: + PseudoElement::${eager_pseudo_name}, + % endfor +]; + +<%def name="pseudo_element_variant(pseudo, tree_arg='..')">\ +PseudoElement::${pseudo.capitalized_pseudo()}${"({})".format(tree_arg) if not pseudo.is_simple_pseudo_element() else ""}\ + + +impl PseudoElement { + /// Returns an index of the pseudo-element. + #[inline] + pub fn index(&self) -> usize { + match *self { + % for i, pseudo in enumerate(PSEUDOS): + ${pseudo_element_variant(pseudo)} => ${i}, + % endfor + PseudoElement::UnknownWebkit(..) => unreachable!(), + } + } + + /// Returns an array of `None` values. + /// + /// FIXME(emilio): Integer generics can't come soon enough. + pub fn pseudo_none_array() -> [Option; PSEUDO_COUNT] { + [ + ${",\n ".join(["None" for pseudo in PSEUDOS])} + ] + } + + /// Whether this pseudo-element is an anonymous box. + #[inline] + pub fn is_anon_box(&self) -> bool { + match *self { + % for pseudo in PSEUDOS: + % if pseudo.is_anon_box(): + ${pseudo_element_variant(pseudo)} => true, + % endif + % endfor + _ => false, + } + } + + /// Whether this pseudo-element is eagerly-cascaded. + #[inline] + pub fn is_eager(&self) -> bool { + matches!(*self, + ${" | ".join(map(lambda name: "PseudoElement::{}".format(name), EAGER_PSEUDOS))}) + } + + /// Whether this pseudo-element is tree pseudo-element. + #[inline] + pub fn is_tree_pseudo_element(&self) -> bool { + match *self { + % for pseudo in TREE_PSEUDOS: + ${pseudo_element_variant(pseudo)} => true, + % endfor + _ => false, + } + } + + /// Whether this pseudo-element is an unknown Webkit-prefixed pseudo-element. + #[inline] + pub fn is_unknown_webkit_pseudo_element(&self) -> bool { + matches!(*self, PseudoElement::UnknownWebkit(..)) + } + + /// Gets the flags associated to this pseudo-element, or 0 if it's an + /// anonymous box. + pub fn flags(&self) -> u32 { + match *self { + % for pseudo in PSEUDOS: + ${pseudo_element_variant(pseudo)} => + % if pseudo.is_tree_pseudo_element(): + structs::CSS_PSEUDO_ELEMENT_ENABLED_IN_UA_SHEETS_AND_CHROME, + % elif pseudo.is_anon_box(): + structs::CSS_PSEUDO_ELEMENT_ENABLED_IN_UA_SHEETS, + % else: + structs::SERVO_CSS_PSEUDO_ELEMENT_FLAGS_${pseudo.pseudo_ident}, + % endif + % endfor + PseudoElement::UnknownWebkit(..) => 0, + } + } + + /// Construct a pseudo-element from a `PseudoStyleType`. + #[inline] + pub fn from_pseudo_type(type_: PseudoStyleType, functional_pseudo_parameter: Option) -> Option { + match type_ { + % for pseudo in PSEUDOS: + % if pseudo.is_simple_pseudo_element(): + PseudoStyleType::${pseudo.pseudo_ident} => { + debug_assert!(functional_pseudo_parameter.is_none()); + Some(${pseudo_element_variant(pseudo)}) + }, + % endif + % endfor + PseudoStyleType::highlight => { + match functional_pseudo_parameter { + Some(p) => Some(PseudoElement::Highlight(p)), + None => None + } + } + _ => None, + } + } + + /// Construct a `PseudoStyleType` from a pseudo-element + #[inline] + pub fn pseudo_type(&self) -> PseudoStyleType { + match *self { + % for pseudo in PSEUDOS: + % if pseudo.is_tree_pseudo_element(): + PseudoElement::${pseudo.capitalized_pseudo()}(..) => PseudoStyleType::XULTree, + % elif pseudo.pseudo_ident == "highlight": + PseudoElement::${pseudo.capitalized_pseudo()}(..) => PseudoStyleType::${pseudo.pseudo_ident}, + % else: + PseudoElement::${pseudo.capitalized_pseudo()} => PseudoStyleType::${pseudo.pseudo_ident}, + % endif + % endfor + PseudoElement::UnknownWebkit(..) => unreachable!(), + } + } + + /// Get the argument list of a tree pseudo-element. + #[inline] + pub fn tree_pseudo_args(&self) -> Option<<&[Atom]> { + match *self { + % for pseudo in TREE_PSEUDOS: + PseudoElement::${pseudo.capitalized_pseudo()}(ref args) => Some(args), + % endfor + _ => None, + } + } + + /// Construct a tree pseudo-element from atom and args. + #[inline] + pub fn from_tree_pseudo_atom(atom: &Atom, args: Box<[Atom]>) -> Option { + % for pseudo in PSEUDOS: + % if pseudo.is_tree_pseudo_element(): + if atom == &atom!("${pseudo.value}") { + return Some(PseudoElement::${pseudo.capitalized_pseudo()}(args.into())); + } + % endif + % endfor + None + } + + /// Constructs a pseudo-element from a string of text. + /// + /// Returns `None` if the pseudo-element is not recognised. + #[inline] + pub fn from_slice(name: &str, allow_unkown_webkit: bool) -> Option { + // We don't need to support tree pseudos because functional + // pseudo-elements needs arguments, and thus should be created + // via other methods. + ascii_case_insensitive_phf_map! { + pseudo -> PseudoElement = { + % for pseudo in SIMPLE_PSEUDOS: + "${pseudo.value[1:]}" => ${pseudo_element_variant(pseudo)}, + % endfor + // Alias some legacy prefixed pseudos to their standardized name at parse time: + "-moz-selection" => PseudoElement::Selection, + "-moz-placeholder" => PseudoElement::Placeholder, + "-moz-list-bullet" => PseudoElement::Marker, + "-moz-list-number" => PseudoElement::Marker, + } + } + if let Some(p) = pseudo::get(name) { + return Some(p.clone()); + } + if starts_with_ignore_ascii_case(name, "-moz-tree-") { + return PseudoElement::tree_pseudo_element(name, Default::default()) + } + const WEBKIT_PREFIX: &str = "-webkit-"; + if allow_unkown_webkit && starts_with_ignore_ascii_case(name, WEBKIT_PREFIX) { + let part = string_as_ascii_lowercase(&name[WEBKIT_PREFIX.len()..]); + return Some(PseudoElement::UnknownWebkit(part.into())); + } + None + } + + /// Constructs a tree pseudo-element from the given name and arguments. + /// "name" must start with "-moz-tree-". + /// + /// Returns `None` if the pseudo-element is not recognized. + #[inline] + pub fn tree_pseudo_element(name: &str, args: thin_vec::ThinVec) -> Option { + debug_assert!(starts_with_ignore_ascii_case(name, "-moz-tree-")); + let tree_part = &name[10..]; + % for pseudo in TREE_PSEUDOS: + if tree_part.eq_ignore_ascii_case("${pseudo.value[11:]}") { + return Some(${pseudo_element_variant(pseudo, "args")}); + } + % endfor + None + } +} + +impl ToCss for PseudoElement { + fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write { + dest.write_char(':')?; + match *self { + % for pseudo in (p for p in PSEUDOS if p.pseudo_ident != "highlight"): + ${pseudo_element_variant(pseudo)} => dest.write_str("${pseudo.value}")?, + % endfor + PseudoElement::Highlight(ref name) => { + dest.write_str(":highlight(")?; + serialize_atom_identifier(name, dest)?; + dest.write_char(')')?; + } + PseudoElement::UnknownWebkit(ref atom) => { + dest.write_str(":-webkit-")?; + serialize_atom_identifier(atom, dest)?; + } + } + if let Some(args) = self.tree_pseudo_args() { + if !args.is_empty() { + dest.write_char('(')?; + let mut iter = args.iter(); + if let Some(first) = iter.next() { + serialize_atom_identifier(&first, dest)?; + for item in iter { + dest.write_str(", ")?; + serialize_atom_identifier(item, dest)?; + } + } + dest.write_char(')')?; + } + } + Ok(()) + } +} diff --git a/servo/components/style/gecko/regen_atoms.py b/servo/components/style/gecko/regen_atoms.py new file mode 100755 index 0000000000..61f2fc4c63 --- /dev/null +++ b/servo/components/style/gecko/regen_atoms.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python + +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +import re +import os +import sys + +from io import BytesIO + +GECKO_DIR = os.path.dirname(__file__.replace("\\", "/")) +sys.path.insert(0, os.path.join(os.path.dirname(GECKO_DIR), "properties")) + +import build + + +# Matches lines like `GK_ATOM(foo, "foo", 0x12345678, true, nsStaticAtom, PseudoElementAtom)`. +PATTERN = re.compile( + '^GK_ATOM\(([^,]*),[^"]*"([^"]*)",\s*(0x[0-9a-f]+),\s*[^,]*,\s*([^,]*),\s*([^)]*)\)', + re.MULTILINE, +) +FILE = "include/nsGkAtomList.h" + + +def map_atom(ident): + if ident in { + "box", + "loop", + "match", + "mod", + "ref", + "self", + "type", + "use", + "where", + "in", + }: + return ident + "_" + return ident + + +class Atom: + def __init__(self, ident, value, hash, ty, atom_type): + self.ident = "nsGkAtoms_{}".format(ident) + self.original_ident = ident + self.value = value + self.hash = hash + # The Gecko type: "nsStaticAtom", "nsCSSPseudoElementStaticAtom", or + # "nsAnonBoxPseudoStaticAtom". + self.ty = ty + # The type of atom: "Atom", "PseudoElement", "NonInheritingAnonBox", + # or "InheritingAnonBox". + self.atom_type = atom_type + + if ( + self.is_pseudo_element() + or self.is_anon_box() + or self.is_tree_pseudo_element() + ): + self.pseudo_ident = (ident.split("_", 1))[1] + + if self.is_anon_box(): + assert self.is_inheriting_anon_box() or self.is_non_inheriting_anon_box() + + def type(self): + return self.ty + + def capitalized_pseudo(self): + return self.pseudo_ident[0].upper() + self.pseudo_ident[1:] + + def is_pseudo_element(self): + return self.atom_type == "PseudoElementAtom" + + def is_anon_box(self): + if self.is_tree_pseudo_element(): + return False + return self.is_non_inheriting_anon_box() or self.is_inheriting_anon_box() + + def is_non_inheriting_anon_box(self): + assert not self.is_tree_pseudo_element() + return self.atom_type == "NonInheritingAnonBoxAtom" + + def is_inheriting_anon_box(self): + if self.is_tree_pseudo_element(): + return False + return self.atom_type == "InheritingAnonBoxAtom" + + def is_tree_pseudo_element(self): + return self.value.startswith(":-moz-tree-") + + def is_simple_pseudo_element(self) -> bool: + return not (self.is_tree_pseudo_element() or self.pseudo_ident == "highlight") + + +def collect_atoms(objdir): + atoms = [] + path = os.path.abspath(os.path.join(objdir, FILE)) + print("cargo:rerun-if-changed={}".format(path)) + with open(path) as f: + content = f.read() + for result in PATTERN.finditer(content): + atoms.append( + Atom( + result.group(1), + result.group(2), + result.group(3), + result.group(4), + result.group(5), + ) + ) + return atoms + + +class FileAvoidWrite(BytesIO): + """File-like object that buffers output and only writes if content changed.""" + + def __init__(self, filename): + BytesIO.__init__(self) + self.name = filename + + def write(self, buf): + if isinstance(buf, str): + buf = buf.encode("utf-8") + BytesIO.write(self, buf) + + def close(self): + buf = self.getvalue() + BytesIO.close(self) + try: + with open(self.name, "rb") as f: + old_content = f.read() + if old_content == buf: + print("{} is not changed, skip".format(self.name)) + return + except IOError: + pass + with open(self.name, "wb") as f: + f.write(buf) + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + if not self.closed: + self.close() + + +PRELUDE = """ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +// Autogenerated file created by components/style/gecko/regen_atoms.py. +// DO NOT EDIT DIRECTLY +"""[ + 1: +] + +RULE_TEMPLATE = """ + ("{atom}") => {{{{ + #[allow(unsafe_code)] #[allow(unused_unsafe)] + unsafe {{ $crate::string_cache::Atom::from_index_unchecked({index}) }} + }}}}; +"""[ + 1: +] + +MACRO_TEMPLATE = """ +/// Returns a static atom by passing the literal string it represents. +#[macro_export] +macro_rules! atom {{ +{body}\ +}} +""" + + +def write_atom_macro(atoms, file_name): + with FileAvoidWrite(file_name) as f: + f.write(PRELUDE) + macro_rules = [ + RULE_TEMPLATE.format(atom=atom.value, name=atom.ident, index=i) + for (i, atom) in enumerate(atoms) + ] + f.write(MACRO_TEMPLATE.format(body="".join(macro_rules))) + + +def write_pseudo_elements(atoms, target_filename): + pseudos = [] + for atom in atoms: + if ( + atom.type() == "nsCSSPseudoElementStaticAtom" + or atom.type() == "nsCSSAnonBoxPseudoStaticAtom" + ): + pseudos.append(atom) + + pseudo_definition_template = os.path.join( + GECKO_DIR, "pseudo_element_definition.mako.rs" + ) + print("cargo:rerun-if-changed={}".format(pseudo_definition_template)) + contents = build.render(pseudo_definition_template, PSEUDOS=pseudos) + + with FileAvoidWrite(target_filename) as f: + f.write(contents) + + +def generate_atoms(dist, out): + atoms = collect_atoms(dist) + write_atom_macro(atoms, os.path.join(out, "atom_macro.rs")) + write_pseudo_elements(atoms, os.path.join(out, "pseudo_element_definition.rs")) + + +if __name__ == "__main__": + if len(sys.argv) != 3: + print("Usage: {} dist out".format(sys.argv[0])) + exit(2) + generate_atoms(sys.argv[1], sys.argv[2]) diff --git a/servo/components/style/gecko/restyle_damage.rs b/servo/components/style/gecko/restyle_damage.rs new file mode 100644 index 0000000000..4749daea18 --- /dev/null +++ b/servo/components/style/gecko/restyle_damage.rs @@ -0,0 +1,121 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Gecko's restyle damage computation (aka change hints, aka `nsChangeHint`). + +use crate::gecko_bindings::bindings; +use crate::gecko_bindings::structs; +use crate::gecko_bindings::structs::nsChangeHint; +use crate::matching::{StyleChange, StyleDifference}; +use crate::properties::ComputedValues; +use std::ops::{BitAnd, BitOr, BitOrAssign, Not}; + +/// The representation of Gecko's restyle damage is just a wrapper over +/// `nsChangeHint`. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct GeckoRestyleDamage(nsChangeHint); + +impl GeckoRestyleDamage { + /// Trivially construct a new `GeckoRestyleDamage`. + #[inline] + pub fn new(raw: nsChangeHint) -> Self { + GeckoRestyleDamage(raw) + } + + /// Get the inner change hint for this damage. + #[inline] + pub fn as_change_hint(&self) -> nsChangeHint { + self.0 + } + + /// Get an empty change hint, that is (`nsChangeHint(0)`). + #[inline] + pub fn empty() -> Self { + GeckoRestyleDamage(nsChangeHint(0)) + } + + /// Returns whether this restyle damage represents the empty damage. + #[inline] + pub fn is_empty(&self) -> bool { + self.0 == nsChangeHint(0) + } + + /// Computes the `StyleDifference` (including the appropriate change hint) + /// given an old and a new style. + pub fn compute_style_difference( + old_style: &ComputedValues, + new_style: &ComputedValues, + ) -> StyleDifference { + let mut any_style_changed = false; + let mut reset_only = false; + let hint = unsafe { + bindings::Gecko_CalcStyleDifference( + old_style.as_gecko_computed_style(), + new_style.as_gecko_computed_style(), + &mut any_style_changed, + &mut reset_only, + ) + }; + if reset_only && !old_style.custom_properties_equal(new_style) { + // The Gecko_CalcStyleDifference call only checks the non-custom + // property structs, so we check the custom properties here. Since + // they generate no damage themselves, we can skip this check if we + // already know we had some inherited (regular) property + // differences. + any_style_changed = true; + reset_only = false; + } + let change = if any_style_changed { + StyleChange::Changed { reset_only } + } else { + StyleChange::Unchanged + }; + let damage = GeckoRestyleDamage(nsChangeHint(hint)); + StyleDifference { damage, change } + } + + /// Returns true if this restyle damage contains all the damage of |other|. + pub fn contains(self, other: Self) -> bool { + self & other == other + } + + /// Gets restyle damage to reconstruct the entire frame, subsuming all + /// other damage. + pub fn reconstruct() -> Self { + GeckoRestyleDamage(structs::nsChangeHint::nsChangeHint_ReconstructFrame) + } +} + +impl Default for GeckoRestyleDamage { + fn default() -> Self { + Self::empty() + } +} + +impl BitOr for GeckoRestyleDamage { + type Output = Self; + fn bitor(self, other: Self) -> Self { + GeckoRestyleDamage(self.0 | other.0) + } +} + +impl BitOrAssign for GeckoRestyleDamage { + fn bitor_assign(&mut self, other: Self) { + *self = *self | other; + } +} + +impl BitAnd for GeckoRestyleDamage { + type Output = Self; + fn bitand(self, other: Self) -> Self { + GeckoRestyleDamage(nsChangeHint((self.0).0 & (other.0).0)) + } +} + +impl Not for GeckoRestyleDamage { + type Output = Self; + fn not(self) -> Self { + GeckoRestyleDamage(nsChangeHint(!(self.0).0)) + } +} diff --git a/servo/components/style/gecko/selector_parser.rs b/servo/components/style/gecko/selector_parser.rs new file mode 100644 index 0000000000..203e6a3609 --- /dev/null +++ b/servo/components/style/gecko/selector_parser.rs @@ -0,0 +1,519 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Gecko-specific bits for selector-parsing. + +use crate::computed_value_flags::ComputedValueFlags; +use crate::invalidation::element::document_state::InvalidationMatchingData; +use crate::properties::ComputedValues; +use crate::selector_parser::{Direction, HorizontalDirection, SelectorParser}; +use crate::str::starts_with_ignore_ascii_case; +use crate::string_cache::{Atom, Namespace, WeakAtom, WeakNamespace}; +use crate::values::{AtomIdent, AtomString}; +use cssparser::{BasicParseError, BasicParseErrorKind, Parser}; +use cssparser::{CowRcStr, SourceLocation, ToCss, Token}; +use dom::{DocumentState, ElementState}; +use selectors::parser::SelectorParseErrorKind; +use std::fmt; +use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss as ToCss_}; +use thin_vec::ThinVec; + +pub use crate::gecko::pseudo_element::{ + PseudoElement, EAGER_PSEUDOS, EAGER_PSEUDO_COUNT, PSEUDO_COUNT, +}; +pub use crate::gecko::snapshot::SnapshotMap; + +bitflags! { + // See NonTSPseudoClass::is_enabled_in() + #[derive(Copy, Clone)] + struct NonTSPseudoClassFlag: u8 { + const PSEUDO_CLASS_ENABLED_IN_UA_SHEETS = 1 << 0; + const PSEUDO_CLASS_ENABLED_IN_CHROME = 1 << 1; + const PSEUDO_CLASS_ENABLED_IN_UA_SHEETS_AND_CHROME = + NonTSPseudoClassFlag::PSEUDO_CLASS_ENABLED_IN_UA_SHEETS.bits() | + NonTSPseudoClassFlag::PSEUDO_CLASS_ENABLED_IN_CHROME.bits(); + } +} + +/// The type used to store the language argument to the `:lang` pseudo-class. +#[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, ToCss, ToShmem)] +#[css(comma)] +pub struct Lang(#[css(iterable)] pub ThinVec); + +/// The type used to store the state argument to the `:state` pseudo-class. +#[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, ToCss, ToShmem)] +pub struct CustomState(pub 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 :state` pseudo-class. + CustomState(CustomState), + /// The non-standard `:-moz-locale-dir` pseudo-class. + MozLocaleDir(Direction), + } + } +} +apply_non_ts_list!(pseudo_class_name); + +impl ToCss for NonTSPseudoClass { + fn to_css(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + macro_rules! pseudo_class_serialize { + ([$(($css:expr, $name:ident, $state:tt, $flags:tt),)*]) => { + match *self { + $(NonTSPseudoClass::$name => concat!(":", $css),)* + NonTSPseudoClass::Lang(ref lang) => { + dest.write_str(":lang(")?; + lang.to_css(&mut CssWriter::new(dest))?; + return dest.write_char(')'); + }, + NonTSPseudoClass::CustomState(ref state) => { + dest.write_str(":state(")?; + state.to_css(&mut CssWriter::new(dest))?; + return dest.write_char(')'); + }, + NonTSPseudoClass::MozLocaleDir(ref dir) => { + dest.write_str(":-moz-locale-dir(")?; + dir.to_css(&mut CssWriter::new(dest))?; + return dest.write_char(')') + }, + NonTSPseudoClass::Dir(ref dir) => { + dest.write_str(":dir(")?; + dir.to_css(&mut CssWriter::new(dest))?; + return dest.write_char(')') + }, + } + } + } + let ser = apply_non_ts_list!(pseudo_class_serialize); + dest.write_str(ser) + } +} + +impl NonTSPseudoClass { + /// Parses the name and returns a non-ts-pseudo-class if succeeds. + /// None otherwise. It doesn't check whether the pseudo-class is enabled + /// in a particular state. + pub fn parse_non_functional(name: &str) -> Option { + macro_rules! pseudo_class_parse { + ([$(($css:expr, $name:ident, $state:tt, $flags:tt),)*]) => { + match_ignore_ascii_case! { &name, + $($css => Some(NonTSPseudoClass::$name),)* + "-moz-full-screen" => Some(NonTSPseudoClass::Fullscreen), + "-moz-read-only" => Some(NonTSPseudoClass::ReadOnly), + "-moz-read-write" => Some(NonTSPseudoClass::ReadWrite), + "-moz-focusring" => Some(NonTSPseudoClass::FocusVisible), + "-moz-ui-valid" => Some(NonTSPseudoClass::UserValid), + "-moz-ui-invalid" => Some(NonTSPseudoClass::UserInvalid), + "-webkit-autofill" => Some(NonTSPseudoClass::Autofill), + _ => None, + } + } + } + apply_non_ts_list!(pseudo_class_parse) + } + + /// Returns true if this pseudo-class has any of the given flags set. + fn has_any_flag(&self, flags: NonTSPseudoClassFlag) -> bool { + macro_rules! check_flag { + (_) => { + false + }; + ($flags:ident) => { + NonTSPseudoClassFlag::$flags.intersects(flags) + }; + } + macro_rules! pseudo_class_check_is_enabled_in { + ([$(($css:expr, $name:ident, $state:tt, $flags:tt),)*]) => { + match *self { + $(NonTSPseudoClass::$name => check_flag!($flags),)* + NonTSPseudoClass::MozLocaleDir(_) => check_flag!(PSEUDO_CLASS_ENABLED_IN_UA_SHEETS_AND_CHROME), + NonTSPseudoClass::CustomState(_) | + NonTSPseudoClass::Lang(_) | + NonTSPseudoClass::Dir(_) => false, + } + } + } + apply_non_ts_list!(pseudo_class_check_is_enabled_in) + } + + /// Returns whether the pseudo-class is enabled in content sheets. + #[inline] + fn is_enabled_in_content(&self) -> bool { + if matches!(*self, Self::PopoverOpen) { + return static_prefs::pref!("dom.element.popover.enabled"); + } + if matches!(*self, Self::CustomState(_)) { + return static_prefs::pref!("dom.element.customstateset.enabled"); + } + !self.has_any_flag(NonTSPseudoClassFlag::PSEUDO_CLASS_ENABLED_IN_UA_SHEETS_AND_CHROME) + } + + /// Get the state flag associated with a pseudo-class, if any. + pub fn state_flag(&self) -> ElementState { + macro_rules! flag { + (_) => { + ElementState::empty() + }; + ($state:ident) => { + ElementState::$state + }; + } + macro_rules! pseudo_class_state { + ([$(($css:expr, $name:ident, $state:tt, $flags:tt),)*]) => { + match *self { + $(NonTSPseudoClass::$name => flag!($state),)* + NonTSPseudoClass::Dir(ref dir) => dir.element_state(), + NonTSPseudoClass::MozLocaleDir(..) | + NonTSPseudoClass::CustomState(..) | + NonTSPseudoClass::Lang(..) => ElementState::empty(), + } + } + } + apply_non_ts_list!(pseudo_class_state) + } + + /// Get the document state flag associated with a pseudo-class, if any. + pub fn document_state_flag(&self) -> DocumentState { + match *self { + NonTSPseudoClass::MozLocaleDir(ref dir) => match dir.as_horizontal_direction() { + Some(HorizontalDirection::Ltr) => DocumentState::LTR_LOCALE, + Some(HorizontalDirection::Rtl) => DocumentState::RTL_LOCALE, + None => DocumentState::empty(), + }, + NonTSPseudoClass::MozWindowInactive => DocumentState::WINDOW_INACTIVE, + NonTSPseudoClass::MozLWTheme => DocumentState::LWTHEME, + _ => DocumentState::empty(), + } + } + + /// Returns true if the given pseudoclass should trigger style sharing cache + /// revalidation. + pub fn needs_cache_revalidation(&self) -> bool { + self.state_flag().is_empty() && + !matches!( + *self, + // :dir() depends on state only, but may have an empty state_flag for invalid + // arguments. + NonTSPseudoClass::Dir(_) | + // We prevent style sharing for NAC. + NonTSPseudoClass::MozNativeAnonymous | + // :-moz-placeholder is parsed but never matches. + NonTSPseudoClass::MozPlaceholder | + // :-moz-is-html, :-moz-lwtheme, :-moz-locale-dir and :-moz-window-inactive + // depend only on the state of the document, which is invariant across all + // elements involved in a given style cache. + NonTSPseudoClass::MozIsHTML | + NonTSPseudoClass::MozLWTheme | + NonTSPseudoClass::MozLocaleDir(_) | + NonTSPseudoClass::MozWindowInactive + ) + } +} + +impl ::selectors::parser::NonTSPseudoClass for NonTSPseudoClass { + type Impl = SelectorImpl; + + #[inline] + fn is_active_or_hover(&self) -> bool { + matches!(*self, NonTSPseudoClass::Active | NonTSPseudoClass::Hover) + } + + /// We intentionally skip the link-related ones. + #[inline] + fn is_user_action_state(&self) -> bool { + matches!( + *self, + NonTSPseudoClass::Hover | NonTSPseudoClass::Active | NonTSPseudoClass::Focus + ) + } +} + +/// The dummy struct we use to implement our selector parsing. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct SelectorImpl; + +/// A set of extra data to carry along with the matching context, either for +/// selector-matching or invalidation. +#[derive(Default)] +pub struct ExtraMatchingData<'a> { + /// The invalidation data to invalidate doc-state pseudo-classes correctly. + pub invalidation_data: InvalidationMatchingData, + + /// The invalidation bits from matching container queries. These are here + /// just for convenience mostly. + pub cascade_input_flags: ComputedValueFlags, + + /// The style of the originating element in order to evaluate @container + /// size queries affecting pseudo-elements. + pub originating_element_style: Option<&'a ComputedValues>, +} + +impl ::selectors::SelectorImpl for SelectorImpl { + type ExtraMatchingData<'a> = ExtraMatchingData<'a>; + type AttrValue = AtomString; + type Identifier = AtomIdent; + type LocalName = AtomIdent; + type NamespacePrefix = AtomIdent; + type NamespaceUrl = Namespace; + type BorrowedNamespaceUrl = WeakNamespace; + type BorrowedLocalName = WeakAtom; + + type PseudoElement = PseudoElement; + type NonTSPseudoClass = NonTSPseudoClass; + + fn should_collect_attr_hash(name: &AtomIdent) -> bool { + !crate::bloom::is_attr_name_excluded_from_filter(name) + } +} + +impl<'a> SelectorParser<'a> { + fn is_pseudo_class_enabled(&self, pseudo_class: &NonTSPseudoClass) -> bool { + if pseudo_class.is_enabled_in_content() { + return true; + } + + if self.in_user_agent_stylesheet() && + pseudo_class.has_any_flag(NonTSPseudoClassFlag::PSEUDO_CLASS_ENABLED_IN_UA_SHEETS) + { + return true; + } + + if self.chrome_rules_enabled() && + pseudo_class.has_any_flag(NonTSPseudoClassFlag::PSEUDO_CLASS_ENABLED_IN_CHROME) + { + return true; + } + + if matches!(*pseudo_class, NonTSPseudoClass::MozBroken) { + return static_prefs::pref!("layout.css.moz-broken.content.enabled"); + } + + 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_parent_selector(&self) -> bool { + true + } + + #[inline] + fn parse_slotted(&self) -> bool { + true + } + + #[inline] + fn parse_host(&self) -> bool { + true + } + + #[inline] + fn parse_nth_child_of(&self) -> bool { + true + } + + #[inline] + fn parse_is_and_where(&self) -> bool { + true + } + + #[inline] + fn parse_has(&self) -> bool { + static_prefs::pref!("layout.css.has-selector.enabled") + } + + #[inline] + fn parse_part(&self) -> bool { + true + } + + #[inline] + fn is_is_alias(&self, function: &str) -> bool { + function.eq_ignore_ascii_case("-moz-any") + } + + #[inline] + fn allow_forgiving_selectors(&self) -> bool { + !self.for_supports_rule + } + + fn parse_non_ts_pseudo_class( + &self, + location: SourceLocation, + name: CowRcStr<'i>, + ) -> Result> { + if let Some(pseudo_class) = NonTSPseudoClass::parse_non_functional(&name) { + if self.is_pseudo_class_enabled(&pseudo_class) { + return Ok(pseudo_class); + } + } + Err( + location.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement( + name, + )), + ) + } + + fn parse_non_ts_functional_pseudo_class<'t>( + &self, + name: CowRcStr<'i>, + parser: &mut Parser<'i, 't>, + after_part: bool, + ) -> Result> { + let pseudo_class = match_ignore_ascii_case! { &name, + "lang" if !after_part => { + let result = parser.parse_comma_separated(|input| { + Ok(AtomIdent::from(input.expect_ident_or_string()?.as_ref())) + })?; + if result.is_empty() { + return Err(parser.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + NonTSPseudoClass::Lang(Lang(result.into())) + }, + "state" => { + let result = AtomIdent::from(parser.expect_ident()?.as_ref()); + NonTSPseudoClass::CustomState(CustomState(result)) + }, + "-moz-locale-dir" if !after_part => { + NonTSPseudoClass::MozLocaleDir(Direction::parse(parser)?) + }, + "dir" if !after_part => { + NonTSPseudoClass::Dir(Direction::parse(parser)?) + }, + _ => return Err(parser.new_custom_error( + SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name.clone()) + )) + }; + if self.is_pseudo_class_enabled(&pseudo_class) { + Ok(pseudo_class) + } else { + Err( + parser.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement( + name, + )), + ) + } + } + + fn parse_pseudo_element( + &self, + location: SourceLocation, + name: CowRcStr<'i>, + ) -> Result> { + let allow_unkown_webkit = !self.for_supports_rule; + if let Some(pseudo) = PseudoElement::from_slice(&name, allow_unkown_webkit) { + if self.is_pseudo_element_enabled(&pseudo) { + return Ok(pseudo); + } + } + + Err( + location.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement( + name, + )), + ) + } + + fn parse_functional_pseudo_element<'t>( + &self, + name: CowRcStr<'i>, + parser: &mut Parser<'i, 't>, + ) -> Result> { + if starts_with_ignore_ascii_case(&name, "-moz-tree-") { + // Tree pseudo-elements can have zero or more arguments, separated + // by either comma or space. + let mut args = ThinVec::new(); + loop { + let location = parser.current_source_location(); + match parser.next() { + Ok(&Token::Ident(ref ident)) => args.push(Atom::from(ident.as_ref())), + Ok(&Token::Comma) => {}, + Ok(t) => return Err(location.new_unexpected_token_error(t.clone())), + Err(BasicParseError { + kind: BasicParseErrorKind::EndOfInput, + .. + }) => break, + _ => unreachable!("Parser::next() shouldn't return any other error"), + } + } + if let Some(pseudo) = PseudoElement::tree_pseudo_element(&name, args) { + if self.is_pseudo_element_enabled(&pseudo) { + return Ok(pseudo); + } + } + } else if name.eq_ignore_ascii_case("highlight") { + let pseudo = PseudoElement::Highlight(AtomIdent::from(parser.expect_ident()?.as_ref())); + if self.is_pseudo_element_enabled(&pseudo) { + return Ok(pseudo); + } + } + Err( + parser.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement( + name, + )), + ) + } + + fn default_namespace(&self) -> Option { + self.namespaces.default.clone() + } + + fn namespace_for_prefix(&self, prefix: &AtomIdent) -> Option { + self.namespaces.prefixes.get(prefix).cloned() + } +} + +impl SelectorImpl { + /// A helper to traverse each eagerly cascaded pseudo-element, executing + /// `fun` on it. + #[inline] + pub fn each_eagerly_cascaded_pseudo_element(mut fun: F) + where + F: FnMut(PseudoElement), + { + for pseudo in &EAGER_PSEUDOS { + fun(pseudo.clone()) + } + } +} + +// Selector and component sizes are important for matching performance. +size_of_test!(selectors::parser::Selector, 8); +size_of_test!(selectors::parser::Component, 24); +size_of_test!(PseudoElement, 16); +size_of_test!(NonTSPseudoClass, 16); diff --git a/servo/components/style/gecko/snapshot.rs b/servo/components/style/gecko/snapshot.rs new file mode 100644 index 0000000000..2ff04406ac --- /dev/null +++ b/servo/components/style/gecko/snapshot.rs @@ -0,0 +1,174 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! A gecko snapshot, that stores the element attributes and state before they +//! change in order to properly calculate restyle hints. + +use crate::dom::TElement; +use crate::gecko::snapshot_helpers; +use crate::gecko::wrapper::GeckoElement; +use crate::gecko_bindings::bindings; +use crate::gecko_bindings::structs::ServoElementSnapshot; +use crate::gecko_bindings::structs::ServoElementSnapshotFlags as Flags; +use crate::gecko_bindings::structs::ServoElementSnapshotTable; +use crate::invalidation::element::element_wrapper::ElementSnapshot; +use crate::selector_parser::AttrValue; +use crate::string_cache::{Atom, Namespace}; +use crate::values::{AtomIdent, AtomString}; +use crate::LocalName; +use crate::WeakAtom; +use dom::ElementState; +use selectors::attr::{AttrSelectorOperation, 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 { + snapshot_helpers::attr_matches(&self.mAttrs, ns, local_name, operation) + } +} + +impl ElementSnapshot for GeckoElementSnapshot { + fn debug_list_attributes(&self) -> String { + use nsstring::nsCString; + let mut string = nsCString::new(); + unsafe { + bindings::Gecko_Snapshot_DebugListAttributes(self, &mut string); + } + String::from_utf8_lossy(&*string).into_owned() + } + + fn state(&self) -> Option { + if self.has_any(Flags::State) { + Some(ElementState::from_bits_retain(self.mState)) + } else { + None + } + } + + #[inline] + fn has_attrs(&self) -> bool { + self.has_any(Flags::Attributes) + } + + #[inline] + fn id_attr(&self) -> Option<&WeakAtom> { + if !self.has_any(Flags::Id) { + return None; + } + + snapshot_helpers::get_id(&*self.mAttrs) + } + + #[inline] + fn is_part(&self, name: &AtomIdent) -> bool { + let attr = match snapshot_helpers::find_attr(&*self.mAttrs, &atom!("part")) { + Some(attr) => attr, + None => return false, + }; + + snapshot_helpers::has_class_or_part(name, CaseSensitivity::CaseSensitive, attr) + } + + #[inline] + fn imported_part(&self, name: &AtomIdent) -> Option { + snapshot_helpers::imported_part(&*self.mAttrs, name) + } + + #[inline] + fn has_class(&self, name: &AtomIdent, case_sensitivity: CaseSensitivity) -> bool { + if !self.has_any(Flags::MaybeClass) { + return false; + } + + snapshot_helpers::has_class_or_part(name, case_sensitivity, &self.mClass) + } + + #[inline] + fn each_class(&self, callback: F) + where + F: FnMut(&AtomIdent), + { + if !self.has_any(Flags::MaybeClass) { + return; + } + + snapshot_helpers::each_class_or_part(&self.mClass, callback) + } + + #[inline] + fn lang_attr(&self) -> Option { + let ptr = unsafe { bindings::Gecko_SnapshotLangValue(self) }; + if ptr.is_null() { + None + } else { + Some(AtomString(unsafe { Atom::from_addrefed(ptr) })) + } + } +} diff --git a/servo/components/style/gecko/snapshot_helpers.rs b/servo/components/style/gecko/snapshot_helpers.rs new file mode 100644 index 0000000000..ab2d08eaf8 --- /dev/null +++ b/servo/components/style/gecko/snapshot_helpers.rs @@ -0,0 +1,316 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Element an snapshot common logic. + +use crate::dom::TElement; +use crate::gecko::wrapper::namespace_id_to_atom; +use crate::gecko_bindings::bindings; +use crate::gecko_bindings::structs::{self, nsAtom}; +use crate::invalidation::element::element_wrapper::ElementSnapshot; +use crate::selector_parser::{AttrValue, SnapshotMap}; +use crate::string_cache::WeakAtom; +use crate::values::AtomIdent; +use crate::{Atom, CaseSensitivityExt, LocalName, Namespace}; +use selectors::attr::{ + AttrSelectorOperation, AttrSelectorOperator, CaseSensitivity, NamespaceConstraint, +}; +use smallvec::SmallVec; + +/// A function that, given an element of type `T`, allows you to get a single +/// class or a class list. +enum Class<'a> { + None, + One(*const nsAtom), + More(&'a [structs::RefPtr]), +} + +#[inline(always)] +fn base_type(attr: &structs::nsAttrValue) -> structs::nsAttrValue_ValueBaseType { + (attr.mBits & structs::NS_ATTRVALUE_BASETYPE_MASK) as structs::nsAttrValue_ValueBaseType +} + +#[inline(always)] +unsafe fn ptr(attr: &structs::nsAttrValue) -> *const T { + (attr.mBits & !structs::NS_ATTRVALUE_BASETYPE_MASK) as *const T +} + +#[inline(always)] +unsafe fn get_class_or_part_from_attr(attr: &structs::nsAttrValue) -> Class { + debug_assert!(bindings::Gecko_AssertClassAttrValueIsSane(attr)); + let base_type = base_type(attr); + if base_type == structs::nsAttrValue_ValueBaseType_eAtomBase { + return Class::One(ptr::(attr)); + } + if base_type == structs::nsAttrValue_ValueBaseType_eOtherBase { + let container = ptr::(attr); + debug_assert_eq!( + (*container).mType, + structs::nsAttrValue_ValueType_eAtomArray + ); + // NOTE: Bindgen doesn't deal with AutoTArray, so cast it below. + let attr_array: *const _ = *(*container) + .__bindgen_anon_1 + .mValue + .as_ref() + .__bindgen_anon_1 + .mAtomArray + .as_ref(); + let array = + (*attr_array).mArray.as_ptr() as *const structs::nsTArray>; + return Class::More(&**array); + } + debug_assert_eq!(base_type, structs::nsAttrValue_ValueBaseType_eStringBase); + Class::None +} + +#[inline(always)] +unsafe fn get_id_from_attr(attr: &structs::nsAttrValue) -> &WeakAtom { + debug_assert_eq!( + base_type(attr), + structs::nsAttrValue_ValueBaseType_eAtomBase + ); + WeakAtom::new(ptr::(attr)) +} + +impl structs::nsAttrName { + #[inline] + fn is_nodeinfo(&self) -> bool { + self.mBits & 1 != 0 + } + + #[inline] + unsafe fn as_nodeinfo(&self) -> &structs::NodeInfo { + debug_assert!(self.is_nodeinfo()); + &*((self.mBits & !1) as *const structs::NodeInfo) + } + + #[inline] + fn namespace_id(&self) -> i32 { + if !self.is_nodeinfo() { + return structs::kNameSpaceID_None; + } + unsafe { self.as_nodeinfo() }.mInner.mNamespaceID + } + + /// Returns the attribute name as an atom pointer. + #[inline] + pub fn name(&self) -> *const nsAtom { + if self.is_nodeinfo() { + unsafe { self.as_nodeinfo() }.mInner.mName + } else { + self.mBits as *const nsAtom + } + } +} + +/// Find an attribute value with a given name and no namespace. +#[inline(always)] +pub fn find_attr<'a>( + attrs: &'a [structs::AttrArray_InternalAttr], + name: &Atom, +) -> Option<&'a structs::nsAttrValue> { + attrs + .iter() + .find(|attr| attr.mName.mBits == name.as_ptr() as usize) + .map(|attr| &attr.mValue) +} + +/// Finds the id attribute from a list of attributes. +#[inline(always)] +pub fn get_id(attrs: &[structs::AttrArray_InternalAttr]) -> Option<&WeakAtom> { + Some(unsafe { get_id_from_attr(find_attr(attrs, &atom!("id"))?) }) +} + +#[inline(always)] +pub(super) fn each_exported_part( + attrs: &[structs::AttrArray_InternalAttr], + name: &AtomIdent, + mut callback: impl FnMut(&AtomIdent), +) { + let attr = match find_attr(attrs, &atom!("exportparts")) { + Some(attr) => attr, + None => return, + }; + let mut length = 0; + let atoms = unsafe { bindings::Gecko_Element_ExportedParts(attr, name.as_ptr(), &mut length) }; + if atoms.is_null() { + return; + } + + unsafe { + for atom in std::slice::from_raw_parts(atoms, length) { + AtomIdent::with(*atom, &mut callback) + } + } +} + +#[inline(always)] +pub(super) fn imported_part( + attrs: &[structs::AttrArray_InternalAttr], + name: &AtomIdent, +) -> Option { + let attr = find_attr(attrs, &atom!("exportparts"))?; + let atom = unsafe { bindings::Gecko_Element_ImportedPart(attr, name.as_ptr()) }; + if atom.is_null() { + return None; + } + Some(AtomIdent(unsafe { Atom::from_raw(atom) })) +} + +/// Given a class or part name, a case sensitivity, and an array of attributes, +/// returns whether the attribute has that name. +#[inline(always)] +pub fn has_class_or_part( + name: &AtomIdent, + case_sensitivity: CaseSensitivity, + attr: &structs::nsAttrValue, +) -> bool { + match unsafe { get_class_or_part_from_attr(attr) } { + Class::None => false, + Class::One(atom) => unsafe { case_sensitivity.eq_atom(name, WeakAtom::new(atom)) }, + Class::More(atoms) => match case_sensitivity { + CaseSensitivity::CaseSensitive => { + let name_ptr = name.as_ptr(); + atoms.iter().any(|atom| atom.mRawPtr == name_ptr) + }, + CaseSensitivity::AsciiCaseInsensitive => unsafe { + atoms + .iter() + .any(|atom| WeakAtom::new(atom.mRawPtr).eq_ignore_ascii_case(name)) + }, + }, + } +} + +/// Given an item, a callback, and a getter, execute `callback` for each class +/// or part name this `item` has. +#[inline(always)] +pub fn each_class_or_part(attr: &structs::nsAttrValue, mut callback: F) +where + F: FnMut(&AtomIdent), +{ + unsafe { + match get_class_or_part_from_attr(attr) { + Class::None => {}, + Class::One(atom) => AtomIdent::with(atom, callback), + Class::More(atoms) => { + for atom in atoms { + AtomIdent::with(atom.mRawPtr, &mut callback) + } + }, + } + } +} + +/// Returns a list of classes that were either added to or removed from the +/// element since the snapshot. +pub fn classes_changed(element: &E, snapshots: &SnapshotMap) -> SmallVec<[Atom; 8]> { + debug_assert!(element.has_snapshot(), "Why bothering?"); + let snapshot = snapshots.get(element).expect("has_snapshot lied"); + if !snapshot.class_changed() { + return SmallVec::new(); + } + + let mut classes_changed = SmallVec::<[Atom; 8]>::new(); + snapshot.each_class(|c| { + if !element.has_class(c, CaseSensitivity::CaseSensitive) { + classes_changed.push(c.0.clone()); + } + }); + element.each_class(|c| { + if !snapshot.has_class(c, CaseSensitivity::CaseSensitive) { + classes_changed.push(c.0.clone()); + } + }); + + classes_changed +} + +/// Returns whether a given attribute selector matches given the internal attrs. +#[inline(always)] +pub(crate) fn attr_matches( + attrs: &[structs::AttrArray_InternalAttr], + ns: &NamespaceConstraint<&Namespace>, + local_name: &LocalName, + operation: &AttrSelectorOperation<&AttrValue>, +) -> bool { + let name_ptr = local_name.as_ptr(); + for attr in attrs { + if attr.mName.name() != name_ptr { + continue; + } + + if attr_matches_checked_name(attr, ns, operation) { + return true; + } + + // The name matched but the value or namespace didn't. The only reason to check the other + // attributes now would be to find one with the same name but a different namespace. + if *ns != NamespaceConstraint::Any { + // We don't want to look for other namespaces, so we're done. + return false; + } + } + false +} + +/// Returns whether a given attribute selector matches given a single attribute, +/// for the case where the caller has already found an attribute with the right name. +fn attr_matches_checked_name( + attr: &structs::AttrArray_InternalAttr, + ns: &NamespaceConstraint<&Namespace>, + operation: &AttrSelectorOperation<&AttrValue>, +) -> bool { + let ns_matches = match *ns { + NamespaceConstraint::Any => true, + NamespaceConstraint::Specific(ns) => { + if *ns == ns!() { + !attr.mName.is_nodeinfo() + } else { + ns.as_ptr() == unsafe { namespace_id_to_atom(attr.mName.namespace_id()) } + } + }, + }; + + if !ns_matches { + return false; + } + + let (operator, case_sensitivity, value) = match *operation { + AttrSelectorOperation::Exists => return true, + AttrSelectorOperation::WithValue { + operator, + case_sensitivity, + value, + } => (operator, case_sensitivity, value), + }; + let ignore_case = match case_sensitivity { + CaseSensitivity::CaseSensitive => false, + CaseSensitivity::AsciiCaseInsensitive => true, + }; + let value = value.as_ptr(); + unsafe { + match operator { + AttrSelectorOperator::Equal => { + bindings::Gecko_AttrEquals(&attr.mValue, value, ignore_case) + }, + AttrSelectorOperator::Includes => { + bindings::Gecko_AttrIncludes(&attr.mValue, value, ignore_case) + }, + AttrSelectorOperator::DashMatch => { + bindings::Gecko_AttrDashEquals(&attr.mValue, value, ignore_case) + }, + AttrSelectorOperator::Prefix => { + bindings::Gecko_AttrHasPrefix(&attr.mValue, value, ignore_case) + }, + AttrSelectorOperator::Suffix => { + bindings::Gecko_AttrHasSuffix(&attr.mValue, value, ignore_case) + }, + AttrSelectorOperator::Substring => { + bindings::Gecko_AttrHasSubstring(&attr.mValue, value, ignore_case) + }, + } + } +} 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..7fe32acc20 --- /dev/null +++ b/servo/components/style/gecko/url.rs @@ -0,0 +1,384 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Common handling for the specified value CSS url() values. + +use crate::gecko_bindings::bindings; +use crate::gecko_bindings::structs; +use crate::parser::{Parse, ParserContext}; +use crate::stylesheets::{CorsMode, UrlExtraData}; +use crate::values::computed::{Context, ToComputedValue}; +use cssparser::Parser; +use malloc_size_of::{MallocSizeOf, MallocSizeOfOps}; +use nsstring::nsCString; +use servo_arc::Arc; +use std::collections::HashMap; +use std::fmt::{self, Write}; +use std::mem::ManuallyDrop; +use std::sync::RwLock; +use style_traits::{CssWriter, ParseError, ToCss}; +use to_shmem::{self, SharedMemoryBuilder, ToShmem}; + +/// A CSS url() value for gecko. +#[derive(Clone, Debug, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +#[css(function = "url")] +#[repr(C)] +pub struct CssUrl(pub Arc); + +/// Data shared between CssUrls. +/// +/// cbindgen:derive-eq=false +/// cbindgen:derive-neq=false +#[derive(Debug, SpecifiedValueInfo, ToCss, ToShmem)] +#[repr(C)] +pub struct CssUrlData { + /// The URL in unresolved string form. + serialization: crate::OwnedStr, + + /// The URL extra data. + #[css(skip)] + pub extra_data: UrlExtraData, + + /// The CORS mode that will be used for the load. + #[css(skip)] + cors_mode: CorsMode, + + /// Data to trigger a load from Gecko. This is mutable in C++. + /// + /// TODO(emilio): Maybe we can eagerly resolve URLs and make this immutable? + #[css(skip)] + load_data: LoadDataSource, +} + +impl PartialEq for CssUrlData { + fn eq(&self, other: &Self) -> bool { + self.serialization == other.serialization && + self.extra_data == other.extra_data && + self.cors_mode == other.cors_mode + } +} + +impl CssUrl { + fn parse_with_cors_mode<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + cors_mode: CorsMode, + ) -> Result> { + let url = input.expect_url()?; + Ok(Self::parse_from_string( + url.as_ref().to_owned(), + context, + cors_mode, + )) + } + + /// Parse a URL from a string value that is a valid CSS token for a URL. + pub fn parse_from_string(url: String, context: &ParserContext, cors_mode: CorsMode) -> Self { + CssUrl(Arc::new(CssUrlData { + serialization: url.into(), + extra_data: context.url_data.clone(), + cors_mode, + load_data: LoadDataSource::Owned(LoadData::default()), + })) + } + + /// Returns true if the URL is definitely invalid. We don't eagerly resolve + /// URLs in gecko, so we just return false here. + /// use its |resolved| status. + pub fn is_invalid(&self) -> bool { + false + } + + /// Returns true if this URL looks like a fragment. + /// See https://drafts.csswg.org/css-values/#local-urls + #[inline] + pub fn is_fragment(&self) -> bool { + self.0.is_fragment() + } + + /// Return the unresolved url as string, or the empty string if it's + /// invalid. + #[inline] + pub fn as_str(&self) -> &str { + self.0.as_str() + } +} + +impl CssUrlData { + /// Returns true if this URL looks like a fragment. + /// See https://drafts.csswg.org/css-values/#local-urls + pub fn is_fragment(&self) -> bool { + self.as_str() + .as_bytes() + .iter() + .next() + .map_or(false, |b| *b == b'#') + } + + /// Return the unresolved url as string, or the empty string if it's + /// invalid. + pub fn as_str(&self) -> &str { + &*self.serialization + } +} + +impl Parse for CssUrl { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + Self::parse_with_cors_mode(context, input, CorsMode::None) + } +} + +impl Eq for CssUrl {} + +impl MallocSizeOf for CssUrl { + fn size_of(&self, _ops: &mut MallocSizeOfOps) -> usize { + // XXX: measure `serialization` once bug 1397971 lands + + // We ignore `extra_data`, because RefPtr is tricky, and there aren't + // many of them in practise (sharing is common). + + 0 + } +} + +/// A key type for LOAD_DATA_TABLE. +#[derive(Eq, Hash, PartialEq)] +struct LoadDataKey(*const LoadDataSource); + +unsafe impl Sync for LoadDataKey {} +unsafe impl Send for LoadDataKey {} + +bitflags! { + /// Various bits of mutable state that are kept for image loads. + #[derive(Debug)] + #[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..d04c73c70f --- /dev/null +++ b/servo/components/style/gecko/values.rs @@ -0,0 +1,77 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#![allow(unsafe_code)] + +//! Different kind of helpers to interact with Gecko values. + +use crate::color::{AbsoluteColor, ColorSpace}; +use crate::counter_style::{Symbol, Symbols}; +use crate::gecko_bindings::bindings; +use crate::gecko_bindings::structs::CounterStylePtr; +use crate::values::generics::CounterStyle; +use crate::values::Either; +use crate::Atom; + +/// Convert a color value to `nscolor`. +pub fn convert_absolute_color_to_nscolor(color: &AbsoluteColor) -> u32 { + let srgb = color.to_color_space(ColorSpace::Srgb); + u32::from_le_bytes([ + (srgb.components.0 * 255.0).round() as u8, + (srgb.components.1 * 255.0).round() as u8, + (srgb.components.2 * 255.0).round() as u8, + (srgb.alpha * 255.0).round() as u8, + ]) +} + +/// Convert a given `nscolor` to a Servo AbsoluteColor value. +pub fn convert_nscolor_to_absolute_color(color: u32) -> AbsoluteColor { + let [r, g, b, a] = color.to_le_bytes(); + AbsoluteColor::srgb_legacy(r, g, b, a as f32 / 255.0) +} + +#[test] +fn convert_ns_color_to_absolute_color_should_be_in_legacy_syntax() { + use crate::color::ColorFlags; + + let result = convert_nscolor_to_absolute_color(0x336699CC); + assert!(result.flags.contains(ColorFlags::IS_LEGACY_SRGB)); + + assert!(result.is_legacy_syntax()); +} + +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..61352ef9c0 --- /dev/null +++ b/servo/components/style/gecko/wrapper.rs @@ -0,0 +1,2211 @@ +/* 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::bloom::each_relevant_element_hash; +use crate::context::{PostAnimationTasks, QuirksMode, SharedStyleContext, UpdateAnimationsTasks}; +use crate::data::ElementData; +use crate::dom::{LayoutIterator, NodeInfo, OpaqueNode, TDocument, TElement, TNode, TShadowRoot}; +use crate::gecko::selector_parser::{CustomState, NonTSPseudoClass, PseudoElement, SelectorImpl}; +use crate::gecko::snapshot_helpers; +use crate::gecko_bindings::bindings; +use crate::gecko_bindings::bindings::Gecko_ElementHasAnimations; +use crate::gecko_bindings::bindings::Gecko_ElementHasCSSAnimations; +use crate::gecko_bindings::bindings::Gecko_ElementHasCSSTransitions; +use crate::gecko_bindings::bindings::Gecko_ElementState; +use crate::gecko_bindings::bindings::Gecko_GetActiveLinkAttrDeclarationBlock; +use crate::gecko_bindings::bindings::Gecko_GetAnimationEffectCount; +use crate::gecko_bindings::bindings::Gecko_GetAnimationRule; +use crate::gecko_bindings::bindings::Gecko_GetExtraContentStyleDeclarations; +use crate::gecko_bindings::bindings::Gecko_GetHTMLPresentationAttrDeclarationBlock; +use crate::gecko_bindings::bindings::Gecko_GetStyleAttrDeclarationBlock; +use crate::gecko_bindings::bindings::Gecko_GetUnvisitedLinkAttrDeclarationBlock; +use crate::gecko_bindings::bindings::Gecko_GetVisitedLinkAttrDeclarationBlock; +use crate::gecko_bindings::bindings::Gecko_IsSignificantChild; +use crate::gecko_bindings::bindings::Gecko_MatchLang; +use crate::gecko_bindings::bindings::Gecko_UnsetDirtyStyleAttr; +use crate::gecko_bindings::bindings::Gecko_UpdateAnimations; +use crate::gecko_bindings::structs; +use crate::gecko_bindings::structs::nsChangeHint; +use crate::gecko_bindings::structs::EffectCompositor_CascadeLevel as CascadeLevel; +use crate::gecko_bindings::structs::ELEMENT_HANDLED_SNAPSHOT; +use crate::gecko_bindings::structs::ELEMENT_HAS_ANIMATION_ONLY_DIRTY_DESCENDANTS_FOR_SERVO; +use crate::gecko_bindings::structs::ELEMENT_HAS_DIRTY_DESCENDANTS_FOR_SERVO; +use crate::gecko_bindings::structs::ELEMENT_HAS_SNAPSHOT; +use crate::gecko_bindings::structs::NODE_DESCENDANTS_NEED_FRAMES; +use crate::gecko_bindings::structs::NODE_NEEDS_FRAME; +use crate::gecko_bindings::structs::{nsAtom, nsIContent, nsINode_BooleanFlag}; +use crate::gecko_bindings::structs::{nsINode as RawGeckoNode, Element as RawGeckoElement}; +use crate::global_style_data::GLOBAL_STYLE_DATA; +use crate::invalidation::element::restyle_hints::RestyleHint; +use crate::media_queries::Device; +use crate::properties::{ + animated_properties::{AnimationValue, AnimationValueMap}, + ComputedValues, Importance, OwnedPropertyDeclarationId, PropertyDeclaration, + PropertyDeclarationBlock, PropertyDeclarationId, PropertyDeclarationIdSet, +}; +use crate::rule_tree::CascadeLevel as ServoCascadeLevel; +use crate::selector_parser::{AttrValue, Lang}; +use crate::shared_lock::{Locked, SharedRwLock}; +use crate::string_cache::{Atom, Namespace, WeakAtom, WeakNamespace}; +use crate::stylist::CascadeData; +use crate::values::computed::Display; +use crate::values::{AtomIdent, AtomString}; +use crate::CaseSensitivityExt; +use crate::LocalName; +use app_units::Au; +use atomic_refcell::{AtomicRef, AtomicRefCell, AtomicRefMut}; +use dom::{DocumentState, ElementState}; +use euclid::default::Size2D; +use fxhash::FxHashMap; +use selectors::attr::{AttrSelectorOperation, CaseSensitivity, NamespaceConstraint}; +use selectors::bloom::{BloomFilter, BLOOM_HASH_MASK}; +use selectors::matching::VisitedHandlingMode; +use selectors::matching::{ElementSelectorFlags, MatchingContext}; +use selectors::sink::Push; +use selectors::{Element, OpaqueElement}; +use servo_arc::{Arc, ArcBorrow}; +use std::cell::Cell; +use std::fmt; +use std::hash::{Hash, Hasher}; +use std::mem; +use std::ptr; +use std::sync::atomic::{AtomicU32, Ordering}; + +#[inline] +fn elements_with_id<'a, 'le>( + array: *const structs::nsTArray<*mut RawGeckoElement>, +) -> &'a [GeckoElement<'le>] { + unsafe { + if array.is_null() { + return &[]; + } + + let elements: &[*mut RawGeckoElement] = &**array; + + // NOTE(emilio): We rely on the in-memory representation of + // GeckoElement<'ld> and *mut RawGeckoElement being the same. + #[allow(dead_code)] + unsafe fn static_assert() { + mem::transmute::<*mut RawGeckoElement, GeckoElement<'static>>(0xbadc0de as *mut _); + } + + mem::transmute(elements) + } +} + +/// A simple wrapper over `Document`. +#[derive(Clone, Copy)] +pub struct GeckoDocument<'ld>(pub &'ld structs::Document); + +impl<'ld> TDocument for GeckoDocument<'ld> { + type ConcreteNode = GeckoNode<'ld>; + + #[inline] + fn as_node(&self) -> Self::ConcreteNode { + GeckoNode(&self.0._base) + } + + #[inline] + fn is_html_document(&self) -> bool { + self.0.mType == structs::Document_Type::eHTML + } + + #[inline] + fn quirks_mode(&self) -> QuirksMode { + self.0.mCompatMode.into() + } + + #[inline] + fn elements_with_id<'a>(&self, id: &AtomIdent) -> Result<&'a [GeckoElement<'ld>], ()> + where + Self: 'a, + { + Ok(elements_with_id(unsafe { + bindings::Gecko_Document_GetElementsWithId(self.0, id.as_ptr()) + })) + } + + fn shared_lock(&self) -> &SharedRwLock { + &GLOBAL_STYLE_DATA.shared_lock + } +} + +/// A simple wrapper over `ShadowRoot`. +#[derive(Clone, Copy)] +pub struct GeckoShadowRoot<'lr>(pub &'lr structs::ShadowRoot); + +impl<'ln> fmt::Debug for GeckoShadowRoot<'ln> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // TODO(emilio): Maybe print the host or something? + write!(f, " ({:#x})", self.as_node().opaque().0) + } +} + +impl<'lr> PartialEq for GeckoShadowRoot<'lr> { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.0 as *const _ == other.0 as *const _ + } +} + +impl<'lr> TShadowRoot for GeckoShadowRoot<'lr> { + type ConcreteNode = GeckoNode<'lr>; + + #[inline] + fn as_node(&self) -> Self::ConcreteNode { + GeckoNode(&self.0._base._base._base._base) + } + + #[inline] + fn host(&self) -> GeckoElement<'lr> { + GeckoElement(unsafe { &*self.0._base.mHost.mRawPtr }) + } + + #[inline] + fn style_data<'a>(&self) -> Option<&'a CascadeData> + where + Self: 'a, + { + let author_styles = unsafe { self.0.mServoStyles.mPtr.as_ref()? }; + Some(&author_styles.data) + } + + #[inline] + fn elements_with_id<'a>(&self, id: &AtomIdent) -> Result<&'a [GeckoElement<'lr>], ()> + where + Self: 'a, + { + Ok(elements_with_id(unsafe { + bindings::Gecko_ShadowRoot_GetElementsWithId(self.0, id.as_ptr()) + })) + } + + #[inline] + fn parts<'a>(&self) -> &[::ConcreteElement] + where + Self: 'a, + { + let slice: &[*const RawGeckoElement] = &*self.0.mParts; + + #[allow(dead_code)] + unsafe fn static_assert() { + mem::transmute::<*const RawGeckoElement, GeckoElement<'static>>(0xbadc0de as *const _); + } + + unsafe { mem::transmute(slice) } + } +} + +/// A simple wrapper over a non-null Gecko node (`nsINode`) pointer. +/// +/// Important: We don't currently refcount the DOM, because the wrapper lifetime +/// magic guarantees that our LayoutFoo references won't outlive the root, and +/// we don't mutate any of the references on the Gecko side during restyle. +/// +/// We could implement refcounting if need be (at a potentially non-trivial +/// performance cost) by implementing Drop and making LayoutFoo non-Copy. +#[derive(Clone, Copy)] +pub struct GeckoNode<'ln>(pub &'ln RawGeckoNode); + +impl<'ln> PartialEq for GeckoNode<'ln> { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.0 as *const _ == other.0 as *const _ + } +} + +impl<'ln> fmt::Debug for GeckoNode<'ln> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if let Some(el) = self.as_element() { + return el.fmt(f); + } + + if self.is_text_node() { + return write!(f, " ({:#x})", self.opaque().0); + } + + if self.is_document() { + return write!(f, " ({:#x})", self.opaque().0); + } + + if let Some(sr) = self.as_shadow_root() { + return sr.fmt(f); + } + + write!(f, " ({:#x})", self.opaque().0) + } +} + +impl<'ln> GeckoNode<'ln> { + #[inline] + fn is_document(&self) -> bool { + // This is a DOM constant that isn't going to change. + const DOCUMENT_NODE: u16 = 9; + self.node_info().mInner.mNodeType == DOCUMENT_NODE + } + + #[inline] + fn is_shadow_root(&self) -> bool { + self.is_in_shadow_tree() && self.parent_node().is_none() + } + + #[inline] + fn from_content(content: &'ln nsIContent) -> Self { + GeckoNode(&content._base) + } + + #[inline] + fn set_flags(&self, flags: u32) { + self.flags_atomic().fetch_or(flags, Ordering::Relaxed); + } + + fn flags_atomic_for(flags: &Cell) -> &AtomicU32 { + const_assert!(std::mem::size_of::>() == std::mem::size_of::()); + const_assert!(std::mem::align_of::>() == std::mem::align_of::()); + + // Rust doesn't provide standalone atomic functions like GCC/clang do + // (via the atomic intrinsics) or via std::atomic_ref, but it guarantees + // that the memory representation of u32 and AtomicU32 matches: + // https://doc.rust-lang.org/std/sync/atomic/struct.AtomicU32.html + unsafe { std::mem::transmute::<&Cell, &AtomicU32>(flags) } + } + + #[inline] + fn flags_atomic(&self) -> &AtomicU32 { + Self::flags_atomic_for(&self.0._base._base_1.mFlags) + } + + #[inline] + fn flags(&self) -> u32 { + self.flags_atomic().load(Ordering::Relaxed) + } + + #[inline] + fn selector_flags_atomic(&self) -> &AtomicU32 { + Self::flags_atomic_for(&self.0.mSelectorFlags) + } + + #[inline] + fn selector_flags(&self) -> u32 { + self.selector_flags_atomic().load(Ordering::Relaxed) + } + + #[inline] + fn set_selector_flags(&self, flags: u32) { + self.selector_flags_atomic() + .fetch_or(flags, Ordering::Relaxed); + } + + #[inline] + fn node_info(&self) -> &structs::NodeInfo { + debug_assert!(!self.0.mNodeInfo.mRawPtr.is_null()); + unsafe { &*self.0.mNodeInfo.mRawPtr } + } + + // These live in different locations depending on processor architecture. + #[cfg(target_pointer_width = "64")] + #[inline] + fn bool_flags(&self) -> u32 { + (self.0)._base._base_1.mBoolFlags + } + + #[cfg(target_pointer_width = "32")] + #[inline] + fn bool_flags(&self) -> u32 { + (self.0).mBoolFlags + } + + #[inline] + fn get_bool_flag(&self, flag: nsINode_BooleanFlag) -> bool { + self.bool_flags() & (1u32 << flag as u32) != 0 + } + + /// This logic is duplicate in Gecko's nsINode::IsInShadowTree(). + #[inline] + fn is_in_shadow_tree(&self) -> bool { + use crate::gecko_bindings::structs::NODE_IS_IN_SHADOW_TREE; + self.flags() & NODE_IS_IN_SHADOW_TREE != 0 + } + + /// Returns true if we know for sure that `flattened_tree_parent` and `parent_node` return the + /// same thing. + /// + /// TODO(emilio): Measure and consider not doing this fast-path, it's only a function call and + /// from profiles it seems that keeping this fast path makes the compiler not inline + /// `flattened_tree_parent` as a whole, so we're not gaining much either. + #[inline] + fn flattened_tree_parent_is_parent(&self) -> bool { + use crate::gecko_bindings::structs::*; + let flags = self.flags(); + + let parent = match self.parent_node() { + Some(p) => p, + None => return true, + }; + + if parent.is_shadow_root() { + return false; + } + + if let Some(parent) = parent.as_element() { + if flags & NODE_IS_NATIVE_ANONYMOUS_ROOT != 0 && parent.is_root() { + return false; + } + if parent.shadow_root().is_some() || parent.is_html_slot_element() { + return false; + } + } + + true + } + + #[inline] + fn flattened_tree_parent(&self) -> Option { + if self.flattened_tree_parent_is_parent() { + debug_assert_eq!( + unsafe { + bindings::Gecko_GetFlattenedTreeParentNode(self.0) + .as_ref() + .map(GeckoNode) + }, + self.parent_node(), + "Fast path stopped holding!" + ); + return self.parent_node(); + } + + // NOTE(emilio): If this call is too expensive, we could manually inline more aggressively. + unsafe { + bindings::Gecko_GetFlattenedTreeParentNode(self.0) + .as_ref() + .map(GeckoNode) + } + } + + #[inline] + fn contains_non_whitespace_content(&self) -> bool { + unsafe { Gecko_IsSignificantChild(self.0, false) } + } + + /// Returns the previous sibling of this node that is an element. + #[inline] + pub fn prev_sibling_element(&self) -> Option { + let mut prev = self.prev_sibling(); + while let Some(p) = prev { + if let Some(e) = p.as_element() { + return Some(e); + } + prev = p.prev_sibling(); + } + None + } + + /// Returns the next sibling of this node that is an element. + #[inline] + pub fn next_sibling_element(&self) -> Option { + let mut next = self.next_sibling(); + while let Some(n) = next { + if let Some(e) = n.as_element() { + return Some(e); + } + next = n.next_sibling(); + } + None + } + + /// Returns last child sibling of this node that is an element. + #[inline] + pub fn last_child_element(&self) -> Option> { + let last = match self.last_child() { + Some(n) => n, + None => return None, + }; + if let Some(e) = last.as_element() { + return Some(e); + } + None + } +} + +impl<'ln> NodeInfo for GeckoNode<'ln> { + #[inline] + fn is_element(&self) -> bool { + self.get_bool_flag(nsINode_BooleanFlag::NodeIsElement) + } + + fn is_text_node(&self) -> bool { + // This is a DOM constant that isn't going to change. + const TEXT_NODE: u16 = 3; + self.node_info().mInner.mNodeType == TEXT_NODE + } +} + +impl<'ln> TNode for GeckoNode<'ln> { + type ConcreteDocument = GeckoDocument<'ln>; + type ConcreteShadowRoot = GeckoShadowRoot<'ln>; + type ConcreteElement = GeckoElement<'ln>; + + #[inline] + fn parent_node(&self) -> Option { + unsafe { self.0.mParent.as_ref().map(GeckoNode) } + } + + #[inline] + fn first_child(&self) -> Option { + unsafe { + self.0 + .mFirstChild + .raw() + .as_ref() + .map(GeckoNode::from_content) + } + } + + #[inline] + fn last_child(&self) -> Option { + unsafe { bindings::Gecko_GetLastChild(self.0).as_ref().map(GeckoNode) } + } + + #[inline] + fn prev_sibling(&self) -> Option { + unsafe { + let prev_or_last = GeckoNode::from_content(self.0.mPreviousOrLastSibling.as_ref()?); + if prev_or_last.0.mNextSibling.raw().is_null() { + return None; + } + Some(prev_or_last) + } + } + + #[inline] + fn next_sibling(&self) -> Option { + unsafe { + self.0 + .mNextSibling + .raw() + .as_ref() + .map(GeckoNode::from_content) + } + } + + #[inline] + fn owner_doc(&self) -> Self::ConcreteDocument { + debug_assert!(!self.node_info().mDocument.is_null()); + GeckoDocument(unsafe { &*self.node_info().mDocument }) + } + + #[inline] + fn is_in_document(&self) -> bool { + self.get_bool_flag(nsINode_BooleanFlag::IsInDocument) + } + + fn traversal_parent(&self) -> Option> { + self.flattened_tree_parent().and_then(|n| n.as_element()) + } + + #[inline] + fn opaque(&self) -> OpaqueNode { + let ptr: usize = self.0 as *const _ as usize; + OpaqueNode(ptr) + } + + fn debug_id(self) -> usize { + unimplemented!() + } + + #[inline] + fn as_element(&self) -> Option> { + if !self.is_element() { + return None; + } + + Some(GeckoElement(unsafe { + &*(self.0 as *const _ as *const RawGeckoElement) + })) + } + + #[inline] + fn as_document(&self) -> Option { + if !self.is_document() { + return None; + } + + debug_assert_eq!(self.owner_doc().as_node(), *self, "How?"); + Some(self.owner_doc()) + } + + #[inline] + fn as_shadow_root(&self) -> Option { + if !self.is_shadow_root() { + return None; + } + + Some(GeckoShadowRoot(unsafe { + &*(self.0 as *const _ as *const structs::ShadowRoot) + })) + } +} + +/// A wrapper on top of two kind of iterators, depending on the parent being +/// iterated. +/// +/// We generally iterate children by traversing the light-tree siblings of the +/// first child like Servo does. +/// +/// However, for nodes with anonymous children, we use a custom (heavier-weight) +/// Gecko-implemented iterator. +/// +/// FIXME(emilio): If we take into account shadow DOM, we're going to need the +/// flat tree pretty much always. We can try to optimize the case where there's +/// no shadow root sibling, probably. +pub enum GeckoChildrenIterator<'a> { + /// A simple iterator that tracks the current node being iterated and + /// replaces it with the next sibling when requested. + Current(Option>), + /// A Gecko-implemented iterator we need to drop appropriately. + GeckoIterator(structs::StyleChildrenIterator), +} + +impl<'a> Drop for GeckoChildrenIterator<'a> { + fn drop(&mut self) { + if let GeckoChildrenIterator::GeckoIterator(ref mut it) = *self { + unsafe { + bindings::Gecko_DestroyStyleChildrenIterator(it); + } + } + } +} + +impl<'a> Iterator for GeckoChildrenIterator<'a> { + type Item = GeckoNode<'a>; + fn next(&mut self) -> Option> { + match *self { + GeckoChildrenIterator::Current(curr) => { + let next = curr.and_then(|node| node.next_sibling()); + *self = GeckoChildrenIterator::Current(next); + curr + }, + GeckoChildrenIterator::GeckoIterator(ref mut it) => unsafe { + // We do this unsafe lengthening of the lifetime here because + // structs::StyleChildrenIterator is actually StyleChildrenIterator<'a>, + // however we can't express this easily with bindgen, and it would + // introduce functions with two input lifetimes into bindgen, + // which would be out of scope for elision. + bindings::Gecko_GetNextStyleChild(&mut *(it as *mut _)) + .as_ref() + .map(GeckoNode) + }, + } + } +} + +/// A simple wrapper over a non-null Gecko `Element` pointer. +#[derive(Clone, Copy)] +pub struct GeckoElement<'le>(pub &'le RawGeckoElement); + +impl<'le> fmt::Debug for GeckoElement<'le> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use nsstring::nsCString; + + write!(f, "<{}", self.local_name())?; + + let mut attrs = nsCString::new(); + unsafe { + bindings::Gecko_Element_DebugListAttributes(self.0, &mut attrs); + } + write!(f, "{}", attrs)?; + write!(f, "> ({:#x})", self.as_node().opaque().0) + } +} + +impl<'le> GeckoElement<'le> { + /// Gets the raw `ElementData` refcell for the element. + #[inline(always)] + pub fn get_data(&self) -> Option<&AtomicRefCell> { + unsafe { self.0.mServoData.get().as_ref() } + } + + /// Returns whether any animation applies to this element. + #[inline] + pub fn has_any_animation(&self) -> bool { + self.may_have_animations() && unsafe { Gecko_ElementHasAnimations(self.0) } + } + + #[inline(always)] + fn attrs(&self) -> &[structs::AttrArray_InternalAttr] { + unsafe { + match self.0.mAttrs.mImpl.mPtr.as_ref() { + Some(attrs) => attrs.mBuffer.as_slice(attrs.mAttrCount as usize), + None => return &[], + } + } + } + + #[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 may_have_anonymous_children(&self) -> bool { + self.as_node() + .get_bool_flag(nsINode_BooleanFlag::ElementMayHaveAnonymousChildren) + } + + #[inline] + fn flags(&self) -> u32 { + self.as_node().flags() + } + + #[inline] + fn set_flags(&self, flags: u32) { + self.as_node().set_flags(flags); + } + + #[inline] + unsafe fn unset_flags(&self, flags: u32) { + self.as_node() + .flags_atomic() + .fetch_and(!flags, Ordering::Relaxed); + } + + /// Returns true if this element has descendants for lazy frame construction. + #[inline] + pub fn descendants_need_frames(&self) -> bool { + self.flags() & NODE_DESCENDANTS_NEED_FRAMES != 0 + } + + /// Returns true if this element needs lazy frame construction. + #[inline] + pub fn needs_frame(&self) -> bool { + self.flags() & NODE_NEEDS_FRAME != 0 + } + + /// Returns a reference to the DOM slots for this Element, if they exist. + #[inline] + fn dom_slots(&self) -> Option<&structs::FragmentOrElement_nsDOMSlots> { + let slots = self.as_node().0.mSlots as *const structs::FragmentOrElement_nsDOMSlots; + unsafe { slots.as_ref() } + } + + /// Returns a reference to the extended DOM slots for this Element. + #[inline] + fn extended_slots(&self) -> Option<&structs::FragmentOrElement_nsExtendedDOMSlots> { + self.dom_slots().and_then(|s| unsafe { + // For the bit usage, see nsContentSlots::GetExtendedSlots. + let e_slots = s._base.mExtendedSlots & + !structs::nsIContent_nsContentSlots_sNonOwningExtendedSlotsFlag; + (e_slots as *const structs::FragmentOrElement_nsExtendedDOMSlots).as_ref() + }) + } + + #[inline] + fn namespace_id(&self) -> i32 { + self.as_node().node_info().mInner.mNamespaceID + } + + #[inline] + fn has_id(&self) -> bool { + self.as_node() + .get_bool_flag(nsINode_BooleanFlag::ElementHasID) + } + + #[inline] + fn state_internal(&self) -> u64 { + if !self + .as_node() + .get_bool_flag(nsINode_BooleanFlag::ElementHasLockedStyleStates) + { + return self.0.mState.bits; + } + unsafe { Gecko_ElementState(self.0) } + } + + #[inline] + fn document_state(&self) -> DocumentState { + DocumentState::from_bits_retain(self.as_node().owner_doc().0.mState.bits) + } + + #[inline] + fn may_have_class(&self) -> bool { + self.as_node() + .get_bool_flag(nsINode_BooleanFlag::ElementMayHaveClass) + } + + #[inline] + fn has_properties(&self) -> bool { + use crate::gecko_bindings::structs::NODE_HAS_PROPERTIES; + + self.flags() & NODE_HAS_PROPERTIES != 0 + } + + #[inline] + fn before_or_after_pseudo(&self, is_before: bool) -> Option { + if !self.has_properties() { + return None; + } + + unsafe { + bindings::Gecko_GetBeforeOrAfterPseudo(self.0, is_before) + .as_ref() + .map(GeckoElement) + } + } + + #[inline] + fn may_have_style_attribute(&self) -> bool { + self.as_node() + .get_bool_flag(nsINode_BooleanFlag::ElementMayHaveStyle) + } + + /// Only safe to call on the main thread, with exclusive access to the + /// element and its ancestors. + /// + /// This function is also called after display property changed for SMIL + /// animation. + /// + /// Also this function schedules style flush. + pub unsafe fn note_explicit_hints(&self, restyle_hint: RestyleHint, change_hint: nsChangeHint) { + use crate::gecko::restyle_damage::GeckoRestyleDamage; + + let damage = GeckoRestyleDamage::new(change_hint); + debug!( + "note_explicit_hints: {:?}, restyle_hint={:?}, change_hint={:?}", + self, restyle_hint, change_hint + ); + + debug_assert!( + !(restyle_hint.has_animation_hint() && restyle_hint.has_non_animation_hint()), + "Animation restyle hints should not appear with non-animation restyle hints" + ); + + let mut data = match self.mutate_data() { + Some(d) => d, + None => { + debug!("(Element not styled, discarding hints)"); + return; + }, + }; + + debug_assert!(data.has_styles(), "how?"); + + // Propagate the bit up the chain. + if restyle_hint.has_animation_hint() { + bindings::Gecko_NoteAnimationOnlyDirtyElement(self.0); + } else { + bindings::Gecko_NoteDirtyElement(self.0); + } + + data.hint.insert(restyle_hint); + data.damage |= damage; + } + + /// This logic is duplicated in Gecko's nsIContent::IsRootOfNativeAnonymousSubtree. + #[inline] + fn is_root_of_native_anonymous_subtree(&self) -> bool { + use crate::gecko_bindings::structs::NODE_IS_NATIVE_ANONYMOUS_ROOT; + return self.flags() & NODE_IS_NATIVE_ANONYMOUS_ROOT != 0; + } + + fn css_transitions_info(&self) -> FxHashMap> { + use crate::gecko_bindings::bindings::Gecko_ElementTransitions_EndValueAt; + use crate::gecko_bindings::bindings::Gecko_ElementTransitions_Length; + + let collection_length = unsafe { Gecko_ElementTransitions_Length(self.0) } as usize; + let mut map = FxHashMap::with_capacity_and_hasher(collection_length, Default::default()); + + for i in 0..collection_length { + let end_value = + unsafe { Arc::from_raw_addrefed(Gecko_ElementTransitions_EndValueAt(self.0, i)) }; + let property = end_value.id(); + debug_assert!(!property.is_logical()); + map.insert(property.to_owned(), end_value); + } + map + } + + fn needs_transitions_update_per_property( + &self, + property_declaration_id: PropertyDeclarationId, + combined_duration_seconds: f32, + before_change_style: &ComputedValues, + after_change_style: &ComputedValues, + existing_transitions: &FxHashMap>, + ) -> bool { + use crate::values::animated::{Animate, Procedure}; + debug_assert!(!property_declaration_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(&property_declaration_id.to_owned()) { + let after_value = + AnimationValue::from_computed_values(property_declaration_id, after_change_style) + .unwrap(); + + return ***existing != after_value; + } + + let from = + AnimationValue::from_computed_values(property_declaration_id, before_change_style); + let to = AnimationValue::from_computed_values(property_declaration_id, after_change_style); + + debug_assert_eq!(to.is_some(), from.is_some()); + + combined_duration_seconds > 0.0f32 && + from != to && + from.unwrap() + .animate( + to.as_ref().unwrap(), + Procedure::Interpolate { progress: 0.5 }, + ) + .is_ok() + } + + /// Get slow selector flags required for nth-of invalidation. + pub fn slow_selector_flags(&self) -> ElementSelectorFlags { + slow_selector_flags_from_node_selector_flags(self.as_node().selector_flags()) + } +} + +/// Convert slow selector flags from the raw `NodeSelectorFlags`. +pub fn slow_selector_flags_from_node_selector_flags(flags: u32) -> ElementSelectorFlags { + use crate::gecko_bindings::structs::NodeSelectorFlags; + let mut result = ElementSelectorFlags::empty(); + if flags & NodeSelectorFlags::HasSlowSelector.0 != 0 { + result.insert(ElementSelectorFlags::HAS_SLOW_SELECTOR); + } + if flags & NodeSelectorFlags::HasSlowSelectorLaterSiblings.0 != 0 { + result.insert(ElementSelectorFlags::HAS_SLOW_SELECTOR_LATER_SIBLINGS); + } + result +} + +/// 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::NodeSelectorFlags; + let mut gecko_flags = 0u32; + if flags.contains(ElementSelectorFlags::HAS_SLOW_SELECTOR) { + gecko_flags |= NodeSelectorFlags::HasSlowSelector.0; + } + if flags.contains(ElementSelectorFlags::HAS_SLOW_SELECTOR_LATER_SIBLINGS) { + gecko_flags |= NodeSelectorFlags::HasSlowSelectorLaterSiblings.0; + } + if flags.contains(ElementSelectorFlags::HAS_SLOW_SELECTOR_NTH) { + gecko_flags |= NodeSelectorFlags::HasSlowSelectorNth.0; + } + if flags.contains(ElementSelectorFlags::HAS_SLOW_SELECTOR_NTH_OF) { + gecko_flags |= NodeSelectorFlags::HasSlowSelectorNthOf.0; + } + if flags.contains(ElementSelectorFlags::HAS_EDGE_CHILD_SELECTOR) { + gecko_flags |= NodeSelectorFlags::HasEdgeChildSelector.0; + } + if flags.contains(ElementSelectorFlags::HAS_EMPTY_SELECTOR) { + gecko_flags |= NodeSelectorFlags::HasEmptySelector.0; + } + if flags.contains(ElementSelectorFlags::ANCHORS_RELATIVE_SELECTOR) { + gecko_flags |= NodeSelectorFlags::RelativeSelectorAnchor.0; + } + if flags.contains(ElementSelectorFlags::ANCHORS_RELATIVE_SELECTOR_NON_SUBJECT) { + gecko_flags |= NodeSelectorFlags::RelativeSelectorAnchorNonSubject.0; + } + if flags.contains(ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR) { + gecko_flags |= NodeSelectorFlags::RelativeSelectorSearchDirectionAncestor.0; + } + if flags.contains(ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_SIBLING) { + gecko_flags |= NodeSelectorFlags::RelativeSelectorSearchDirectionSibling.0; + } + + gecko_flags +} + +fn get_animation_rule( + element: &GeckoElement, + cascade_level: CascadeLevel, +) -> Option>> { + // 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(crate::properties::property_counts::ANIMATABLE), + Default::default(), + ); + if unsafe { Gecko_GetAnimationRule(element.0, cascade_level, &mut animation_values) } { + let shared_lock = &GLOBAL_STYLE_DATA.shared_lock; + Some(Arc::new(shared_lock.wrap( + PropertyDeclarationBlock::from_animation_value_map(&animation_values), + ))) + } else { + None + } +} + +/// Turns a gecko namespace id into an atom. Might panic if you pass any random thing that isn't a +/// namespace id. +#[inline(always)] +pub unsafe fn namespace_id_to_atom(id: i32) -> *mut nsAtom { + unsafe { + let namespace_manager = structs::nsNameSpaceManager_sInstance.mRawPtr; + (*namespace_manager).mURIArray[id as usize].mRawPtr + } +} + +impl<'le> TElement for GeckoElement<'le> { + type ConcreteNode = GeckoNode<'le>; + type TraversalChildrenIterator = GeckoChildrenIterator<'le>; + + fn inheritance_parent(&self) -> Option { + if self.is_pseudo_element() { + return self.pseudo_element_originating_element(); + } + + self.as_node() + .flattened_tree_parent() + .and_then(|n| n.as_element()) + } + + fn traversal_children(&self) -> LayoutIterator> { + // This condition is similar to the check that + // StyleChildrenIterator::IsNeeded does, except that it might return + // true if we used to (but no longer) have anonymous content from + // ::before/::after, or nsIAnonymousContentCreators. + if self.is_html_slot_element() || + self.shadow_root().is_some() || + self.may_have_anonymous_children() + { + unsafe { + let mut iter: structs::StyleChildrenIterator = ::std::mem::zeroed(); + bindings::Gecko_ConstructStyleChildrenIterator(self.0, &mut iter); + return LayoutIterator(GeckoChildrenIterator::GeckoIterator(iter)); + } + } + + LayoutIterator(GeckoChildrenIterator::Current(self.as_node().first_child())) + } + + fn before_pseudo_element(&self) -> Option { + self.before_or_after_pseudo(/* is_before = */ true) + } + + fn after_pseudo_element(&self) -> Option { + self.before_or_after_pseudo(/* is_before = */ false) + } + + fn marker_pseudo_element(&self) -> Option { + if !self.has_properties() { + return None; + } + + unsafe { + bindings::Gecko_GetMarkerPseudo(self.0) + .as_ref() + .map(GeckoElement) + } + } + + #[inline] + fn is_html_element(&self) -> bool { + self.namespace_id() == structs::kNameSpaceID_XHTML as i32 + } + + #[inline] + fn is_mathml_element(&self) -> bool { + self.namespace_id() == structs::kNameSpaceID_MathML as i32 + } + + #[inline] + fn is_svg_element(&self) -> bool { + self.namespace_id() == structs::kNameSpaceID_SVG as i32 + } + + #[inline] + fn is_xul_element(&self) -> bool { + self.namespace_id() == structs::root::kNameSpaceID_XUL as i32 + } + + #[inline] + fn local_name(&self) -> &WeakAtom { + unsafe { WeakAtom::new(self.as_node().node_info().mInner.mName) } + } + + #[inline] + fn namespace(&self) -> &WeakNamespace { + unsafe { WeakNamespace::new(namespace_id_to_atom(self.namespace_id())) } + } + + #[inline] + fn query_container_size(&self, display: &Display) -> Size2D> { + // If an element gets 'display: contents' and its nsIFrame has not been removed yet, + // Gecko_GetQueryContainerSize will not notice that it can't have size containment. + // Other cases like 'display: inline' will be handled once the new nsIFrame is created. + if display.is_contents() { + return Size2D::new(None, None); + } + + let mut width = -1; + let mut height = -1; + unsafe { + bindings::Gecko_GetQueryContainerSize(self.0, &mut width, &mut height); + } + Size2D::new( + if width >= 0 { Some(Au(width)) } else { None }, + if height >= 0 { Some(Au(height)) } else { None }, + ) + } + + /// Return the list of slotted nodes of this node. + #[inline] + fn slotted_nodes(&self) -> &[Self::ConcreteNode] { + if !self.is_html_slot_element() || !self.as_node().is_in_shadow_tree() { + return &[]; + } + + let slot: &structs::HTMLSlotElement = unsafe { mem::transmute(self.0) }; + + if cfg!(debug_assertions) { + let base: &RawGeckoElement = &slot._base._base._base; + assert_eq!(base as *const _, self.0 as *const _, "Bad cast"); + } + + // FIXME(emilio): Workaround a bindgen bug on Android that causes + // mAssignedNodes to be at the wrong offset. See bug 1466406. + // + // Bug 1466580 tracks running the Android layout tests on automation. + // + // The actual bindgen bug still needs reduction. + let assigned_nodes: &[structs::RefPtr] = if !cfg!(target_os = "android") { + debug_assert_eq!( + unsafe { bindings::Gecko_GetAssignedNodes(self.0) }, + &slot.mAssignedNodes as *const _, + ); + + &*slot.mAssignedNodes + } else { + unsafe { &**bindings::Gecko_GetAssignedNodes(self.0) } + }; + + debug_assert_eq!( + mem::size_of::>(), + mem::size_of::(), + "Bad cast!" + ); + + unsafe { mem::transmute(assigned_nodes) } + } + + #[inline] + fn shadow_root(&self) -> Option> { + let slots = self.extended_slots()?; + unsafe { slots.mShadowRoot.mRawPtr.as_ref().map(GeckoShadowRoot) } + } + + #[inline] + fn containing_shadow(&self) -> Option> { + let slots = self.extended_slots()?; + unsafe { + slots + ._base + .mContainingShadow + .mRawPtr + .as_ref() + .map(GeckoShadowRoot) + } + } + + fn each_anonymous_content_child(&self, mut f: F) + where + F: FnMut(Self), + { + if !self.may_have_anonymous_children() { + return; + } + + let array: *mut structs::nsTArray<*mut nsIContent> = + unsafe { bindings::Gecko_GetAnonymousContentForElement(self.0) }; + + if array.is_null() { + return; + } + + for content in unsafe { &**array } { + let node = GeckoNode::from_content(unsafe { &**content }); + let element = match node.as_element() { + Some(e) => e, + None => continue, + }; + + f(element); + } + + unsafe { bindings::Gecko_DestroyAnonymousContentList(array) }; + } + + #[inline] + fn as_node(&self) -> Self::ConcreteNode { + unsafe { GeckoNode(&*(self.0 as *const _ as *const RawGeckoNode)) } + } + + fn owner_doc_matches_for_testing(&self, device: &Device) -> bool { + self.as_node().owner_doc().0 as *const structs::Document == device.document() as *const _ + } + + fn style_attribute(&self) -> Option>> { + if !self.may_have_style_attribute() { + return None; + } + + unsafe { + let declarations = Gecko_GetStyleAttrDeclarationBlock(self.0).as_ref()?; + Some(ArcBorrow::from_ref(declarations)) + } + } + + fn unset_dirty_style_attribute(&self) { + if !self.may_have_style_attribute() { + return; + } + + unsafe { Gecko_UnsetDirtyStyleAttr(self.0) }; + } + + fn smil_override(&self) -> Option>> { + unsafe { + let slots = self.extended_slots()?; + + let declaration: &structs::DeclarationBlock = + slots.mSMILOverrideStyleDeclaration.mRawPtr.as_ref()?; + + let raw: &structs::StyleLockedDeclarationBlock = declaration.mRaw.mRawPtr.as_ref()?; + Some(ArcBorrow::from_ref(raw)) + } + } + + fn animation_rule( + &self, + _: &SharedStyleContext, + ) -> Option>> { + get_animation_rule(self, CascadeLevel::Animations) + } + + fn transition_rule( + &self, + _: &SharedStyleContext, + ) -> Option>> { + get_animation_rule(self, CascadeLevel::Transitions) + } + + #[inline] + fn state(&self) -> ElementState { + ElementState::from_bits_retain(self.state_internal()) + } + + #[inline] + fn has_custom_state(&self, state: &CustomState) -> bool { + if !self.is_html_element() { + return false; + } + let check_state_ptr: *const nsAtom = state.0.as_ptr(); + self.extended_slots().map_or(false, |slot| { + (&slot.mCustomStates).iter().any(|setstate| { + let setstate_ptr: *const nsAtom = setstate.mRawPtr; + setstate_ptr == check_state_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_attr_name(&self, mut callback: F) + where + F: FnMut(&AtomIdent), + { + for attr in self.attrs() { + unsafe { AtomIdent::with(attr.mName.name(), |a| callback(a)) } + } + } + + fn each_class(&self, callback: F) + where + F: FnMut(&AtomIdent), + { + let attr = match self.get_class_attr() { + Some(c) => c, + None => return, + }; + + snapshot_helpers::each_class_or_part(attr, callback) + } + + #[inline] + fn each_exported_part(&self, name: &AtomIdent, callback: F) + where + F: FnMut(&AtomIdent), + { + snapshot_helpers::each_exported_part(self.attrs(), name, callback) + } + + fn each_part(&self, callback: F) + where + F: FnMut(&AtomIdent), + { + let attr = match self.get_part_attr() { + Some(c) => c, + None => return, + }; + + snapshot_helpers::each_class_or_part(attr, callback) + } + + #[inline] + fn has_snapshot(&self) -> bool { + self.flags() & ELEMENT_HAS_SNAPSHOT != 0 + } + + #[inline] + fn handled_snapshot(&self) -> bool { + self.flags() & ELEMENT_HANDLED_SNAPSHOT != 0 + } + + unsafe fn set_handled_snapshot(&self) { + debug_assert!(self.has_data()); + self.set_flags(ELEMENT_HANDLED_SNAPSHOT) + } + + #[inline] + fn has_dirty_descendants(&self) -> bool { + self.flags() & ELEMENT_HAS_DIRTY_DESCENDANTS_FOR_SERVO != 0 + } + + unsafe fn set_dirty_descendants(&self) { + debug_assert!(self.has_data()); + debug!("Setting dirty descendants: {:?}", self); + self.set_flags(ELEMENT_HAS_DIRTY_DESCENDANTS_FOR_SERVO) + } + + unsafe fn unset_dirty_descendants(&self) { + self.unset_flags(ELEMENT_HAS_DIRTY_DESCENDANTS_FOR_SERVO) + } + + #[inline] + fn has_animation_only_dirty_descendants(&self) -> bool { + self.flags() & ELEMENT_HAS_ANIMATION_ONLY_DIRTY_DESCENDANTS_FOR_SERVO != 0 + } + + unsafe fn set_animation_only_dirty_descendants(&self) { + self.set_flags(ELEMENT_HAS_ANIMATION_ONLY_DIRTY_DESCENDANTS_FOR_SERVO) + } + + unsafe fn unset_animation_only_dirty_descendants(&self) { + self.unset_flags(ELEMENT_HAS_ANIMATION_ONLY_DIRTY_DESCENDANTS_FOR_SERVO) + } + + unsafe fn clear_descendant_bits(&self) { + self.unset_flags( + ELEMENT_HAS_DIRTY_DESCENDANTS_FOR_SERVO | + ELEMENT_HAS_ANIMATION_ONLY_DIRTY_DESCENDANTS_FOR_SERVO | + NODE_DESCENDANTS_NEED_FRAMES, + ) + } + + fn is_visited_link(&self) -> bool { + self.state().intersects(ElementState::VISITED) + } + + /// We want to match rules from the same tree in all cases, except for native anonymous content + /// that _isn't_ part directly of a UA widget (e.g., such generated by form controls, or + /// pseudo-elements). + #[inline] + fn matches_user_and_content_rules(&self) -> bool { + use crate::gecko_bindings::structs::{ + NODE_HAS_BEEN_IN_UA_WIDGET, NODE_IS_IN_NATIVE_ANONYMOUS_SUBTREE, + }; + let flags = self.flags(); + (flags & NODE_IS_IN_NATIVE_ANONYMOUS_SUBTREE) == 0 || + (flags & NODE_HAS_BEEN_IN_UA_WIDGET) != 0 + } + + #[inline] + fn implemented_pseudo_element(&self) -> Option { + if self.matches_user_and_content_rules() { + return None; + } + + if !self.has_properties() { + return None; + } + + PseudoElement::from_pseudo_type( + unsafe { bindings::Gecko_GetImplementedPseudo(self.0) }, + None, + ) + } + + #[inline] + fn store_children_to_process(&self, _: isize) { + // This is only used for bottom-up traversal, and is thus a no-op for Gecko. + } + + fn did_process_child(&self) -> isize { + panic!("Atomic child count not implemented in Gecko"); + } + + unsafe fn ensure_data(&self) -> AtomicRefMut { + if !self.has_data() { + debug!("Creating ElementData for {:?}", self); + let ptr = Box::into_raw(Box::new(AtomicRefCell::new(ElementData::default()))); + self.0.mServoData.set(ptr); + } + self.mutate_data().unwrap() + } + + unsafe fn clear_data(&self) { + let ptr = self.0.mServoData.get(); + self.unset_flags( + ELEMENT_HAS_SNAPSHOT | + ELEMENT_HANDLED_SNAPSHOT | + structs::Element_kAllServoDescendantBits | + NODE_NEEDS_FRAME, + ); + if !ptr.is_null() { + debug!("Dropping ElementData for {:?}", self); + let data = Box::from_raw(self.0.mServoData.get()); + self.0.mServoData.set(ptr::null_mut()); + + // Perform a mutable borrow of the data in debug builds. This + // serves as an assertion that there are no outstanding borrows + // when we destroy the data. + debug_assert!({ + let _ = data.borrow_mut(); + true + }); + } + } + + #[inline] + fn skip_item_display_fixup(&self) -> bool { + debug_assert!( + !self.is_pseudo_element(), + "Just don't call me if I'm a pseudo, you should know the answer already" + ); + self.is_root_of_native_anonymous_subtree() + } + + #[inline] + fn may_have_animations(&self) -> bool { + if let Some(pseudo) = self.implemented_pseudo_element() { + if pseudo.animations_stored_in_parent() { + // FIXME(emilio): When would the parent of a ::before / ::after + // pseudo-element be null? + return self.parent_element().map_or(false, |p| { + p.as_node() + .get_bool_flag(nsINode_BooleanFlag::ElementHasAnimations) + }); + } + } + self.as_node() + .get_bool_flag(nsINode_BooleanFlag::ElementHasAnimations) + } + + /// Process various tasks that are a result of animation-only restyle. + fn process_post_animation(&self, tasks: PostAnimationTasks) { + debug_assert!(!tasks.is_empty(), "Should be involved a task"); + + // If display style was changed from none to other, we need to resolve + // the descendants in the display:none subtree. Instead of resolving + // those styles in animation-only restyle, we defer it to a subsequent + // normal restyle. + if tasks.intersects(PostAnimationTasks::DISPLAY_CHANGED_FROM_NONE_FOR_SMIL) { + debug_assert!( + self.implemented_pseudo_element() + .map_or(true, |p| !p.is_before_or_after()), + "display property animation shouldn't run on pseudo elements \ + since it's only for SMIL" + ); + unsafe { + self.note_explicit_hints( + RestyleHint::restyle_subtree(), + nsChangeHint::nsChangeHint_Empty, + ); + } + } + } + + /// Update various animation-related state on a given (pseudo-)element as + /// results of normal restyle. + fn update_animations( + &self, + before_change_style: Option>, + tasks: UpdateAnimationsTasks, + ) { + // We have to update animations even if the element has no computed + // style since it means the element is in a display:none subtree, we + // should destroy all CSS animations in display:none subtree. + let computed_data = self.borrow_data(); + let computed_values = computed_data.as_ref().map(|d| d.styles.primary()); + let before_change_values = before_change_style + .as_ref() + .map_or(ptr::null(), |x| x.as_gecko_computed_style()); + let computed_values_opt = computed_values + .as_ref() + .map_or(ptr::null(), |x| x.as_gecko_computed_style()); + unsafe { + Gecko_UpdateAnimations( + self.0, + before_change_values, + computed_values_opt, + tasks.bits(), + ); + } + } + + #[inline] + fn has_animations(&self, _: &SharedStyleContext) -> bool { + self.has_any_animation() + } + + fn has_css_animations(&self, _: &SharedStyleContext, _: Option) -> bool { + self.may_have_animations() && unsafe { Gecko_ElementHasCSSAnimations(self.0) } + } + + fn has_css_transitions(&self, _: &SharedStyleContext, _: Option) -> bool { + self.may_have_animations() && unsafe { Gecko_ElementHasCSSTransitions(self.0) } + } + + // Detect if there are any changes that require us to update transitions. + // + // This is used as a more thoroughgoing check than the cheaper + // might_need_transitions_update check. + // + // The following logic shadows the logic used on the Gecko side + // (nsTransitionManager::DoUpdateTransitions) where we actually perform the + // update. + // + // https://drafts.csswg.org/css-transitions/#starting + fn needs_transitions_update( + &self, + before_change_style: &ComputedValues, + after_change_style: &ComputedValues, + ) -> bool { + let after_change_ui_style = after_change_style.get_ui(); + let existing_transitions = self.css_transitions_info(); + + if after_change_style.get_box().clone_display().is_none() { + // We need to cancel existing transitions. + return !existing_transitions.is_empty(); + } + + let mut transitions_to_keep = PropertyDeclarationIdSet::default(); + for transition_property in after_change_style.transition_properties() { + let physical_longhand = PropertyDeclarationId::Longhand( + transition_property + .longhand_id + .to_physical(after_change_style.writing_mode), + ); + transitions_to_keep.insert(physical_longhand); + if self.needs_transitions_update_per_property( + physical_longhand, + after_change_ui_style + .transition_combined_duration_at(transition_property.index) + .seconds(), + before_change_style, + after_change_style, + &existing_transitions, + ) { + return true; + } + } + + // Check if we have to cancel the running transition because this is not + // a matching transition-property value. + existing_transitions + .keys() + .any(|property| !transitions_to_keep.contains(property.as_borrowed())) + } + + /// Whether there is an ElementData container. + #[inline] + fn has_data(&self) -> bool { + self.get_data().is_some() + } + + /// Immutably borrows the ElementData. + fn borrow_data(&self) -> Option> { + self.get_data().map(|x| x.borrow()) + } + + /// Mutably borrows the ElementData. + fn mutate_data(&self) -> Option> { + self.get_data().map(|x| x.borrow_mut()) + } + + #[inline] + fn lang_attr(&self) -> Option { + let ptr = unsafe { bindings::Gecko_LangValue(self.0) }; + if ptr.is_null() { + None + } else { + Some(AtomString(unsafe { Atom::from_addrefed(ptr) })) + } + } + + fn match_element_lang(&self, override_lang: Option>, value: &Lang) -> bool { + // Gecko supports :lang() from CSS Selectors 4, which accepts a list + // of language tags, and does BCP47-style range matching. + let override_lang_ptr = match override_lang { + Some(Some(ref atom)) => atom.as_ptr(), + _ => ptr::null_mut(), + }; + value.0.iter().any(|lang| unsafe { + Gecko_MatchLang( + self.0, + override_lang_ptr, + override_lang.is_some(), + lang.as_slice().as_ptr(), + ) + }) + } + + fn is_html_document_body_element(&self) -> bool { + if self.local_name() != &**local_name!("body") { + return false; + } + + if !self.is_html_element() { + return false; + } + + unsafe { bindings::Gecko_IsDocumentBody(self.0) } + } + + fn synthesize_presentational_hints_for_legacy_attributes( + &self, + visited_handling: VisitedHandlingMode, + hints: &mut V, + ) where + V: Push, + { + use crate::properties::longhands::_x_lang::SpecifiedValue as SpecifiedLang; + use crate::properties::longhands::color::SpecifiedValue as SpecifiedColor; + use crate::stylesheets::layer_rule::LayerOrder; + use crate::values::specified::{color::Color, font::XTextScale}; + lazy_static! { + static ref TABLE_COLOR_RULE: ApplicableDeclarationBlock = { + let global_style_data = &*GLOBAL_STYLE_DATA; + let pdb = PropertyDeclarationBlock::with_one( + PropertyDeclaration::Color(SpecifiedColor(Color::InheritFromBodyQuirk.into())), + Importance::Normal, + ); + let arc = Arc::new_leaked(global_style_data.shared_lock.wrap(pdb)); + ApplicableDeclarationBlock::from_declarations( + arc, + ServoCascadeLevel::PresHints, + LayerOrder::root(), + ) + }; + static ref MATHML_LANG_RULE: ApplicableDeclarationBlock = { + let global_style_data = &*GLOBAL_STYLE_DATA; + let pdb = PropertyDeclarationBlock::with_one( + PropertyDeclaration::XLang(SpecifiedLang(atom!("x-math"))), + Importance::Normal, + ); + let arc = Arc::new_leaked(global_style_data.shared_lock.wrap(pdb)); + ApplicableDeclarationBlock::from_declarations( + arc, + ServoCascadeLevel::PresHints, + LayerOrder::root(), + ) + }; + static ref SVG_TEXT_DISABLE_SCALE_RULE: ApplicableDeclarationBlock = { + let global_style_data = &*GLOBAL_STYLE_DATA; + let pdb = PropertyDeclarationBlock::with_one( + PropertyDeclaration::XTextScale(XTextScale::None), + Importance::Normal, + ); + let arc = Arc::new_leaked(global_style_data.shared_lock.wrap(pdb)); + ApplicableDeclarationBlock::from_declarations( + arc, + ServoCascadeLevel::PresHints, + LayerOrder::root(), + ) + }; + }; + + let ns = self.namespace_id(); + //
elements get a default MozCenterOrInherit which may get overridden + if ns == structs::kNameSpaceID_XHTML as i32 { + if self.local_name().as_ptr() == atom!("table").as_ptr() && + self.as_node().owner_doc().quirks_mode() == QuirksMode::Quirks + { + hints.push(TABLE_COLOR_RULE.clone()); + } + } + if ns == structs::kNameSpaceID_SVG as i32 { + if self.local_name().as_ptr() == atom!("text").as_ptr() { + hints.push(SVG_TEXT_DISABLE_SCALE_RULE.clone()); + } + } + let declarations = + unsafe { Gecko_GetHTMLPresentationAttrDeclarationBlock(self.0).as_ref() }; + if let Some(decl) = declarations { + hints.push(ApplicableDeclarationBlock::from_declarations( + unsafe { Arc::from_raw_addrefed(decl) }, + ServoCascadeLevel::PresHints, + LayerOrder::root(), + )); + } + let declarations = unsafe { Gecko_GetExtraContentStyleDeclarations(self.0).as_ref() }; + if let Some(decl) = declarations { + hints.push(ApplicableDeclarationBlock::from_declarations( + unsafe { Arc::from_raw_addrefed(decl) }, + ServoCascadeLevel::PresHints, + LayerOrder::root(), + )); + } + + // Support for link, vlink, and alink presentation hints on + if self.is_link() { + // Unvisited vs. visited styles are computed up-front based on the + // visited mode (not the element's actual state). + let declarations = match visited_handling { + VisitedHandlingMode::AllLinksVisitedAndUnvisited => { + unreachable!( + "We should never try to selector match with \ + AllLinksVisitedAndUnvisited" + ); + }, + VisitedHandlingMode::AllLinksUnvisited => unsafe { + Gecko_GetUnvisitedLinkAttrDeclarationBlock(self.0).as_ref() + }, + VisitedHandlingMode::RelevantLinkVisited => unsafe { + Gecko_GetVisitedLinkAttrDeclarationBlock(self.0).as_ref() + }, + }; + if let Some(decl) = declarations { + hints.push(ApplicableDeclarationBlock::from_declarations( + unsafe { Arc::from_raw_addrefed(decl) }, + ServoCascadeLevel::PresHints, + LayerOrder::root(), + )); + } + + let active = self + .state() + .intersects(NonTSPseudoClass::Active.state_flag()); + if active { + let declarations = + unsafe { Gecko_GetActiveLinkAttrDeclarationBlock(self.0).as_ref() }; + if let Some(decl) = declarations { + hints.push(ApplicableDeclarationBlock::from_declarations( + unsafe { Arc::from_raw_addrefed(decl) }, + ServoCascadeLevel::PresHints, + LayerOrder::root(), + )); + } + } + } + + // xml:lang has precedence over lang, which can be + // set by Gecko_GetHTMLPresentationAttrDeclarationBlock + // + // http://www.whatwg.org/specs/web-apps/current-work/multipage/elements.html#language + let ptr = unsafe { bindings::Gecko_GetXMLLangValue(self.0) }; + if !ptr.is_null() { + let global_style_data = &*GLOBAL_STYLE_DATA; + + let pdb = PropertyDeclarationBlock::with_one( + PropertyDeclaration::XLang(SpecifiedLang(unsafe { Atom::from_addrefed(ptr) })), + Importance::Normal, + ); + let arc = Arc::new(global_style_data.shared_lock.wrap(pdb)); + hints.push(ApplicableDeclarationBlock::from_declarations( + arc, + ServoCascadeLevel::PresHints, + LayerOrder::root(), + )) + } + // MathML's default lang has precedence over both `lang` and `xml:lang` + if ns == structs::kNameSpaceID_MathML as i32 { + if self.local_name().as_ptr() == atom!("math").as_ptr() { + hints.push(MATHML_LANG_RULE.clone()); + } + } + } + + fn has_selector_flags(&self, flags: ElementSelectorFlags) -> bool { + let node_flags = selector_flags_to_node_flags(flags); + self.as_node().selector_flags() & node_flags == node_flags + } + + fn relative_selector_search_direction(&self) -> Option { + use crate::gecko_bindings::structs::NodeSelectorFlags; + let flags = self.as_node().selector_flags(); + if (flags & NodeSelectorFlags::RelativeSelectorSearchDirectionAncestorSibling.0) != 0 { + Some(ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR_SIBLING) + } else if (flags & NodeSelectorFlags::RelativeSelectorSearchDirectionAncestor.0) != 0 { + Some(ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR) + } else if (flags & NodeSelectorFlags::RelativeSelectorSearchDirectionSibling.0) != 0 { + Some(ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_SIBLING) + } else { + None + } + } +} + +impl<'le> PartialEq for GeckoElement<'le> { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.0 as *const _ == other.0 as *const _ + } +} + +impl<'le> Eq for GeckoElement<'le> {} + +impl<'le> Hash for GeckoElement<'le> { + #[inline] + fn hash(&self, state: &mut H) { + (self.0 as *const RawGeckoElement).hash(state); + } +} + +impl<'le> ::selectors::Element for GeckoElement<'le> { + type Impl = SelectorImpl; + + #[inline] + fn opaque(&self) -> OpaqueElement { + OpaqueElement::new(self.0) + } + + #[inline] + fn parent_element(&self) -> Option { + let parent_node = self.as_node().parent_node(); + parent_node.and_then(|n| n.as_element()) + } + + #[inline] + fn parent_node_is_shadow_root(&self) -> bool { + self.as_node() + .parent_node() + .map_or(false, |p| p.is_shadow_root()) + } + + #[inline] + fn containing_shadow_host(&self) -> Option { + let shadow = self.containing_shadow()?; + Some(shadow.host()) + } + + #[inline] + fn is_pseudo_element(&self) -> bool { + self.implemented_pseudo_element().is_some() + } + + #[inline] + fn pseudo_element_originating_element(&self) -> Option { + debug_assert!(self.is_pseudo_element()); + debug_assert!(!self.matches_user_and_content_rules()); + let mut current = *self; + loop { + if current.is_root_of_native_anonymous_subtree() { + return current.traversal_parent(); + } + + current = current.traversal_parent()?; + } + } + + #[inline] + fn assigned_slot(&self) -> Option { + let slot = self.extended_slots()?._base.mAssignedSlot.mRawPtr; + + unsafe { Some(GeckoElement(&slot.as_ref()?._base._base._base)) } + } + + #[inline] + fn prev_sibling_element(&self) -> Option { + let mut sibling = self.as_node().prev_sibling(); + while let Some(sibling_node) = sibling { + if let Some(el) = sibling_node.as_element() { + return Some(el); + } + sibling = sibling_node.prev_sibling(); + } + None + } + + #[inline] + fn next_sibling_element(&self) -> Option { + let mut sibling = self.as_node().next_sibling(); + while let Some(sibling_node) = sibling { + if let Some(el) = sibling_node.as_element() { + return Some(el); + } + sibling = sibling_node.next_sibling(); + } + None + } + + #[inline] + fn first_element_child(&self) -> Option { + let mut child = self.as_node().first_child(); + while let Some(child_node) = child { + if let Some(el) = child_node.as_element() { + return Some(el); + } + child = child_node.next_sibling(); + } + None + } + + fn apply_selector_flags(&self, flags: ElementSelectorFlags) { + // Handle flags that apply to the element. + let self_flags = flags.for_self(); + if !self_flags.is_empty() { + self.as_node() + .set_selector_flags(selector_flags_to_node_flags(flags)) + } + + // Handle flags that apply to the parent. + let parent_flags = flags.for_parent(); + if !parent_flags.is_empty() { + if let Some(p) = self.as_node().parent_node() { + if p.is_element() || p.is_shadow_root() { + p.set_selector_flags(selector_flags_to_node_flags(parent_flags)); + } + } + } + } + + fn has_attr_in_no_namespace(&self, local_name: &LocalName) -> bool { + for attr in self.attrs() { + if attr.mName.mBits == local_name.as_ptr() as usize { + return true; + } + } + false + } + + fn attr_matches( + &self, + ns: &NamespaceConstraint<&Namespace>, + local_name: &LocalName, + operation: &AttrSelectorOperation<&AttrValue>, + ) -> bool { + snapshot_helpers::attr_matches(self.attrs(), ns, local_name, operation) + } + + #[inline] + fn is_root(&self) -> bool { + if self + .as_node() + .get_bool_flag(nsINode_BooleanFlag::ParentIsContent) + { + return false; + } + + if !self.as_node().is_in_document() { + return false; + } + + debug_assert!(self + .as_node() + .parent_node() + .map_or(false, |p| p.is_document())); + // XXX this should always return true at this point, shouldn't it? + unsafe { bindings::Gecko_IsRootElement(self.0) } + } + + fn is_empty(&self) -> bool { + !self + .as_node() + .dom_children() + .any(|child| unsafe { Gecko_IsSignificantChild(child.0, true) }) + } + + #[inline] + fn has_local_name(&self, name: &WeakAtom) -> bool { + self.local_name() == name + } + + #[inline] + fn has_namespace(&self, ns: &WeakNamespace) -> bool { + self.namespace() == ns + } + + #[inline] + fn is_same_type(&self, other: &Self) -> bool { + self.local_name() == other.local_name() && self.namespace() == other.namespace() + } + + fn match_non_ts_pseudo_class( + &self, + pseudo_class: &NonTSPseudoClass, + context: &mut MatchingContext, + ) -> bool { + use selectors::matching::*; + match *pseudo_class { + NonTSPseudoClass::Autofill | + NonTSPseudoClass::Defined | + NonTSPseudoClass::Focus | + NonTSPseudoClass::Enabled | + NonTSPseudoClass::Disabled | + NonTSPseudoClass::Checked | + NonTSPseudoClass::Fullscreen | + NonTSPseudoClass::Indeterminate | + NonTSPseudoClass::MozInert | + NonTSPseudoClass::PopoverOpen | + NonTSPseudoClass::PlaceholderShown | + NonTSPseudoClass::Target | + NonTSPseudoClass::Valid | + NonTSPseudoClass::Invalid | + NonTSPseudoClass::MozBroken | + NonTSPseudoClass::Required | + NonTSPseudoClass::Optional | + NonTSPseudoClass::ReadOnly | + NonTSPseudoClass::ReadWrite | + NonTSPseudoClass::FocusWithin | + NonTSPseudoClass::FocusVisible | + NonTSPseudoClass::MozDragOver | + NonTSPseudoClass::MozDevtoolsHighlighted | + NonTSPseudoClass::MozStyleeditorTransitioning | + NonTSPseudoClass::MozMathIncrementScriptLevel | + NonTSPseudoClass::InRange | + NonTSPseudoClass::OutOfRange | + NonTSPseudoClass::Default | + NonTSPseudoClass::UserValid | + NonTSPseudoClass::UserInvalid | + NonTSPseudoClass::MozMeterOptimum | + NonTSPseudoClass::MozMeterSubOptimum | + NonTSPseudoClass::MozMeterSubSubOptimum | + NonTSPseudoClass::MozHasDirAttr | + NonTSPseudoClass::MozDirAttrLTR | + NonTSPseudoClass::MozDirAttrRTL | + NonTSPseudoClass::MozDirAttrLikeAuto | + NonTSPseudoClass::Modal | + NonTSPseudoClass::MozTopmostModal | + NonTSPseudoClass::Active | + NonTSPseudoClass::Hover | + NonTSPseudoClass::MozAutofillPreview | + NonTSPseudoClass::MozRevealed | + NonTSPseudoClass::MozValueEmpty => self.state().intersects(pseudo_class.state_flag()), + // TODO: This applying only to HTML elements is weird. + NonTSPseudoClass::Dir(ref dir) => { + self.is_html_element() && self.state().intersects(dir.element_state()) + }, + NonTSPseudoClass::AnyLink => self.is_link(), + NonTSPseudoClass::Link => { + self.is_link() && context.visited_handling().matches_unvisited() + }, + NonTSPseudoClass::CustomState(ref state) => self.has_custom_state(state), + NonTSPseudoClass::Visited => { + self.is_link() && context.visited_handling().matches_visited() + }, + NonTSPseudoClass::MozFirstNode => { + if context.needs_selector_flags() { + self.apply_selector_flags(ElementSelectorFlags::HAS_EDGE_CHILD_SELECTOR); + } + let mut elem = self.as_node(); + while let Some(prev) = elem.prev_sibling() { + if prev.contains_non_whitespace_content() { + return false; + } + elem = prev; + } + true + }, + NonTSPseudoClass::MozLastNode => { + if context.needs_selector_flags() { + self.apply_selector_flags(ElementSelectorFlags::HAS_EDGE_CHILD_SELECTOR); + } + let mut elem = self.as_node(); + while let Some(next) = elem.next_sibling() { + if next.contains_non_whitespace_content() { + return false; + } + elem = next; + } + true + }, + NonTSPseudoClass::MozOnlyWhitespace => { + if context.needs_selector_flags() { + self.apply_selector_flags(ElementSelectorFlags::HAS_EMPTY_SELECTOR); + } + if self + .as_node() + .dom_children() + .any(|c| c.contains_non_whitespace_content()) + { + return false; + } + true + }, + NonTSPseudoClass::MozNativeAnonymous => !self.matches_user_and_content_rules(), + NonTSPseudoClass::MozTableBorderNonzero => unsafe { + bindings::Gecko_IsTableBorderNonzero(self.0) + }, + NonTSPseudoClass::MozSelectListBox => unsafe { + bindings::Gecko_IsSelectListBox(self.0) + }, + NonTSPseudoClass::MozIsHTML => self.as_node().owner_doc().is_html_document(), + NonTSPseudoClass::MozLWTheme | + NonTSPseudoClass::MozLocaleDir(..) | + NonTSPseudoClass::MozWindowInactive => { + let state_bit = pseudo_class.document_state_flag(); + if state_bit.is_empty() { + debug_assert!( + matches!(pseudo_class, NonTSPseudoClass::MozLocaleDir(..)), + "Only moz-locale-dir should ever return an empty state" + ); + return false; + } + if context + .extra_data + .invalidation_data + .document_state + .intersects(state_bit) + { + return !context.in_negation(); + } + self.document_state().contains(state_bit) + }, + NonTSPseudoClass::MozPlaceholder => false, + NonTSPseudoClass::Lang(ref lang_arg) => self.match_element_lang(None, lang_arg), + } + } + + fn match_pseudo_element( + &self, + pseudo_element: &PseudoElement, + _context: &mut MatchingContext, + ) -> bool { + // TODO(emilio): I believe we could assert we are a pseudo-element and + // match the proper pseudo-element, given how we rulehash the stuff + // based on the pseudo. + match self.implemented_pseudo_element() { + Some(ref pseudo) => *pseudo == *pseudo_element, + None => false, + } + } + + #[inline] + fn is_link(&self) -> bool { + self.state().intersects(ElementState::VISITED_OR_UNVISITED) + } + + #[inline] + fn has_id(&self, id: &AtomIdent, case_sensitivity: CaseSensitivity) -> bool { + if !self.has_id() { + return false; + } + + let element_id = match snapshot_helpers::get_id(self.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() + } + + fn add_element_unique_hashes(&self, filter: &mut BloomFilter) -> bool { + each_relevant_element_hash(*self, |hash| filter.insert_hash(hash & BLOOM_HASH_MASK)); + true + } +} diff --git a/servo/components/style/gecko_bindings/mod.rs b/servo/components/style/gecko_bindings/mod.rs new file mode 100644 index 0000000000..f0b0adc7ec --- /dev/null +++ b/servo/components/style/gecko_bindings/mod.rs @@ -0,0 +1,28 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Gecko's C++ bindings, along with some rust helpers to ease its use. + +// FIXME: We allow `improper_ctypes` (for now), because the lint doesn't allow +// foreign structs to have `PhantomData`. We should remove this once the lint +// ignores this case. + +#[allow( + dead_code, + improper_ctypes, + non_camel_case_types, + non_snake_case, + non_upper_case_globals, + missing_docs +)] +// TODO: Remove this when updating bindgen, see +// https://github.com/rust-lang/rust-bindgen/issues/1651 +#[cfg_attr(test, allow(deref_nullptr))] +pub mod structs { + include!(concat!(env!("OUT_DIR"), "/gecko/structs.rs")); +} + +pub use self::structs as bindings; + +pub mod sugar; diff --git a/servo/components/style/gecko_bindings/sugar/mod.rs b/servo/components/style/gecko_bindings/sugar/mod.rs new file mode 100644 index 0000000000..00faf63ba6 --- /dev/null +++ b/servo/components/style/gecko_bindings/sugar/mod.rs @@ -0,0 +1,13 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Rust sugar and convenience methods for Gecko types. + +mod ns_com_ptr; +mod ns_compatibility; +mod ns_style_auto_array; +mod ns_t_array; +pub mod origin_flags; +pub mod ownership; +pub mod refptr; diff --git a/servo/components/style/gecko_bindings/sugar/ns_com_ptr.rs b/servo/components/style/gecko_bindings/sugar/ns_com_ptr.rs new file mode 100644 index 0000000000..1c54541bd8 --- /dev/null +++ b/servo/components/style/gecko_bindings/sugar/ns_com_ptr.rs @@ -0,0 +1,15 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Little helpers for `nsCOMPtr`. + +use crate::gecko_bindings::structs::nsCOMPtr; + +impl nsCOMPtr { + /// Get this pointer as a raw pointer. + #[inline] + pub fn raw(&self) -> *mut T { + self.mRawPtr + } +} diff --git a/servo/components/style/gecko_bindings/sugar/ns_compatibility.rs b/servo/components/style/gecko_bindings/sugar/ns_compatibility.rs new file mode 100644 index 0000000000..f4b81e9f79 --- /dev/null +++ b/servo/components/style/gecko_bindings/sugar/ns_compatibility.rs @@ -0,0 +1,19 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Little helper for `nsCompatibility`. + +use crate::context::QuirksMode; +use crate::gecko_bindings::structs::nsCompatibility; + +impl From for QuirksMode { + #[inline] + fn from(mode: nsCompatibility) -> QuirksMode { + match mode { + nsCompatibility::eCompatibility_FullStandards => QuirksMode::NoQuirks, + nsCompatibility::eCompatibility_AlmostStandards => QuirksMode::LimitedQuirks, + nsCompatibility::eCompatibility_NavQuirks => QuirksMode::Quirks, + } + } +} diff --git a/servo/components/style/gecko_bindings/sugar/ns_style_auto_array.rs b/servo/components/style/gecko_bindings/sugar/ns_style_auto_array.rs new file mode 100644 index 0000000000..b5772a6c77 --- /dev/null +++ b/servo/components/style/gecko_bindings/sugar/ns_style_auto_array.rs @@ -0,0 +1,111 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Rust helpers for Gecko's `nsStyleAutoArray`. + +use crate::gecko_bindings::bindings::Gecko_EnsureStyleAnimationArrayLength; +use crate::gecko_bindings::bindings::Gecko_EnsureStyleScrollTimelineArrayLength; +use crate::gecko_bindings::bindings::Gecko_EnsureStyleTransitionArrayLength; +use crate::gecko_bindings::bindings::Gecko_EnsureStyleViewTimelineArrayLength; +use crate::gecko_bindings::structs::nsStyleAutoArray; +use crate::gecko_bindings::structs::{StyleAnimation, StyleTransition}; +use crate::gecko_bindings::structs::{StyleScrollTimeline, StyleViewTimeline}; +use std::iter::{once, Chain, IntoIterator, Once}; +use std::ops::{Index, IndexMut}; +use std::slice::{Iter, IterMut}; + +impl Index for nsStyleAutoArray { + type Output = T; + fn index(&self, index: usize) -> &T { + match index { + 0 => &self.mFirstElement, + _ => &self.mOtherElements[index - 1], + } + } +} + +impl IndexMut for nsStyleAutoArray { + fn index_mut(&mut self, index: usize) -> &mut T { + match index { + 0 => &mut self.mFirstElement, + _ => &mut self.mOtherElements[index - 1], + } + } +} + +impl nsStyleAutoArray { + /// Mutably iterate over the array elements. + pub fn iter_mut(&mut self) -> Chain, IterMut> { + once(&mut self.mFirstElement).chain(self.mOtherElements.iter_mut()) + } + + /// Iterate over the array elements. + pub fn iter(&self) -> Chain, Iter> { + once(&self.mFirstElement).chain(self.mOtherElements.iter()) + } + + /// Returns the length of the array. + /// + /// Note that often structs containing autoarrays will have additional + /// member fields that contain the length, which must be kept in sync. + pub fn len(&self) -> usize { + 1 + self.mOtherElements.len() + } +} + +impl nsStyleAutoArray { + /// Ensures that the array has length at least the given length. + pub fn ensure_len(&mut self, len: usize) { + unsafe { + Gecko_EnsureStyleAnimationArrayLength( + self as *mut nsStyleAutoArray as *mut _, + len, + ); + } + } +} + +impl nsStyleAutoArray { + /// Ensures that the array has length at least the given length. + pub fn ensure_len(&mut self, len: usize) { + unsafe { + Gecko_EnsureStyleTransitionArrayLength( + self as *mut nsStyleAutoArray as *mut _, + len, + ); + } + } +} + +impl nsStyleAutoArray { + /// Ensures that the array has length at least the given length. + pub fn ensure_len(&mut self, len: usize) { + unsafe { + Gecko_EnsureStyleViewTimelineArrayLength( + self as *mut nsStyleAutoArray as *mut _, + len, + ); + } + } +} + +impl nsStyleAutoArray { + /// Ensures that the array has length at least the given length. + pub fn ensure_len(&mut self, len: usize) { + unsafe { + Gecko_EnsureStyleScrollTimelineArrayLength( + self as *mut nsStyleAutoArray as *mut _, + len, + ); + } + } +} + +impl<'a, T> IntoIterator for &'a mut nsStyleAutoArray { + type Item = &'a mut T; + type IntoIter = Chain, IterMut<'a, T>>; + fn into_iter(self) -> Self::IntoIter { + self.iter_mut() + } +} diff --git a/servo/components/style/gecko_bindings/sugar/ns_t_array.rs b/servo/components/style/gecko_bindings/sugar/ns_t_array.rs new file mode 100644 index 0000000000..d10ed420dd --- /dev/null +++ b/servo/components/style/gecko_bindings/sugar/ns_t_array.rs @@ -0,0 +1,144 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Rust helpers for Gecko's nsTArray. + +use crate::gecko_bindings::bindings; +use crate::gecko_bindings::structs::{nsTArray, nsTArrayHeader, CopyableTArray}; +use std::mem; +use std::ops::{Deref, DerefMut}; +use std::slice; + +impl Deref for nsTArray { + type Target = [T]; + + #[inline] + fn deref<'a>(&'a self) -> &'a [T] { + unsafe { slice::from_raw_parts(self.slice_begin(), self.header().mLength as usize) } + } +} + +impl DerefMut for nsTArray { + fn deref_mut<'a>(&'a mut self) -> &'a mut [T] { + unsafe { slice::from_raw_parts_mut(self.slice_begin(), self.header().mLength as usize) } + } +} + +impl nsTArray { + #[inline] + fn header<'a>(&'a self) -> &'a nsTArrayHeader { + debug_assert!(!self.mBuffer.is_null()); + unsafe { mem::transmute(self.mBuffer) } + } + + // unsafe, since header may be in shared static or something + unsafe fn header_mut<'a>(&'a mut self) -> &'a mut nsTArrayHeader { + debug_assert!(!self.mBuffer.is_null()); + + mem::transmute(self.mBuffer) + } + + #[inline] + unsafe fn slice_begin(&self) -> *mut T { + debug_assert!(!self.mBuffer.is_null()); + (self.mBuffer as *const nsTArrayHeader).offset(1) as *mut _ + } + + /// Ensures the array has enough capacity at least to hold `cap` elements. + /// + /// NOTE: This doesn't call the constructor on the values! + pub fn ensure_capacity(&mut self, cap: usize) { + if cap >= self.len() { + unsafe { + bindings::Gecko_EnsureTArrayCapacity( + self as *mut nsTArray as *mut _, + cap, + mem::size_of::(), + ) + } + } + } + + /// Clears the array storage without calling the destructor on the values. + #[inline] + pub unsafe fn clear(&mut self) { + if self.len() != 0 { + bindings::Gecko_ClearPODTArray( + self as *mut nsTArray as *mut _, + mem::size_of::(), + mem::align_of::(), + ); + } + } + + /// Clears a POD array. This is safe since copy types are memcopyable. + #[inline] + pub fn clear_pod(&mut self) + where + T: Copy, + { + unsafe { self.clear() } + } + + /// Resize and set the length of the array to `len`. + /// + /// unsafe because this may leave the array with uninitialized elements. + /// + /// This will not call constructors. If you need that, either manually add + /// bindings or run the typed `EnsureCapacity` call on the gecko side. + pub unsafe fn set_len(&mut self, len: u32) { + // this can leak + debug_assert!(len >= self.len() as u32); + if self.len() == len as usize { + return; + } + self.ensure_capacity(len as usize); + self.header_mut().mLength = len; + } + + /// Resizes an array containing only POD elements + /// + /// unsafe because this may leave the array with uninitialized elements. + /// + /// This will not leak since it only works on POD types (and thus doesn't assert) + pub unsafe fn set_len_pod(&mut self, len: u32) + where + T: Copy, + { + if self.len() == len as usize { + return; + } + self.ensure_capacity(len as usize); + let header = self.header_mut(); + header.mLength = len; + } + + /// Collects the given iterator into this array. + /// + /// Not unsafe because we won't leave uninitialized elements in the array. + pub fn assign_from_iter_pod(&mut self, iter: I) + where + T: Copy, + I: ExactSizeIterator + Iterator, + { + debug_assert!(iter.len() <= 0xFFFFFFFF); + unsafe { + self.set_len_pod(iter.len() as u32); + } + self.iter_mut().zip(iter).for_each(|(r, v)| *r = v); + } +} + +impl Deref for CopyableTArray { + type Target = nsTArray; + fn deref(&self) -> &Self::Target { + &self._base + } +} + +impl DerefMut for CopyableTArray { + fn deref_mut(&mut self) -> &mut nsTArray { + &mut self._base + } +} diff --git a/servo/components/style/gecko_bindings/sugar/origin_flags.rs b/servo/components/style/gecko_bindings/sugar/origin_flags.rs new file mode 100644 index 0000000000..b27060405a --- /dev/null +++ b/servo/components/style/gecko_bindings/sugar/origin_flags.rs @@ -0,0 +1,31 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Helper to iterate over `OriginFlags` bits. + +use crate::gecko_bindings::structs::OriginFlags; +use crate::stylesheets::OriginSet; + +/// Checks that the values for OriginFlags are the ones we expect. +pub fn assert_flags_match() { + use crate::stylesheets::origin::*; + debug_assert_eq!( + OriginFlags::UserAgent.0, + OriginSet::ORIGIN_USER_AGENT.bits() + ); + debug_assert_eq!(OriginFlags::Author.0, OriginSet::ORIGIN_AUTHOR.bits()); + debug_assert_eq!(OriginFlags::User.0, OriginSet::ORIGIN_USER.bits()); +} + +impl From for OriginSet { + fn from(flags: OriginFlags) -> Self { + Self::from_bits_retain(flags.0) + } +} + +impl From for OriginFlags { + fn from(set: OriginSet) -> Self { + OriginFlags(set.bits()) + } +} diff --git a/servo/components/style/gecko_bindings/sugar/ownership.rs b/servo/components/style/gecko_bindings/sugar/ownership.rs new file mode 100644 index 0000000000..31b512cf1e --- /dev/null +++ b/servo/components/style/gecko_bindings/sugar/ownership.rs @@ -0,0 +1,61 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Helpers for different FFI pointer kinds that Gecko's FFI layer uses. + +use crate::gecko_bindings::structs::root::mozilla::detail::CopyablePtr; +use servo_arc::Arc; +use std::marker::PhantomData; +use std::ops::{Deref, DerefMut}; +use std::ptr; + +/// Gecko-FFI-safe Arc (T is an ArcInner). +/// +/// This can be null. +/// +/// Leaks on drop. Please don't drop this. +#[repr(C)] +pub struct Strong { + ptr: *const GeckoType, + _marker: PhantomData, +} + +impl From> for Strong { + fn from(arc: Arc) -> Self { + Self { + ptr: Arc::into_raw(arc), + _marker: PhantomData, + } + } +} + +impl Strong { + #[inline] + /// Returns whether this reference is null. + pub fn is_null(&self) -> bool { + self.ptr.is_null() + } + + #[inline] + /// Returns a null pointer + pub fn null() -> Self { + Self { + ptr: ptr::null(), + _marker: PhantomData, + } + } +} + +impl Deref for CopyablePtr { + type Target = T; + fn deref(&self) -> &Self::Target { + &self.mPtr + } +} + +impl DerefMut for CopyablePtr { + fn deref_mut<'a>(&'a mut self) -> &'a mut T { + &mut self.mPtr + } +} diff --git a/servo/components/style/gecko_bindings/sugar/refptr.rs b/servo/components/style/gecko_bindings/sugar/refptr.rs new file mode 100644 index 0000000000..c4a0479a07 --- /dev/null +++ b/servo/components/style/gecko_bindings/sugar/refptr.rs @@ -0,0 +1,289 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! A rust helper to ease the use of Gecko's refcounted types. + +use crate::gecko_bindings::{bindings, structs}; +use crate::Atom; +use servo_arc::Arc; +use std::fmt::Write; +use std::marker::PhantomData; +use std::ops::Deref; +use std::{fmt, mem, ptr}; + +/// Trait for all objects that have Addref() and Release +/// methods and can be placed inside RefPtr +pub unsafe trait RefCounted { + /// Bump the reference count. + fn addref(&self); + /// Decrease the reference count. + unsafe fn release(&self); +} + +/// Trait for types which can be shared across threads in RefPtr. +pub unsafe trait ThreadSafeRefCounted: RefCounted {} + +/// A custom RefPtr implementation to take into account Drop semantics and +/// a bit less-painful memory management. +pub struct RefPtr { + ptr: *mut T, + _marker: PhantomData, +} + +impl fmt::Debug for RefPtr { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("RefPtr { ")?; + self.ptr.fmt(f)?; + f.write_char('}') + } +} + +impl RefPtr { + /// Create a new RefPtr from an already addrefed pointer obtained from FFI. + /// + /// The pointer must be valid, non-null and have been addrefed. + pub unsafe fn from_addrefed(ptr: *mut T) -> Self { + debug_assert!(!ptr.is_null()); + RefPtr { + ptr, + _marker: PhantomData, + } + } + + /// Returns whether the current pointer is null. + pub fn is_null(&self) -> bool { + self.ptr.is_null() + } + + /// Returns a null pointer. + pub fn null() -> Self { + Self { + ptr: ptr::null_mut(), + _marker: PhantomData, + } + } + + /// Create a new RefPtr from a pointer obtained from FFI. + /// + /// This method calls addref() internally + pub unsafe fn new(ptr: *mut T) -> Self { + let ret = RefPtr { + ptr, + _marker: PhantomData, + }; + ret.addref(); + ret + } + + /// Produces an FFI-compatible RefPtr that can be stored in style structs. + /// + /// structs::RefPtr does not have a destructor, so this may leak + pub fn forget(self) -> structs::RefPtr { + let ret = structs::RefPtr { + mRawPtr: self.ptr, + _phantom_0: PhantomData, + }; + mem::forget(self); + ret + } + + /// Returns the raw inner pointer to be fed back into FFI. + pub fn get(&self) -> *mut T { + self.ptr + } + + /// Addref the inner data, obviously leaky on its own. + pub fn addref(&self) { + if !self.ptr.is_null() { + unsafe { + (*self.ptr).addref(); + } + } + } + + /// Release the inner data. + /// + /// Call only when the data actually needs releasing. + pub unsafe fn release(&self) { + if !self.ptr.is_null() { + (*self.ptr).release(); + } + } +} + +impl Deref for RefPtr { + type Target = T; + fn deref(&self) -> &T { + debug_assert!(!self.ptr.is_null()); + unsafe { &*self.ptr } + } +} + +impl structs::RefPtr { + /// Produces a Rust-side RefPtr from an FFI RefPtr, bumping the refcount. + /// + /// Must be called on a valid, non-null structs::RefPtr. + pub unsafe fn to_safe(&self) -> RefPtr { + let r = RefPtr { + ptr: self.mRawPtr, + _marker: PhantomData, + }; + r.addref(); + r + } + /// Produces a Rust-side RefPtr, consuming the existing one (and not bumping + /// the refcount). + pub unsafe fn into_safe(self) -> RefPtr { + debug_assert!(!self.mRawPtr.is_null()); + RefPtr { + ptr: self.mRawPtr, + _marker: PhantomData, + } + } + + /// Replace a structs::RefPtr with a different one, appropriately + /// addref/releasing. + /// + /// Both `self` and `other` must be valid, but can be null. + /// + /// Safe when called on an aliased pointer because the refcount in that case + /// needs to be at least two. + pub unsafe fn set(&mut self, other: &Self) { + self.clear(); + if !other.mRawPtr.is_null() { + *self = other.to_safe().forget(); + } + } + + /// Clear an instance of the structs::RefPtr, by releasing + /// it and setting its contents to null. + /// + /// `self` must be valid, but can be null. + pub unsafe fn clear(&mut self) { + if !self.mRawPtr.is_null() { + (*self.mRawPtr).release(); + self.mRawPtr = ptr::null_mut(); + } + } + + /// Replace a `structs::RefPtr` with a `RefPtr`, + /// consuming the `RefPtr`, and releasing the old + /// value in `self` if necessary. + /// + /// `self` must be valid, possibly null. + pub fn set_move(&mut self, other: RefPtr) { + if !self.mRawPtr.is_null() { + unsafe { + (*self.mRawPtr).release(); + } + } + *self = other.forget(); + } +} + +impl structs::RefPtr { + /// Returns a new, null refptr. + pub fn null() -> Self { + Self { + mRawPtr: ptr::null_mut(), + _phantom_0: PhantomData, + } + } + + /// Create a new RefPtr from an arc. + pub fn from_arc(s: Arc) -> Self { + Self { + mRawPtr: Arc::into_raw(s) as *mut _, + _phantom_0: PhantomData, + } + } + + /// Sets the contents to an Arc. + pub fn set_arc(&mut self, other: Arc) { + unsafe { + if !self.mRawPtr.is_null() { + let _ = Arc::from_raw(self.mRawPtr); + } + self.mRawPtr = Arc::into_raw(other) as *mut _; + } + } +} + +impl Drop for RefPtr { + fn drop(&mut self) { + unsafe { self.release() } + } +} + +impl Clone for RefPtr { + fn clone(&self) -> Self { + self.addref(); + RefPtr { + ptr: self.ptr, + _marker: PhantomData, + } + } +} + +impl PartialEq for RefPtr { + fn eq(&self, other: &Self) -> bool { + self.ptr == other.ptr + } +} + +unsafe impl Send for RefPtr {} +unsafe impl Sync for RefPtr {} + +macro_rules! impl_refcount { + ($t:ty, $addref:path, $release:path) => { + unsafe impl RefCounted for $t { + #[inline] + fn addref(&self) { + unsafe { $addref(self as *const _ as *mut _) } + } + + #[inline] + unsafe fn release(&self) { + $release(self as *const _ as *mut _) + } + } + }; +} + +// Companion of NS_DECL_THREADSAFE_FFI_REFCOUNTING. +// +// Gets you a free RefCounted impl implemented via FFI. +macro_rules! impl_threadsafe_refcount { + ($t:ty, $addref:path, $release:path) => { + impl_refcount!($t, $addref, $release); + unsafe impl ThreadSafeRefCounted for $t {} + }; +} + +impl_threadsafe_refcount!( + structs::mozilla::URLExtraData, + bindings::Gecko_AddRefURLExtraDataArbitraryThread, + bindings::Gecko_ReleaseURLExtraDataArbitraryThread +); +impl_threadsafe_refcount!( + structs::nsIURI, + bindings::Gecko_AddRefnsIURIArbitraryThread, + bindings::Gecko_ReleasensIURIArbitraryThread +); +impl_threadsafe_refcount!( + structs::SheetLoadDataHolder, + bindings::Gecko_AddRefSheetLoadDataHolderArbitraryThread, + bindings::Gecko_ReleaseSheetLoadDataHolderArbitraryThread +); + +#[inline] +unsafe fn addref_atom(atom: *mut structs::nsAtom) { + mem::forget(Atom::from_raw(atom)); +} + +#[inline] +unsafe fn release_atom(atom: *mut structs::nsAtom) { + let _ = Atom::from_addrefed(atom); +} +impl_threadsafe_refcount!(structs::nsAtom, addref_atom, release_atom); diff --git a/servo/components/style/gecko_string_cache/mod.rs b/servo/components/style/gecko_string_cache/mod.rs new file mode 100644 index 0000000000..79a5d46525 --- /dev/null +++ b/servo/components/style/gecko_string_cache/mod.rs @@ -0,0 +1,497 @@ +/* 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::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. +/// +/// * An index 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 `(index << 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; + +impl Deref for Atom { + type Target = WeakAtom; + + #[inline] + fn deref(&self) -> &WeakAtom { + unsafe { + let addr = if self.is_static() { + // This is really hot. + &gGkAtoms.mAtoms.get_unchecked(self.0.get() >> 1)._base as *const nsAtom + } else { + self.0.get() as *const nsAtom + }; + WeakAtom::new(addr as *const nsAtom) + } + } +} + +impl PrecomputedHash for Atom { + #[inline] + fn precomputed_hash(&self) -> u32 { + self.get_hash() + } +} + +impl Borrow for Atom { + #[inline] + fn borrow(&self) -> &WeakAtom { + self + } +} + +impl ToShmem for Atom { + fn to_shmem(&self, _builder: &mut SharedMemoryBuilder) -> to_shmem::Result { + if !self.is_static() { + return Err(format!( + "ToShmem failed for Atom: must be a static atom: {}", + self + )); + } + + Ok(ManuallyDrop::new(Atom(self.0))) + } +} + +impl Eq for WeakAtom {} +impl PartialEq for WeakAtom { + #[inline] + fn eq(&self, other: &Self) -> bool { + let weak: *const WeakAtom = self; + let other: *const WeakAtom = other; + weak == other + } +} + +impl PartialEq for WeakAtom { + #[inline] + fn eq(&self, other: &Atom) -> bool { + self == &**other + } +} + +unsafe impl Send for Atom {} +unsafe impl Sync for Atom {} +unsafe impl Sync for WeakAtom {} + +impl WeakAtom { + /// Construct a `WeakAtom` from a raw `nsAtom`. + #[inline] + pub unsafe fn new<'a>(atom: *const nsAtom) -> &'a mut Self { + &mut *(atom as *mut WeakAtom) + } + + /// Clone this atom, bumping the refcount if the atom is not static. + #[inline] + pub fn clone(&self) -> Atom { + unsafe { Atom::from_raw(self.as_ptr()) } + } + + /// Get the atom hash. + #[inline] + pub fn get_hash(&self) -> u32 { + self.0.mHash + } + + /// Get the atom as a slice of utf-16 chars. + #[inline] + pub fn as_slice(&self) -> &[u16] { + let string = if self.is_static() { + let atom_ptr = self.as_ptr() as *const nsStaticAtom; + let string_offset = unsafe { (*atom_ptr).mStringOffset }; + let string_offset = -(string_offset as isize); + let u8_ptr = atom_ptr as *const u8; + // It is safe to use offset() here because both addresses are within + // the same struct, e.g. mozilla::detail::gGkAtoms. + unsafe { u8_ptr.offset(string_offset) as *const u16 } + } else { + let atom_ptr = self.as_ptr() as *const nsDynamicAtom; + let buffer_ptr = unsafe { (*atom_ptr).mStringBuffer.mRawPtr }; + // Dynamic atom chars are stored at the end of the string buffer. + unsafe { buffer_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 { + let index = ptr.offset_from(&gGkAtoms.mAtoms[0] as *const _); + debug_assert!(index >= 0, "Should be a non-negative index"); + debug_assert!( + (index as usize) < STATIC_ATOM_COUNT, + "Should be a valid static atom index" + ); + NonZeroUsize::new_unchecked(((index as usize) << 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 { + debug_assert!((index as usize) < STATIC_ATOM_COUNT); + Atom(NonZeroUsize::new_unchecked(((index as usize) << 1) | 1)) + } + + /// Creates an atom from an atom pointer. + #[inline(always)] + pub unsafe fn from_raw(ptr: *mut nsAtom) -> Self { + let atom = Atom(make_handle(ptr)); + if !atom.is_static() { + Gecko_AddRefAtom(ptr); + } + atom + } + + /// Creates an atom from an atom pointer that has already had AddRef + /// called on it. This may be a static or dynamic atom. + #[inline] + pub unsafe fn from_addrefed(ptr: *mut nsAtom) -> Self { + assert!(!ptr.is_null()); + Atom(make_handle(ptr)) + } + + /// Convert this atom into an addrefed nsAtom pointer. + #[inline] + pub fn into_addrefed(self) -> *mut nsAtom { + let ptr = self.as_ptr(); + mem::forget(self); + ptr + } +} + +impl Hash for Atom { + fn hash(&self, state: &mut H) + where + H: Hasher, + { + state.write_u32(self.get_hash()); + } +} + +impl Hash for WeakAtom { + fn hash(&self, state: &mut H) + where + H: Hasher, + { + state.write_u32(self.get_hash()); + } +} + +impl Clone for Atom { + #[inline(always)] + fn clone(&self) -> Atom { + unsafe { + let atom = Atom(self.0); + if !atom.is_static() { + Gecko_AddRefAtom(atom.as_ptr()); + } + atom + } + } +} + +impl Drop for Atom { + #[inline] + fn drop(&mut self) { + if !self.is_static() { + unsafe { + Gecko_ReleaseAtom(self.as_ptr()); + } + } + } +} + +impl Default for Atom { + #[inline] + fn default() -> Self { + atom!("") + } +} + +impl fmt::Debug for Atom { + fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result { + write!(w, "Atom(0x{:08x}, {})", self.0, self) + } +} + +impl fmt::Display for Atom { + fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result { + self.deref().fmt(w) + } +} + +impl<'a> From<&'a str> for Atom { + #[inline] + fn from(string: &str) -> Atom { + debug_assert!(string.len() <= u32::max_value() as usize); + unsafe { + Atom::from_addrefed(Gecko_Atomize( + string.as_ptr() as *const _, + string.len() as u32, + )) + } + } +} + +impl<'a> From<&'a [u16]> for Atom { + #[inline] + fn from(slice: &[u16]) -> Atom { + Atom::from(&*nsStr::from(slice)) + } +} + +impl<'a> From<&'a nsAString> for Atom { + #[inline] + fn from(string: &nsAString) -> Atom { + unsafe { Atom::from_addrefed(Gecko_Atomize16(string)) } + } +} + +impl<'a> From> for Atom { + #[inline] + fn from(string: Cow<'a, str>) -> Atom { + Atom::from(&*string) + } +} + +impl From for Atom { + #[inline] + fn from(string: String) -> Atom { + Atom::from(&*string) + } +} + +malloc_size_of_is_0!(Atom); + +impl SpecifiedValueInfo for Atom {} diff --git a/servo/components/style/gecko_string_cache/namespace.rs b/servo/components/style/gecko_string_cache/namespace.rs new file mode 100644 index 0000000000..d9745b9e21 --- /dev/null +++ b/servo/components/style/gecko_string_cache/namespace.rs @@ -0,0 +1,105 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! A type to represent a namespace. + +use crate::gecko_bindings::structs::nsAtom; +use crate::string_cache::{Atom, WeakAtom}; +use precomputed_hash::PrecomputedHash; +use std::borrow::Borrow; +use std::fmt; +use std::ops::Deref; + +/// In Gecko namespaces are just regular atoms, so this is a simple macro to +/// forward one macro to the other. +#[macro_export] +macro_rules! ns { + () => { + $crate::string_cache::Namespace(atom!("")) + }; + ($s:tt) => { + $crate::string_cache::Namespace(atom!($s)) + }; +} + +/// A Gecko namespace is just a wrapped atom. +#[derive( + Clone, + Debug, + Default, + Eq, + Hash, + MallocSizeOf, + PartialEq, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(transparent)] +pub struct Namespace(pub Atom); + +impl PrecomputedHash for Namespace { + #[inline] + fn precomputed_hash(&self) -> u32 { + self.0.precomputed_hash() + } +} + +/// A Gecko WeakNamespace is a wrapped WeakAtom. +#[derive(Deref, Hash)] +pub struct WeakNamespace(WeakAtom); + +impl Deref for Namespace { + type Target = WeakNamespace; + + #[inline] + fn deref(&self) -> &WeakNamespace { + let weak: *const WeakAtom = &*self.0; + unsafe { &*(weak as *const WeakNamespace) } + } +} + +impl<'a> From<&'a str> for Namespace { + fn from(s: &'a str) -> Self { + Namespace(Atom::from(s)) + } +} + +impl fmt::Display for Namespace { + fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result { + self.0.fmt(w) + } +} + +impl Borrow for Namespace { + #[inline] + fn borrow(&self) -> &WeakNamespace { + self + } +} + +impl WeakNamespace { + /// Trivially construct a WeakNamespace. + #[inline] + pub unsafe fn new<'a>(atom: *mut nsAtom) -> &'a Self { + &*(atom as *const WeakNamespace) + } + + /// Clone this WeakNamespace to obtain a strong reference to the same + /// underlying namespace. + #[inline] + pub fn clone(&self) -> Namespace { + Namespace(self.0.clone()) + } +} + +impl Eq for WeakNamespace {} +impl PartialEq for WeakNamespace { + #[inline] + fn eq(&self, other: &Self) -> bool { + let weak: *const WeakNamespace = self; + let other: *const WeakNamespace = other; + weak == other + } +} diff --git a/servo/components/style/global_style_data.rs b/servo/components/style/global_style_data.rs new file mode 100644 index 0000000000..38d72b2c74 --- /dev/null +++ b/servo/components/style/global_style_data.rs @@ -0,0 +1,212 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Global style data + +use crate::context::StyleSystemOptions; +#[cfg(feature = "gecko")] +use crate::gecko_bindings::bindings; +use crate::parallel::STYLE_THREAD_STACK_SIZE_KB; +use crate::shared_lock::SharedRwLock; +use crate::thread_state; +use gecko_profiler; +use parking_lot::{Mutex, RwLock, RwLockReadGuard}; +use rayon; +#[cfg(unix)] +use std::os::unix::thread::{JoinHandleExt, RawPthread}; +#[cfg(windows)] +use std::os::windows::{io::AsRawHandle, prelude::RawHandle}; +use std::{io, thread}; +use thin_vec::ThinVec; + +/// Platform-specific handle to a thread. +#[cfg(unix)] +pub type PlatformThreadHandle = RawPthread; +/// Platform-specific handle to a thread. +#[cfg(windows)] +pub type PlatformThreadHandle = RawHandle; + +/// Global style data +pub struct GlobalStyleData { + /// Shared RWLock for CSSOM objects + pub shared_lock: SharedRwLock, + + /// Global style system options determined by env vars. + pub options: StyleSystemOptions, +} + +/// Global thread pool. +pub struct StyleThreadPool { + /// How many threads parallel styling can use. If not using a thread pool, this is set to `None`. + pub num_threads: Option, + + /// The parallel styling thread pool. + /// + /// For leak-checking purposes, we want to terminate the thread-pool, which + /// waits for all the async jobs to complete. Thus the RwLock. + style_thread_pool: RwLock>, +} + +fn thread_name(index: usize) -> String { + format!("StyleThread#{}", index) +} + +lazy_static! { + /// JoinHandles for spawned style threads. These will be joined during + /// StyleThreadPool::shutdown() after exiting the thread pool. + /// + /// This would be quite inefficient if rayon destroyed and re-created + /// threads regularly during threadpool operation in response to demand, + /// however rayon actually never destroys its threads until the entire + /// thread pool is shut-down, so the size of this list is bounded. + static ref STYLE_THREAD_JOIN_HANDLES: Mutex>> = + Mutex::new(Vec::new()); +} + +fn thread_spawn(options: rayon::ThreadBuilder) -> io::Result<()> { + let mut b = thread::Builder::new(); + if let Some(name) = options.name() { + b = b.name(name.to_owned()); + } + if let Some(stack_size) = options.stack_size() { + b = b.stack_size(stack_size); + } + let join_handle = b.spawn(|| options.run())?; + STYLE_THREAD_JOIN_HANDLES.lock().push(join_handle); + Ok(()) +} + +fn thread_startup(_index: usize) { + thread_state::initialize_layout_worker_thread(); + #[cfg(feature = "gecko")] + unsafe { + bindings::Gecko_SetJemallocThreadLocalArena(true); + let name = thread_name(_index); + gecko_profiler::register_thread(&name); + } +} + +fn thread_shutdown(_: usize) { + #[cfg(feature = "gecko")] + unsafe { + gecko_profiler::unregister_thread(); + bindings::Gecko_SetJemallocThreadLocalArena(false); + } +} + +impl StyleThreadPool { + /// Shuts down the thread pool, waiting for all work to complete. + pub fn shutdown() { + if STYLE_THREAD_JOIN_HANDLES.lock().is_empty() { + return; + } + { + // Drop the pool. + let _ = STYLE_THREAD_POOL.style_thread_pool.write().take(); + } + + // Join spawned threads until all of the threads have been joined. This + // will usually be pretty fast, as on shutdown there should be basically + // no threads left running. + while let Some(join_handle) = STYLE_THREAD_JOIN_HANDLES.lock().pop() { + let _ = join_handle.join(); + } + } + + /// Returns a reference to the thread pool. + /// + /// We only really want to give read-only access to the pool, except + /// for shutdown(). + pub fn pool(&self) -> RwLockReadGuard> { + self.style_thread_pool.read() + } + + /// Returns a list of the pool's platform-specific thread handles. + pub fn get_thread_handles(handles: &mut ThinVec) { + // Force the lazy initialization of STYLE_THREAD_POOL so that the threads get spawned and + // their join handles are added to STYLE_THREAD_JOIN_HANDLES. + lazy_static::initialize(&STYLE_THREAD_POOL); + + for join_handle in STYLE_THREAD_JOIN_HANDLES.lock().iter() { + #[cfg(unix)] + let handle = join_handle.as_pthread_t(); + #[cfg(windows)] + let handle = join_handle.as_raw_handle(); + + handles.push(handle); + } + } +} + +#[cfg(feature = "servo")] +fn stylo_threads_pref() -> i32 { + pref!(layout.threads) +} + +#[cfg(feature = "gecko")] +fn stylo_threads_pref() -> i32 { + static_prefs::pref!("layout.css.stylo-threads") +} + +/// 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). +pub(crate) const STYLO_MAX_THREADS: usize = 6; + +lazy_static! { + /// Global thread pool + pub static ref STYLE_THREAD_POOL: StyleThreadPool = { + use std::cmp; + // We always set this pref on startup, before layout or script have had a chance of + // accessing (and thus creating) the thread-pool. + let threads_pref: i32 = stylo_threads_pref(); + let num_threads = if threads_pref >= 0 { + threads_pref as usize + } else { + // Gecko may wish to override the default number of threads, for example on + // systems with heterogeneous CPUs. + #[cfg(feature = "gecko")] + let num_threads = unsafe { bindings::Gecko_GetNumStyleThreads() }; + #[cfg(not(feature = "gecko"))] + let num_threads = -1; + + if num_threads >= 0 { + num_threads as usize + } else { + use num_cpus; + // 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. + cmp::max(num_cpus::get() * 3 / 4, 1) + } + }; + + let num_threads = cmp::min(num_threads, STYLO_MAX_THREADS); + // Since the main-thread is also part of the pool, having one thread or less doesn't make + // sense. + let (pool, num_threads) = if num_threads <= 1 { + (None, None) + } else { + let workers = rayon::ThreadPoolBuilder::new() + .spawn_handler(thread_spawn) + .use_current_thread() + .num_threads(num_threads) + .thread_name(thread_name) + .start_handler(thread_startup) + .exit_handler(thread_shutdown) + .stack_size(STYLE_THREAD_STACK_SIZE_KB * 1024) + .build(); + (workers.ok(), Some(num_threads)) + }; + + StyleThreadPool { + num_threads, + style_thread_pool: RwLock::new(pool), + } + }; + + /// Global style data + pub static ref GLOBAL_STYLE_DATA: GlobalStyleData = GlobalStyleData { + shared_lock: SharedRwLock::new_leaked(), + options: StyleSystemOptions::default(), + }; +} diff --git a/servo/components/style/invalidation/element/document_state.rs b/servo/components/style/invalidation/element/document_state.rs new file mode 100644 index 0000000000..0b846510d8 --- /dev/null +++ b/servo/components/style/invalidation/element/document_state.rs @@ -0,0 +1,154 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! An invalidation processor for style changes due to document state changes. + +use crate::dom::TElement; +use crate::invalidation::element::invalidation_map::Dependency; +use crate::invalidation::element::invalidator::{ + DescendantInvalidationLists, InvalidationVector, SiblingTraversalMap, +}; +use crate::invalidation::element::invalidator::{Invalidation, InvalidationProcessor}; +use crate::invalidation::element::state_and_attributes; +use crate::stylist::CascadeData; +use dom::DocumentState; +use selectors::matching::{ + MatchingContext, MatchingForInvalidation, MatchingMode, NeedsSelectorFlags, QuirksMode, + SelectorCaches, VisitedHandlingMode, +}; + +/// A struct holding the members necessary to invalidate document state +/// selectors. +#[derive(Debug)] +pub struct InvalidationMatchingData { + /// The document state that has changed, which makes it always match. + pub document_state: DocumentState, +} + +impl Default for InvalidationMatchingData { + #[inline(always)] + fn default() -> Self { + Self { + document_state: DocumentState::empty(), + } + } +} + +/// An invalidation processor for style changes due to state and attribute +/// changes. +pub struct DocumentStateInvalidationProcessor<'a, 'b, E: TElement, I> { + rules: I, + matching_context: MatchingContext<'a, E::Impl>, + traversal_map: SiblingTraversalMap, + document_states_changed: DocumentState, + _marker: std::marker::PhantomData<&'b ()>, +} + +impl<'a, 'b, E: TElement, I> DocumentStateInvalidationProcessor<'a, 'b, E, I> { + /// Creates a new DocumentStateInvalidationProcessor. + #[inline] + pub fn new( + rules: I, + document_states_changed: DocumentState, + selector_caches: &'a mut SelectorCaches, + quirks_mode: QuirksMode, + ) -> Self { + let mut matching_context = MatchingContext::<'a, E::Impl>::new_for_visited( + MatchingMode::Normal, + None, + selector_caches, + VisitedHandlingMode::AllLinksVisitedAndUnvisited, + quirks_mode, + NeedsSelectorFlags::No, + MatchingForInvalidation::No, + ); + + matching_context.extra_data.invalidation_data.document_state = document_states_changed; + + Self { + rules, + document_states_changed, + matching_context, + traversal_map: SiblingTraversalMap::default(), + _marker: std::marker::PhantomData, + } + } +} + +impl<'a, 'b, E, I> InvalidationProcessor<'b, 'a, E> + for DocumentStateInvalidationProcessor<'a, 'b, 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<'b>, + _descendant_invalidations: &mut DescendantInvalidationLists<'b>, + _sibling_invalidations: &mut InvalidationVector<'b>, + ) -> 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 sibling_traversal_map(&self) -> &SiblingTraversalMap { + &self.traversal_map + } + + fn recursion_limit_exceeded(&mut self, _: E) { + unreachable!("We don't run document state invalidation with stack limits") + } + + fn should_process_descendants(&mut self, element: E) -> bool { + match element.borrow_data() { + Some(d) => state_and_attributes::should_process_descendants(&d), + None => false, + } + } + + fn invalidated_descendants(&mut self, element: E, child: E) { + state_and_attributes::invalidated_descendants(element, child) + } + + fn invalidated_self(&mut self, element: E) { + state_and_attributes::invalidated_self(element); + } + + fn invalidated_sibling(&mut self, sibling: E, of: E) { + state_and_attributes::invalidated_sibling(sibling, of); + } +} diff --git a/servo/components/style/invalidation/element/element_wrapper.rs b/servo/components/style/invalidation/element/element_wrapper.rs new file mode 100644 index 0000000000..e17afd7774 --- /dev/null +++ b/servo/components/style/invalidation/element/element_wrapper.rs @@ -0,0 +1,388 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! A wrapper over an element and a snapshot, that allows us to selector-match +//! against a past state of the element. + +use crate::dom::TElement; +use crate::selector_parser::{AttrValue, NonTSPseudoClass, PseudoElement, SelectorImpl}; +use crate::selector_parser::{Snapshot, SnapshotMap}; +use crate::values::AtomIdent; +use crate::{CaseSensitivityExt, LocalName, Namespace, WeakAtom}; +use dom::ElementState; +use selectors::attr::{AttrSelectorOperation, CaseSensitivity, NamespaceConstraint}; +use selectors::bloom::BloomFilter; +use selectors::matching::{ElementSelectorFlags, MatchingContext}; +use selectors::{Element, OpaqueElement}; +use std::cell::Cell; +use std::fmt; + +/// In order to compute restyle hints, we perform a selector match against a +/// list of partial selectors whose rightmost simple selector may be sensitive +/// to the thing being changed. We do this matching twice, once for the element +/// as it exists now and once for the element as it existed at the time of the +/// last restyle. If the results of the selector match differ, that means that +/// the given partial selector is sensitive to the change, and we compute a +/// restyle hint based on its combinator. +/// +/// In order to run selector matching against the old element state, we generate +/// a wrapper for the element which claims to have the old state. This is the +/// ElementWrapper logic below. +/// +/// Gecko does this differently for element states, and passes a mask called +/// mStateMask, which indicates the states that need to be ignored during +/// selector matching. This saves an ElementWrapper allocation and an additional +/// selector match call at the expense of additional complexity inside the +/// selector matching logic. This only works for boolean states though, so we +/// still need to take the ElementWrapper approach for attribute-dependent +/// style. So we do it the same both ways for now to reduce complexity, but it's +/// worth measuring the performance impact (if any) of the mStateMask approach. +pub trait ElementSnapshot: Sized { + /// The state of the snapshot, if any. + fn state(&self) -> Option; + + /// If this snapshot contains attribute information. + fn has_attrs(&self) -> bool; + + /// Gets the attribute information of the snapshot as a string. + /// + /// Only for debugging purposes. + fn debug_list_attributes(&self) -> String { + String::new() + } + + /// The ID attribute per this snapshot. Should only be called if + /// `has_attrs()` returns true. + fn id_attr(&self) -> Option<&WeakAtom>; + + /// Whether this snapshot contains the class `name`. Should only be called + /// if `has_attrs()` returns true. + fn has_class(&self, name: &AtomIdent, case_sensitivity: CaseSensitivity) -> bool; + + /// Whether this snapshot represents the part named `name`. Should only be + /// called if `has_attrs()` returns true. + fn is_part(&self, name: &AtomIdent) -> bool; + + /// See Element::imported_part. + fn imported_part(&self, name: &AtomIdent) -> Option; + + /// A callback that should be called for each class of the snapshot. Should + /// only be called if `has_attrs()` returns true. + fn each_class(&self, _: F) + where + F: FnMut(&AtomIdent); + + /// The `xml:lang=""` or `lang=""` attribute value per this snapshot. + fn lang_attr(&self) -> Option; +} + +/// A simple wrapper over an element and a snapshot, that allows us to +/// selector-match against a past state of the element. +#[derive(Clone)] +pub struct ElementWrapper<'a, E> +where + E: TElement, +{ + element: E, + cached_snapshot: Cell>, + snapshot_map: &'a SnapshotMap, +} + +impl<'a, E> ElementWrapper<'a, E> +where + E: TElement, +{ + /// Trivially constructs an `ElementWrapper`. + pub fn new(el: E, snapshot_map: &'a SnapshotMap) -> Self { + ElementWrapper { + element: el, + cached_snapshot: Cell::new(None), + snapshot_map: snapshot_map, + } + } + + /// Gets the snapshot associated with this element, if any. + pub fn snapshot(&self) -> Option<&'a Snapshot> { + if !self.element.has_snapshot() { + return None; + } + + if let Some(s) = self.cached_snapshot.get() { + return Some(s); + } + + let snapshot = self.snapshot_map.get(&self.element); + debug_assert!(snapshot.is_some(), "has_snapshot lied!"); + + self.cached_snapshot.set(snapshot); + + snapshot + } + + /// Returns the states that have changed since the element was snapshotted. + pub fn state_changes(&self) -> ElementState { + let snapshot = match self.snapshot() { + Some(s) => s, + None => return ElementState::empty(), + }; + + match snapshot.state() { + Some(state) => state ^ self.element.state(), + None => ElementState::empty(), + } + } + + /// Returns the value of the `xml:lang=""` (or, if appropriate, `lang=""`) + /// attribute from this element's snapshot or the closest ancestor + /// element snapshot with the attribute specified. + fn get_lang(&self) -> Option { + let mut current = self.clone(); + loop { + let lang = match self.snapshot() { + Some(snapshot) if snapshot.has_attrs() => snapshot.lang_attr(), + _ => current.element.lang_attr(), + }; + if lang.is_some() { + return lang; + } + current = current.parent_element()?; + } + } +} + +impl<'a, E> fmt::Debug for ElementWrapper<'a, E> +where + E: TElement, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // Ignore other fields for now, can change later if needed. + self.element.fmt(f) + } +} + +impl<'a, E> Element for ElementWrapper<'a, E> +where + E: TElement, +{ + type Impl = SelectorImpl; + + fn match_non_ts_pseudo_class( + &self, + pseudo_class: &NonTSPseudoClass, + context: &mut MatchingContext, + ) -> bool { + // Some pseudo-classes need special handling to evaluate them against + // the snapshot. + match *pseudo_class { + // For :link and :visited, we don't actually want to test the + // element state directly. + // + // Instead, we use the `visited_handling` to determine if they + // match. + NonTSPseudoClass::Link => { + return self.is_link() && context.visited_handling().matches_unvisited(); + }, + NonTSPseudoClass::Visited => { + return self.is_link() && context.visited_handling().matches_visited(); + }, + + #[cfg(feature = "gecko")] + NonTSPseudoClass::MozTableBorderNonzero => { + if let Some(snapshot) = self.snapshot() { + if snapshot.has_other_pseudo_class_state() { + return snapshot.mIsTableBorderNonzero(); + } + } + }, + + #[cfg(feature = "gecko")] + NonTSPseudoClass::MozSelectListBox => { + if let Some(snapshot) = self.snapshot() { + if snapshot.has_other_pseudo_class_state() { + return snapshot.mIsSelectListBox(); + } + } + }, + + // :lang() needs to match using the closest ancestor xml:lang="" or + // lang="" attribtue from snapshots. + NonTSPseudoClass::Lang(ref lang_arg) => { + return self + .element + .match_element_lang(Some(self.get_lang()), lang_arg); + }, + + _ => {}, + } + + let flag = pseudo_class.state_flag(); + if flag.is_empty() { + return self + .element + .match_non_ts_pseudo_class(pseudo_class, context); + } + match self.snapshot().and_then(|s| s.state()) { + Some(snapshot_state) => snapshot_state.intersects(flag), + None => self + .element + .match_non_ts_pseudo_class(pseudo_class, context), + } + } + + fn apply_selector_flags(&self, _flags: ElementSelectorFlags) { + debug_assert!(false, "Shouldn't need selector flags for invalidation"); + } + + fn match_pseudo_element( + &self, + pseudo_element: &PseudoElement, + context: &mut MatchingContext, + ) -> bool { + self.element.match_pseudo_element(pseudo_element, context) + } + + fn is_link(&self) -> bool { + match self.snapshot().and_then(|s| s.state()) { + Some(state) => state.intersects(ElementState::VISITED_OR_UNVISITED), + None => self.element.is_link(), + } + } + + fn opaque(&self) -> OpaqueElement { + self.element.opaque() + } + + fn parent_element(&self) -> Option { + let parent = self.element.parent_element()?; + Some(Self::new(parent, self.snapshot_map)) + } + + fn parent_node_is_shadow_root(&self) -> bool { + self.element.parent_node_is_shadow_root() + } + + fn containing_shadow_host(&self) -> Option { + let host = self.element.containing_shadow_host()?; + Some(Self::new(host, self.snapshot_map)) + } + + fn prev_sibling_element(&self) -> Option { + let sibling = self.element.prev_sibling_element()?; + Some(Self::new(sibling, self.snapshot_map)) + } + + fn next_sibling_element(&self) -> Option { + let sibling = self.element.next_sibling_element()?; + Some(Self::new(sibling, self.snapshot_map)) + } + + fn first_element_child(&self) -> Option { + let child = self.element.first_element_child()?; + Some(Self::new(child, self.snapshot_map)) + } + + #[inline] + fn is_html_element_in_html_document(&self) -> bool { + self.element.is_html_element_in_html_document() + } + + #[inline] + fn is_html_slot_element(&self) -> bool { + self.element.is_html_slot_element() + } + + #[inline] + fn has_local_name( + &self, + local_name: &::BorrowedLocalName, + ) -> bool { + self.element.has_local_name(local_name) + } + + #[inline] + fn has_namespace( + &self, + ns: &::BorrowedNamespaceUrl, + ) -> bool { + self.element.has_namespace(ns) + } + + #[inline] + fn is_same_type(&self, other: &Self) -> bool { + self.element.is_same_type(&other.element) + } + + fn attr_matches( + &self, + ns: &NamespaceConstraint<&Namespace>, + local_name: &LocalName, + operation: &AttrSelectorOperation<&AttrValue>, + ) -> bool { + match self.snapshot() { + Some(snapshot) if snapshot.has_attrs() => { + snapshot.attr_matches(ns, local_name, operation) + }, + _ => self.element.attr_matches(ns, local_name, operation), + } + } + + fn has_id(&self, id: &AtomIdent, case_sensitivity: CaseSensitivity) -> bool { + match self.snapshot() { + Some(snapshot) if snapshot.has_attrs() => snapshot + .id_attr() + .map_or(false, |atom| case_sensitivity.eq_atom(&atom, id)), + _ => self.element.has_id(id, case_sensitivity), + } + } + + fn is_part(&self, name: &AtomIdent) -> bool { + match self.snapshot() { + Some(snapshot) if snapshot.has_attrs() => snapshot.is_part(name), + _ => self.element.is_part(name), + } + } + + fn imported_part(&self, name: &AtomIdent) -> Option { + match self.snapshot() { + Some(snapshot) if snapshot.has_attrs() => snapshot.imported_part(name), + _ => self.element.imported_part(name), + } + } + + fn has_class(&self, name: &AtomIdent, case_sensitivity: CaseSensitivity) -> bool { + match self.snapshot() { + Some(snapshot) if snapshot.has_attrs() => snapshot.has_class(name, case_sensitivity), + _ => self.element.has_class(name, case_sensitivity), + } + } + + fn is_empty(&self) -> bool { + self.element.is_empty() + } + + fn is_root(&self) -> bool { + self.element.is_root() + } + + fn is_pseudo_element(&self) -> bool { + self.element.is_pseudo_element() + } + + fn pseudo_element_originating_element(&self) -> Option { + self.element + .pseudo_element_originating_element() + .map(|e| ElementWrapper::new(e, self.snapshot_map)) + } + + fn assigned_slot(&self) -> Option { + self.element + .assigned_slot() + .map(|e| ElementWrapper::new(e, self.snapshot_map)) + } + + fn add_element_unique_hashes(&self, _filter: &mut BloomFilter) -> bool { + // Should not be relevant in the context of checking past elements in invalidation. + false + } +} 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..cb03862740 --- /dev/null +++ b/servo/components/style/invalidation/element/invalidation_map.rs @@ -0,0 +1,1425 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Code for invalidations due to state or attribute changes. + +use crate::context::QuirksMode; +use crate::selector_map::{ + MaybeCaseInsensitiveHashMap, PrecomputedHashMap, SelectorMap, SelectorMapEntry, +}; +use crate::selector_parser::{NonTSPseudoClass, SelectorImpl}; +use crate::AllocErr; +use crate::{Atom, LocalName, Namespace, ShrinkIfNeeded}; +use dom::{DocumentState, ElementState}; +use selectors::attr::NamespaceConstraint; +use selectors::parser::{ + Combinator, Component, RelativeSelector, RelativeSelectorCombinatorCount, + RelativeSelectorMatchHint, +}; +use selectors::parser::{Selector, SelectorIter}; +use selectors::visitor::{SelectorListKind, SelectorVisitor}; +use servo_arc::Arc; +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. + #[ignore_malloc_size_of = "CssRules have primary refs, we measure there"] + 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) + /// + #[ignore_malloc_size_of = "Arc"] + pub parent: Option>, + + /// What kind of relative selector invalidation this generates. + /// None if this dependency is not within a relative selector. + relative_kind: Option, +} + +impl SelectorMapEntry for Dependency { + fn selector(&self) -> SelectorIter { + self.selector.iter_from(self.selector_offset) + } +} + +/// The kind of elements down the tree this dependency may affect. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, MallocSizeOf)] +pub enum NormalDependencyInvalidationKind { + /// 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, +} + +/// The kind of elements up the tree this relative selector dependency may +/// affect. Because this travels upwards, it's not viable for parallel subtree +/// traversal, and is handled separately. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, MallocSizeOf)] +pub enum RelativeDependencyInvalidationKind { + /// This dependency may affect relative selector anchors for ancestors. + Ancestors, + /// This dependency may affect a relative selector anchor for the parent. + Parent, + /// This dependency may affect a relative selector anchor for the previous sibling. + PrevSibling, + /// This dependency may affect relative selector anchors for ancestors' previous siblings. + AncestorPrevSibling, + /// This dependency may affect relative selector anchors for earlier siblings. + EarlierSibling, + /// This dependency may affect relative selector anchors for ancestors' earlier siblings. + AncestorEarlierSibling, +} + +/// Invalidation kind merging normal and relative dependencies. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, MallocSizeOf)] +pub enum DependencyInvalidationKind { + /// This dependency is a normal dependency. + Normal(NormalDependencyInvalidationKind), + /// This dependency is a relative dependency. + Relative(RelativeDependencyInvalidationKind), +} + +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, + relative_kind: None, + } + } + + /// Returns the combinator to the right of the partial selector this + /// dependency represents. + /// + /// TODO(emilio): Consider storing inline if it helps cache locality? + 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 normal invalidation that this would generate. The dependency + /// in question must be a normal dependency. + pub fn normal_invalidation_kind(&self) -> NormalDependencyInvalidationKind { + debug_assert!( + self.relative_kind.is_none(), + "Querying normal invalidation kind on relative dependency." + ); + match self.combinator() { + None => NormalDependencyInvalidationKind::Element, + Some(Combinator::Child) | Some(Combinator::Descendant) => { + NormalDependencyInvalidationKind::Descendants + }, + Some(Combinator::LaterSibling) | Some(Combinator::NextSibling) => { + NormalDependencyInvalidationKind::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) => { + NormalDependencyInvalidationKind::ElementAndDescendants + }, + Some(Combinator::SlotAssignment) => NormalDependencyInvalidationKind::SlottedElements, + Some(Combinator::Part) => NormalDependencyInvalidationKind::Parts, + } + } + + /// The kind of invalidation that this would generate. + pub fn invalidation_kind(&self) -> DependencyInvalidationKind { + if let Some(kind) = self.relative_kind { + return DependencyInvalidationKind::Relative(kind); + } + DependencyInvalidationKind::Normal(self.normal_invalidation_kind()) + } + + /// Is the combinator to the right of this dependency's compound selector + /// the next sibling combinator? This matters for insertion/removal in between + /// two elements connected through next sibling, e.g. `.foo:has(> .a + .b)` + /// where an element gets inserted between `.a` and `.b`. + pub fn right_combinator_is_next_sibling(&self) -> bool { + if self.selector_offset == 0 { + return false; + } + matches!( + self.selector + .combinator_at_match_order(self.selector_offset - 1), + Combinator::NextSibling + ) + } + + /// Is this dependency's compound selector a single compound in `:has` + /// with the next sibling relative combinator i.e. `:has(> .foo)`? + /// This matters for insertion between an anchor and an element + /// connected through next sibling, e.g. `.a:has(> .b)`. + pub fn dependency_is_relative_with_single_next_sibling(&self) -> bool { + match self.invalidation_kind() { + DependencyInvalidationKind::Normal(_) => false, + DependencyInvalidationKind::Relative(kind) => { + kind == RelativeDependencyInvalidationKind::PrevSibling + }, + } + } +} + +/// 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, +} + +/// Dependency mapping for classes or IDs. +pub type IdOrClassDependencyMap = MaybeCaseInsensitiveHashMap>; +/// Dependency mapping for non-tree-strctural pseudo-class states. +pub type StateDependencyMap = SelectorMap; +/// Dependency mapping for local names. +pub type LocalNameDependencyMap = PrecomputedHashMap>; + +/// A map where we store invalidations. +/// +/// This is slightly different to a SelectorMap, in the sense of that the same +/// selector may appear multiple times. +/// +/// In particular, we want to lookup as few things as possible to get the fewer +/// selectors the better, so this looks up by id, class, or looks at the list of +/// state/other attribute affecting selectors. +#[derive(Clone, Debug, MallocSizeOf)] +pub struct InvalidationMap { + /// A map from a given class name to all the selectors with that class + /// selector. + pub class_to_selector: IdOrClassDependencyMap, + /// 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: IdOrClassDependencyMap, + /// A map of all the state dependencies. + pub state_affecting_selectors: StateDependencyMap, + /// 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: LocalNameDependencyMap, +} + +/// Tree-structural pseudoclasses that we care about for (Relative selector) invalidation. +/// Specifically, we need to store information on ones that don't generate the inner selector. +#[derive(Clone, Copy, Debug, MallocSizeOf)] +pub struct TSStateForInvalidation(u8); + +bitflags! { + impl TSStateForInvalidation : u8 { + /// :empty + const EMPTY = 1 << 0; + /// :nth etc, without of. + const NTH = 1 << 1; + /// "Simple" edge child selectors, like :first-child, :last-child, etc. + /// Excludes :*-of-type. + const NTH_EDGE = 1 << 2; + } +} + +/// Dependency for tree-structural pseudo-classes. +#[derive(Clone, Debug, MallocSizeOf)] +pub struct TSStateDependency { + /// The other dependency fields. + pub dep: Dependency, + /// The state this dependency is affected by. + pub state: TSStateForInvalidation, +} + +impl SelectorMapEntry for TSStateDependency { + fn selector(&self) -> SelectorIter { + self.dep.selector() + } +} + +/// Dependency mapping for tree-structural pseudo-class states. +pub type TSStateDependencyMap = SelectorMap; +/// Dependency mapping for * selectors. +pub type AnyDependencyMap = SmallVec<[Dependency; 1]>; + +/// A map to store all relative selector invalidations. +/// This keeps a lot more data than the usual map, because any change can generate +/// upward traversals that need to be handled separately. +#[derive(Clone, Debug, MallocSizeOf)] +pub struct RelativeSelectorInvalidationMap { + /// Portion common to the normal invalidation map, except that this is for relative selectors and their inner selectors. + pub map: InvalidationMap, + /// A map for a given tree-structural pseudo-class to all the relative selector dependencies with that type. + pub ts_state_to_selector: TSStateDependencyMap, + /// A map from a given type name to all the relative selector dependencies with that type. + pub type_to_selector: LocalNameDependencyMap, + /// All relative selector dependencies that specify `*`. + pub any_to_selector: AnyDependencyMap, + /// Flag indicating if any relative selector is used. + pub used: bool, + /// Flag indicating if invalidating a relative selector requires ancestor traversal. + pub needs_ancestors_traversal: bool, +} + +impl RelativeSelectorInvalidationMap { + /// Creates an empty `InvalidationMap`. + pub fn new() -> Self { + Self { + map: InvalidationMap::new(), + ts_state_to_selector: TSStateDependencyMap::default(), + type_to_selector: LocalNameDependencyMap::default(), + any_to_selector: SmallVec::default(), + used: false, + needs_ancestors_traversal: false, + } + } + + /// Returns the number of dependencies stored in the invalidation map. + pub fn len(&self) -> usize { + self.map.len() + } + + /// Clears this map, leaving it empty. + pub fn clear(&mut self) { + self.map.clear(); + self.ts_state_to_selector.clear(); + self.type_to_selector.clear(); + self.any_to_selector.clear(); + } + + /// Shrink the capacity of hash maps if needed. + pub fn shrink_if_needed(&mut self) { + self.map.shrink_if_needed(); + self.ts_state_to_selector.shrink_if_needed(); + self.type_to_selector.shrink_if_needed(); + } +} + +impl InvalidationMap { + /// Creates an empty `InvalidationMap`. + pub fn new() -> Self { + Self { + class_to_selector: IdOrClassDependencyMap::new(), + id_to_selector: IdOrClassDependencyMap::new(), + state_affecting_selectors: StateDependencyMap::new(), + document_state_selectors: Vec::new(), + other_attribute_affecting_selectors: LocalNameDependencyMap::default(), + } + } + + /// Returns the number of dependencies stored in the invalidation map. + pub fn len(&self) -> usize { + self.state_affecting_selectors.len() + + self.document_state_selectors.len() + + self.other_attribute_affecting_selectors + .iter() + .fold(0, |accum, (_, ref v)| accum + v.len()) + + self.id_to_selector + .iter() + .fold(0, |accum, (_, ref v)| accum + v.len()) + + self.class_to_selector + .iter() + .fold(0, |accum, (_, ref v)| accum + v.len()) + } + + /// Clears this map, leaving it empty. + pub fn clear(&mut self) { + self.class_to_selector.clear(); + self.id_to_selector.clear(); + self.state_affecting_selectors.clear(); + self.document_state_selectors.clear(); + self.other_attribute_affecting_selectors.clear(); + } + + /// Shrink the capacity of hash maps if needed. + pub fn shrink_if_needed(&mut self) { + self.class_to_selector.shrink_if_needed(); + self.id_to_selector.shrink_if_needed(); + self.state_affecting_selectors.shrink_if_needed(); + self.other_attribute_affecting_selectors.shrink_if_needed(); + } +} + +/// Adds a selector to the given `InvalidationMap`. Returns Err(..) to signify OOM. +pub fn note_selector_for_invalidation( + selector: &Selector, + quirks_mode: QuirksMode, + map: &mut InvalidationMap, + relative_selector_invalidation_map: &mut RelativeSelectorInvalidationMap, +) -> Result<(), AllocErr> { + debug!("note_selector_for_invalidation({:?})", selector); + + let mut document_state = DocumentState::empty(); + { + let mut parent_stack = ParentSelectors::new(); + let mut alloc_error = None; + let mut collector = SelectorDependencyCollector { + map, + relative_selector_invalidation_map, + 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()), + }; + map.document_state_selectors.try_reserve(1)?; + map.document_state_selectors.push(dep); + } + Ok(()) +} +struct PerCompoundState { + /// The offset at which our compound starts. + offset: usize, + + /// The state this compound selector is affected by. + element_state: ElementState, +} + +impl PerCompoundState { + fn new(offset: usize) -> Self { + Self { + offset, + element_state: ElementState::empty(), + } + } +} + +struct ParentDependencyEntry { + selector: Selector, + offset: usize, + cached_dependency: Option>, +} + +trait Collector { + fn dependency(&mut self) -> Dependency; + fn id_map(&mut self) -> &mut IdOrClassDependencyMap; + fn class_map(&mut self) -> &mut IdOrClassDependencyMap; + fn state_map(&mut self) -> &mut StateDependencyMap; + fn attribute_map(&mut self) -> &mut LocalNameDependencyMap; + fn update_states(&mut self, element_state: ElementState, document_state: DocumentState); + + // In normal invalidations, type-based dependencies don't need to be explicitly tracked; + // elements don't change their types, and mutations cause invalidations to go descendant + // (Where they are about to be styled anyway), and/or later-sibling direction (Where they + // siblings after inserted/removed elements get restyled anyway). + // However, for relative selectors, a DOM mutation can affect and arbitrary ancestor and/or + // earlier siblings, so we need to keep track of them. + fn type_map(&mut self) -> &mut LocalNameDependencyMap { + unreachable!(); + } + + // Tree-structural pseudo-selectors generally invalidates in a well-defined way, which are + // handled by RestyleManager. However, for relative selectors, as with type invalidations, + // the direction of invalidation becomes arbitrary, so we need to keep track of them. + fn ts_state_map(&mut self) -> &mut TSStateDependencyMap { + unreachable!(); + } + + // Same story as type invalidation maps. + fn any_vec(&mut self) -> &mut AnyDependencyMap { + unreachable!(); + } +} + +fn on_attribute( + local_name: &LocalName, + local_name_lower: &LocalName, + collector: &mut C, +) -> Result<(), AllocErr> { + add_attr_dependency(local_name.clone(), collector)?; + + if local_name != local_name_lower { + add_attr_dependency(local_name_lower.clone(), collector)?; + } + Ok(()) +} + +fn on_id_or_class( + s: &Component, + quirks_mode: QuirksMode, + collector: &mut C, +) -> Result<(), AllocErr> { + let dependency = collector.dependency(); + let (atom, map) = match *s { + Component::ID(ref atom) => (atom, collector.id_map()), + Component::Class(ref atom) => (atom, collector.class_map()), + _ => unreachable!(), + }; + let entry = map.try_entry(atom.0.clone(), quirks_mode)?; + let vec = entry.or_insert_with(SmallVec::new); + vec.try_reserve(1)?; + vec.push(dependency); + Ok(()) +} + +fn add_attr_dependency(name: LocalName, collector: &mut C) -> Result<(), AllocErr> { + let dependency = collector.dependency(); + let map = collector.attribute_map(); + add_local_name(name, dependency, map) +} + +fn add_local_name( + name: LocalName, + dependency: Dependency, + map: &mut LocalNameDependencyMap, +) -> Result<(), AllocErr> { + map.try_reserve(1)?; + let vec = map.entry(name).or_default(); + vec.try_reserve(1)?; + vec.push(dependency); + Ok(()) +} + +fn on_pseudo_class(pc: &NonTSPseudoClass, collector: &mut C) -> Result<(), AllocErr> { + collector.update_states(pc.state_flag(), pc.document_state_flag()); + + let attr_name = match *pc { + #[cfg(feature = "gecko")] + NonTSPseudoClass::MozTableBorderNonzero => local_name!("border"), + #[cfg(feature = "gecko")] + NonTSPseudoClass::MozSelectListBox => { + // This depends on two attributes. + add_attr_dependency(local_name!("multiple"), collector)?; + return add_attr_dependency(local_name!("size"), collector); + }, + NonTSPseudoClass::Lang(..) => local_name!("lang"), + _ => return Ok(()), + }; + + add_attr_dependency(attr_name, collector) +} + +fn add_pseudo_class_dependency( + element_state: ElementState, + quirks_mode: QuirksMode, + collector: &mut C, +) -> Result<(), AllocErr> { + if element_state.is_empty() { + return Ok(()); + } + let dependency = collector.dependency(); + collector.state_map().insert( + StateDependency { + dep: dependency, + state: element_state, + }, + quirks_mode, + ) +} + +type ParentSelectors = SmallVec<[ParentDependencyEntry; 5]>; + +/// A struct that collects invalidations for a given compound selector. +struct SelectorDependencyCollector<'a> { + map: &'a mut InvalidationMap, + relative_selector_invalidation_map: &'a mut RelativeSelectorInvalidationMap, + + /// 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. The dependency field is cached and reference counted. + parent_selectors: &'a mut ParentSelectors, + + /// 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, +} + +fn parent_dependency( + parent_selectors: &mut ParentSelectors, + outer_parent: Option<&Arc>, +) -> Option> { + if parent_selectors.is_empty() { + return outer_parent.cloned(); + } + + fn dependencies_from( + entries: &mut [ParentDependencyEntry], + outer_parent: &Option<&Arc>, + ) -> Option> { + if entries.is_empty() { + return None; + } + + let last_index = entries.len() - 1; + let (previous, last) = entries.split_at_mut(last_index); + let last = &mut last[0]; + let selector = &last.selector; + let selector_offset = last.offset; + Some( + last.cached_dependency + .get_or_insert_with(|| { + Arc::new(Dependency { + selector: selector.clone(), + selector_offset, + parent: dependencies_from(previous, outer_parent), + relative_kind: None, + }) + }) + .clone(), + ) + } + + dependencies_from(parent_selectors, &outer_parent) +} + +impl<'a> Collector for SelectorDependencyCollector<'a> { + fn dependency(&mut self) -> Dependency { + let parent = parent_dependency(self.parent_selectors, None); + Dependency { + selector: self.selector.clone(), + selector_offset: self.compound_state.offset, + parent, + relative_kind: None, + } + } + + fn id_map(&mut self) -> &mut IdOrClassDependencyMap { + &mut self.map.id_to_selector + } + + fn class_map(&mut self) -> &mut IdOrClassDependencyMap { + &mut self.map.class_to_selector + } + + fn state_map(&mut self) -> &mut StateDependencyMap { + &mut self.map.state_affecting_selectors + } + + fn attribute_map(&mut self) -> &mut LocalNameDependencyMap { + &mut self.map.other_attribute_affecting_selectors + } + + fn update_states(&mut self, element_state: ElementState, document_state: DocumentState) { + self.compound_state.element_state |= element_state; + *self.document_state |= document_state; + } +} + +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 let Err(err) = add_pseudo_class_dependency( + self.compound_state.element_state, + self.quirks_mode, + self, + ) { + *self.alloc_error = Some(err); + return false; + } + + let combinator = iter.next_sequence(); + if combinator.is_none() { + return true; + } + index += 1; // account for the combinator + } + } +} + +impl<'a> SelectorVisitor for SelectorDependencyCollector<'a> { + type Impl = SelectorImpl; + + fn visit_selector_list( + &mut self, + _list_kind: SelectorListKind, + list: &[Selector], + ) -> bool { + for selector in list { + // Here we cheat a bit: We can visit the rightmost compound with + // the "outer" visitor, and it'd be fine. This reduces the amount of + // state and attribute invalidations, and we need to check the outer + // selector to the left anyway to avoid over-invalidation, so it + // avoids matching it twice uselessly. + let mut iter = selector.iter(); + let mut index = 0; + + for ss in &mut iter { + if !ss.visit(self) { + return false; + } + index += 1; + } + + let combinator = iter.next_sequence(); + if combinator.is_none() { + continue; + } + + index += 1; // account for the combinator. + + self.parent_selectors.push(ParentDependencyEntry { + selector: self.selector.clone(), + offset: self.compound_state.offset, + cached_dependency: None, + }); + let mut nested = SelectorDependencyCollector { + map: &mut *self.map, + relative_selector_invalidation_map: &mut *self.relative_selector_invalidation_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_relative_selector_list( + &mut self, + list: &[selectors::parser::RelativeSelector], + ) -> bool { + self.relative_selector_invalidation_map.used = true; + for relative_selector in list { + // We can't cheat here like we do with other selector lists - the rightmost + // compound of a relative selector is not the subject of the invalidation. + self.parent_selectors.push(ParentDependencyEntry { + selector: self.selector.clone(), + offset: self.compound_state.offset, + cached_dependency: None, + }); + let mut nested = RelativeSelectorDependencyCollector { + map: &mut *self.relative_selector_invalidation_map, + document_state: &mut *self.document_state, + selector: &relative_selector, + combinator_count: RelativeSelectorCombinatorCount::new(relative_selector), + parent_selectors: &mut *self.parent_selectors, + quirks_mode: self.quirks_mode, + compound_state: RelativeSelectorPerCompoundState::new(0), + alloc_error: &mut *self.alloc_error, + }; + if !nested.visit_whole_selector() { + return false; + } + self.parent_selectors.pop(); + } + true + } + + fn visit_simple_selector(&mut self, s: &Component) -> bool { + match *s { + Component::ID(..) | Component::Class(..) => { + if let Err(err) = on_id_or_class(s, self.quirks_mode, self) { + *self.alloc_error = Some(err.into()); + return false; + } + true + }, + Component::NonTSPseudoClass(ref pc) => { + if let Err(err) = on_pseudo_class(pc, self) { + *self.alloc_error = Some(err.into()); + return false; + } + true + }, + _ => true, + } + } + + fn visit_attribute_selector( + &mut self, + _: &NamespaceConstraint<&Namespace>, + local_name: &LocalName, + local_name_lower: &LocalName, + ) -> bool { + if let Err(err) = on_attribute(local_name, local_name_lower, self) { + *self.alloc_error = Some(err); + return false; + } + true + } +} + +struct RelativeSelectorPerCompoundState { + state: PerCompoundState, + ts_state: TSStateForInvalidation, + added_entry: bool, +} + +impl RelativeSelectorPerCompoundState { + fn new(offset: usize) -> Self { + Self { + state: PerCompoundState::new(offset), + ts_state: TSStateForInvalidation::empty(), + added_entry: false, + } + } +} + +/// A struct that collects invalidations for a given compound selector. +struct RelativeSelectorDependencyCollector<'a> { + map: &'a mut RelativeSelectorInvalidationMap, + + /// 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 inner relative selector and offset we're iterating. + selector: &'a RelativeSelector, + /// Running combinator for this inner relative selector. + combinator_count: RelativeSelectorCombinatorCount, + + /// 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. The dependency field is cached and reference counted. + parent_selectors: &'a mut ParentSelectors, + + /// The quirks mode of the document where we're inserting dependencies. + quirks_mode: QuirksMode, + + /// State relevant to a given compound selector. + compound_state: RelativeSelectorPerCompoundState, + + /// The allocation error, if we OOM. + alloc_error: &'a mut Option, +} + +fn add_non_unique_info( + selector: &Selector, + offset: usize, + collector: &mut C, +) -> Result<(), AllocErr> { + // Go through this compound again. + for ss in selector.iter_from(offset) { + match ss { + Component::LocalName(ref name) => { + let dependency = collector.dependency(); + add_local_name(name.name.clone(), dependency, &mut collector.type_map())?; + if name.name != name.lower_name { + let dependency = collector.dependency(); + add_local_name( + name.lower_name.clone(), + dependency, + &mut collector.type_map(), + )?; + } + return Ok(()); + }, + _ => (), + }; + } + // Ouch. Add one for *. + collector.any_vec().try_reserve(1)?; + let dependency = collector.dependency(); + collector.any_vec().push(dependency); + Ok(()) +} + +fn add_ts_pseudo_class_dependency( + state: TSStateForInvalidation, + quirks_mode: QuirksMode, + collector: &mut C, +) -> Result<(), AllocErr> { + if state.is_empty() { + return Ok(()); + } + let dependency = collector.dependency(); + collector.ts_state_map().insert( + TSStateDependency { + dep: dependency, + state, + }, + quirks_mode, + ) +} + +impl<'a> RelativeSelectorDependencyCollector<'a> { + fn visit_whole_selector(&mut self) -> bool { + let mut iter = self.selector.selector.iter_skip_relative_selector_anchor(); + let mut index = 0; + + self.map.needs_ancestors_traversal |= match self.selector.match_hint { + RelativeSelectorMatchHint::InNextSiblingSubtree | + RelativeSelectorMatchHint::InSiblingSubtree | + RelativeSelectorMatchHint::InSubtree => true, + _ => false, + }; + loop { + // Reset the compound state. + self.compound_state = RelativeSelectorPerCompoundState::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 let Err(err) = add_pseudo_class_dependency( + self.compound_state.state.element_state, + self.quirks_mode, + self, + ) { + *self.alloc_error = Some(err); + return false; + } + if let Err(err) = + add_ts_pseudo_class_dependency(self.compound_state.ts_state, self.quirks_mode, self) + { + *self.alloc_error = Some(err); + return false; + } + + if !self.compound_state.added_entry { + // Not great - we didn't add any uniquely identifiable information. + if let Err(err) = add_non_unique_info( + &self.selector.selector, + self.compound_state.state.offset, + self, + ) { + *self.alloc_error = Some(err); + return false; + } + } + + let combinator = iter.next_sequence(); + if let Some(c) = combinator { + match c { + Combinator::Child | Combinator::Descendant => { + self.combinator_count.child_or_descendants -= 1 + }, + Combinator::NextSibling | Combinator::LaterSibling => { + self.combinator_count.adjacent_or_next_siblings -= 1 + }, + Combinator::Part | Combinator::PseudoElement | Combinator::SlotAssignment => (), + } + } else { + return true; + } + index += 1; // account for the combinator + } + } +} + +impl<'a> Collector for RelativeSelectorDependencyCollector<'a> { + fn dependency(&mut self) -> Dependency { + let parent = parent_dependency(self.parent_selectors, None); + Dependency { + selector: self.selector.selector.clone(), + selector_offset: self.compound_state.state.offset, + relative_kind: Some(match self.combinator_count.get_match_hint() { + RelativeSelectorMatchHint::InChild => RelativeDependencyInvalidationKind::Parent, + RelativeSelectorMatchHint::InSubtree => { + RelativeDependencyInvalidationKind::Ancestors + }, + RelativeSelectorMatchHint::InNextSibling => { + RelativeDependencyInvalidationKind::PrevSibling + }, + RelativeSelectorMatchHint::InSibling => { + RelativeDependencyInvalidationKind::EarlierSibling + }, + RelativeSelectorMatchHint::InNextSiblingSubtree => { + RelativeDependencyInvalidationKind::AncestorPrevSibling + }, + RelativeSelectorMatchHint::InSiblingSubtree => { + RelativeDependencyInvalidationKind::AncestorEarlierSibling + }, + }), + parent, + } + } + + fn id_map(&mut self) -> &mut IdOrClassDependencyMap { + &mut self.map.map.id_to_selector + } + + fn class_map(&mut self) -> &mut IdOrClassDependencyMap { + &mut self.map.map.class_to_selector + } + + fn state_map(&mut self) -> &mut StateDependencyMap { + &mut self.map.map.state_affecting_selectors + } + + fn attribute_map(&mut self) -> &mut LocalNameDependencyMap { + &mut self.map.map.other_attribute_affecting_selectors + } + + fn update_states(&mut self, element_state: ElementState, document_state: DocumentState) { + self.compound_state.state.element_state |= element_state; + *self.document_state |= document_state; + } + + fn type_map(&mut self) -> &mut LocalNameDependencyMap { + &mut self.map.type_to_selector + } + + fn ts_state_map(&mut self) -> &mut TSStateDependencyMap { + &mut self.map.ts_state_to_selector + } + + fn any_vec(&mut self) -> &mut AnyDependencyMap { + &mut self.map.any_to_selector + } +} + +impl<'a> SelectorVisitor for RelativeSelectorDependencyCollector<'a> { + type Impl = SelectorImpl; + + fn visit_selector_list( + &mut self, + _list_kind: SelectorListKind, + list: &[Selector], + ) -> bool { + let mut parent_stack = ParentSelectors::new(); + let parent_dependency = Arc::new(self.dependency()); + for selector in list { + // Subjects inside relative selectors aren't really subjects. + // This simplifies compound state tracking as well (Additional + // states we track for relative selector's inner selectors should + // not leak out of the relevant selector). + let mut nested = RelativeSelectorInnerDependencyCollector { + map: &mut *self.map, + parent_dependency: &parent_dependency, + document_state: &mut *self.document_state, + selector, + parent_selectors: &mut parent_stack, + quirks_mode: self.quirks_mode, + compound_state: RelativeSelectorPerCompoundState::new(0), + alloc_error: &mut *self.alloc_error, + }; + if !nested.visit_whole_selector() { + return false; + } + } + true + } + + fn visit_relative_selector_list( + &mut self, + _list: &[selectors::parser::RelativeSelector], + ) -> bool { + // Ignore nested relative selectors. These can happen as a result of nesting. + true + } + + fn visit_simple_selector(&mut self, s: &Component) -> bool { + match *s { + Component::ID(..) | Component::Class(..) => { + self.compound_state.added_entry = true; + if let Err(err) = on_id_or_class(s, self.quirks_mode, self) { + *self.alloc_error = Some(err.into()); + return false; + } + true + }, + Component::NonTSPseudoClass(ref pc) => { + if !pc + .state_flag() + .intersects(ElementState::VISITED_OR_UNVISITED) + { + // Visited/Unvisited styling doesn't take the usual state invalidation path. + self.compound_state.added_entry = true; + } + if let Err(err) = on_pseudo_class(pc, self) { + *self.alloc_error = Some(err.into()); + return false; + } + true + }, + Component::Empty => { + self.compound_state + .ts_state + .insert(TSStateForInvalidation::EMPTY); + true + }, + Component::Nth(data) => { + let kind = if data.is_simple_edge() { + TSStateForInvalidation::NTH_EDGE + } else { + TSStateForInvalidation::NTH + }; + self.compound_state + .ts_state + .insert(kind); + true + }, + Component::RelativeSelectorAnchor => unreachable!("Should not visit this far"), + _ => true, + } + } + + fn visit_attribute_selector( + &mut self, + _: &NamespaceConstraint<&Namespace>, + local_name: &LocalName, + local_name_lower: &LocalName, + ) -> bool { + self.compound_state.added_entry = true; + if let Err(err) = on_attribute(local_name, local_name_lower, self) { + *self.alloc_error = Some(err); + return false; + } + true + } +} + +/// A struct that collects invalidations from a complex selector inside a relative selector. +/// TODO(dshin): All of this duplication is not great Perhaps should be merged to the normal +/// one, if possible? See bug 1855690. +struct RelativeSelectorInnerDependencyCollector<'a, 'b> { + map: &'a mut RelativeSelectorInvalidationMap, + + /// 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, + + /// Parent relative selector dependency. + parent_dependency: &'b Arc, + + /// The current inner relative 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. The dependency field is cached and reference counted. + parent_selectors: &'a mut ParentSelectors, + + /// The quirks mode of the document where we're inserting dependencies. + quirks_mode: QuirksMode, + + /// State relevant to a given compound selector. + compound_state: RelativeSelectorPerCompoundState, + + /// The allocation error, if we OOM. + alloc_error: &'a mut Option, +} + +impl<'a, 'b> Collector for RelativeSelectorInnerDependencyCollector<'a, 'b> { + fn dependency(&mut self) -> Dependency { + let parent = parent_dependency(self.parent_selectors, Some(self.parent_dependency)); + Dependency { + selector: self.selector.clone(), + selector_offset: self.compound_state.state.offset, + parent, + relative_kind: None, + } + } + + fn id_map(&mut self) -> &mut IdOrClassDependencyMap { + &mut self.map.map.id_to_selector + } + + fn class_map(&mut self) -> &mut IdOrClassDependencyMap { + &mut self.map.map.class_to_selector + } + + fn state_map(&mut self) -> &mut StateDependencyMap { + &mut self.map.map.state_affecting_selectors + } + + fn attribute_map(&mut self) -> &mut LocalNameDependencyMap { + &mut self.map.map.other_attribute_affecting_selectors + } + + fn update_states(&mut self, element_state: ElementState, document_state: DocumentState) { + self.compound_state.state.element_state |= element_state; + *self.document_state |= document_state; + } + + fn type_map(&mut self) -> &mut LocalNameDependencyMap { + &mut self.map.type_to_selector + } + + fn ts_state_map(&mut self) -> &mut TSStateDependencyMap { + &mut self.map.ts_state_to_selector + } + + fn any_vec(&mut self) -> &mut AnyDependencyMap { + &mut self.map.any_to_selector + } +} + +impl<'a, 'b> RelativeSelectorInnerDependencyCollector<'a, 'b> { + fn visit_whole_selector(&mut self) -> bool { + let mut iter = self.selector.iter(); + let mut index = 0; + loop { + // Reset the compound state. + self.compound_state = RelativeSelectorPerCompoundState::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 let Err(err) = add_pseudo_class_dependency( + self.compound_state.state.element_state, + self.quirks_mode, + self, + ) { + *self.alloc_error = Some(err); + return false; + } + + if let Err(err) = + add_ts_pseudo_class_dependency(self.compound_state.ts_state, self.quirks_mode, self) + { + *self.alloc_error = Some(err); + return false; + } + + if !self.compound_state.added_entry { + // Not great - we didn't add any uniquely identifiable information. + if let Err(err) = + add_non_unique_info(&self.selector, self.compound_state.state.offset, self) + { + *self.alloc_error = Some(err); + return false; + } + } + + let combinator = iter.next_sequence(); + if combinator.is_none() { + return true; + } + index += 1; // account for the combinator + } + } +} + +impl<'a, 'b> SelectorVisitor for RelativeSelectorInnerDependencyCollector<'a, 'b> { + type Impl = SelectorImpl; + + fn visit_selector_list( + &mut self, + _list_kind: SelectorListKind, + list: &[Selector], + ) -> bool { + let parent_dependency = Arc::new(self.dependency()); + for selector in list { + // Subjects inside relative selectors aren't really subjects. + // This simplifies compound state tracking as well (Additional + // states we track for relative selector's inner selectors should + // not leak out of the relevant selector). + let mut nested = RelativeSelectorInnerDependencyCollector { + map: &mut *self.map, + parent_dependency: &parent_dependency, + document_state: &mut *self.document_state, + selector, + parent_selectors: &mut *self.parent_selectors, + quirks_mode: self.quirks_mode, + compound_state: RelativeSelectorPerCompoundState::new(0), + alloc_error: &mut *self.alloc_error, + }; + if !nested.visit_whole_selector() { + return false; + } + } + true + } + + fn visit_relative_selector_list( + &mut self, + _list: &[selectors::parser::RelativeSelector], + ) -> bool { + // Ignore nested relative selectors. These can happen as a result of nesting. + true + } + + fn visit_simple_selector(&mut self, s: &Component) -> bool { + match *s { + Component::ID(..) | Component::Class(..) => { + self.compound_state.added_entry = true; + if let Err(err) = on_id_or_class(s, self.quirks_mode, self) { + *self.alloc_error = Some(err.into()); + return false; + } + true + }, + Component::NonTSPseudoClass(ref pc) => { + if !pc + .state_flag() + .intersects(ElementState::VISITED_OR_UNVISITED) + { + // Visited/Unvisited styling doesn't take the usual state invalidation path. + self.compound_state.added_entry = true; + } + if let Err(err) = on_pseudo_class(pc, self) { + *self.alloc_error = Some(err.into()); + return false; + } + true + }, + Component::Empty => { + self.compound_state + .ts_state + .insert(TSStateForInvalidation::EMPTY); + true + }, + Component::Nth(data) => { + let kind = if data.is_simple_edge() { + TSStateForInvalidation::NTH_EDGE + } else { + TSStateForInvalidation::NTH + }; + self.compound_state + .ts_state + .insert(kind); + true + }, + Component::RelativeSelectorAnchor => unreachable!("Should not visit this far"), + _ => true, + } + } + + fn visit_attribute_selector( + &mut self, + _: &NamespaceConstraint<&Namespace>, + local_name: &LocalName, + local_name_lower: &LocalName, + ) -> bool { + self.compound_state.added_entry = true; + if let Err(err) = on_attribute(local_name, local_name_lower, self) { + *self.alloc_error = Some(err); + 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..5dee1f5dcf --- /dev/null +++ b/servo/components/style/invalidation/element/invalidator.rs @@ -0,0 +1,1130 @@ +/* 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, NormalDependencyInvalidationKind, + RelativeDependencyInvalidationKind, +}; +use selectors::matching::matches_compound_selector_from; +use selectors::matching::{CompoundSelectorMatchingResult, MatchingContext}; +use selectors::parser::{Combinator, Component}; +use selectors::OpaqueElement; +use smallvec::SmallVec; +use std::fmt; +use std::fmt::Write; + +struct SiblingInfo +where + E: TElement, +{ + affected: E, + prev_sibling: Option, + next_sibling: Option, +} + +/// Traversal mapping for elements under consideration. It acts like a snapshot map, +/// though this only "maps" one element at most. +/// For general invalidations, this has no effect, especially since when +/// DOM mutates, the mutation's effect should not escape the subtree being mutated. +/// This is not the case for relative selectors, unfortunately, so we may end up +/// traversing a portion of the DOM tree that mutated. In case the mutation is removal, +/// its sibling relation is severed by the time the invalidation happens. This structure +/// recovers that relation. Note - it assumes that there is only one element under this +/// effect. +pub struct SiblingTraversalMap +where + E: TElement, +{ + info: Option>, +} + +impl Default for SiblingTraversalMap +where + E: TElement, +{ + fn default() -> Self { + Self { info: None } + } +} + +impl SiblingTraversalMap +where + E: TElement, +{ + /// Create a new traversal map with the affected element. + pub fn new(affected: E, prev_sibling: Option, next_sibling: Option) -> Self { + Self { + info: Some(SiblingInfo { + affected, + prev_sibling, + next_sibling, + }), + } + } + + /// Get the element's previous sibling element. + pub fn next_sibling_for(&self, element: &E) -> Option { + if let Some(ref info) = self.info { + if *element == info.affected { + return info.next_sibling; + } + } + element.next_sibling_element() + } + + /// Get the element's previous sibling element. + pub fn prev_sibling_for(&self, element: &E) -> Option { + if let Some(ref info) = self.info { + if *element == info.affected { + return info.prev_sibling; + } + } + element.prev_sibling_element() + } +} + +/// A trait to abstract the collection of invalidations for a given pass. +pub trait InvalidationProcessor<'a, 'b, E> +where + E: TElement, +{ + /// Whether an invalidation that contains only a pseudo-element selector + /// like ::before or ::after triggers invalidation of the element that would + /// originate it. + fn invalidates_on_pseudo_element(&self) -> bool { + false + } + + /// Whether the invalidation processor only cares about light-tree + /// descendants of a given element, that is, doesn't invalidate + /// pseudo-elements, NAC, shadow dom... + fn light_tree_only(&self) -> bool { + false + } + + /// When a dependency from a :where or :is selector matches, it may still be + /// the case that we don't need to invalidate the full style. Consider the + /// case of: + /// + /// div .foo:where(.bar *, .baz) .qux + /// + /// We can get to the `*` part after a .bar class change, but you only need + /// to restyle the element if it also matches .foo. + /// + /// Similarly, you only need to restyle .baz if the whole result of matching + /// the selector changes. + /// + /// This function is called to check the result of matching the "outer" + /// dependency that we generate for the parent of the `:where` selector, + /// that is, in the case above it should match + /// `div .foo:where(.bar *, .baz)`. + /// + /// Returning true unconditionally here is over-optimistic and may + /// over-invalidate. + fn check_outer_dependency(&mut self, dependency: &Dependency, element: E) -> bool; + + /// The matching context that should be used to process invalidations. + fn matching_context(&mut self) -> &mut MatchingContext<'b, E::Impl>; + + /// The traversal map that should be used to process invalidations. + fn sibling_traversal_map(&self) -> &SiblingTraversalMap; + + /// Collect invalidations for a given element's descendants and siblings. + /// + /// Returns whether the element itself was invalidated. + fn collect_invalidations( + &mut self, + element: E, + self_invalidations: &mut InvalidationVector<'a>, + descendant_invalidations: &mut DescendantInvalidationLists<'a>, + sibling_invalidations: &mut InvalidationVector<'a>, + ) -> bool; + + /// Returns whether the invalidation process should process the descendants + /// of the given element. + fn should_process_descendants(&mut self, element: E) -> bool; + + /// Executes an arbitrary action when the recursion limit is exceded (if + /// any). + fn recursion_limit_exceeded(&mut self, element: E); + + /// Executes an action when `Self` is invalidated. + fn invalidated_self(&mut self, element: E); + + /// Executes an action when `sibling` is invalidated as a sibling of + /// `of`. + fn invalidated_sibling(&mut self, sibling: E, of: E); + + /// Executes an action when any descendant of `Self` is invalidated. + fn invalidated_descendants(&mut self, element: E, child: E); + + /// Executes an action when an element in a relative selector is reached. + /// Lets the dependency to be borrowed for further processing out of the + /// invalidation traversal. + fn found_relative_selector_invalidation( + &mut self, + _element: E, + _kind: RelativeDependencyInvalidationKind, + _relative_dependency: &'a Dependency, + ) { + debug_assert!(false, "Reached relative selector dependency"); + } +} + +/// 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, 'c, E, P: 'a> +where + 'b: 'a, + E: TElement, + P: InvalidationProcessor<'b, 'c, E>, +{ + element: E, + stack_limit_checker: Option<&'a StackLimitChecker>, + processor: &'a mut P, + _marker: std::marker::PhantomData<(&'b (), &'c ())>, +} + +/// 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.normal_invalidation_kind() != + NormalDependencyInvalidationKind::Element, + "No point to this, if the dependency matched the element we should just invalidate it" + ); + Self { + dependency, + scope, + // + 1 to go past the combinator. + offset: dependency.selector.len() + 1 - dependency.selector_offset, + matched_by_any_previous: false, + } + } + + /// Whether this invalidation is effective for the next sibling or + /// descendant after us. + fn effective_for_next(&self) -> bool { + if self.offset == 0 { + return true; + } + + // TODO(emilio): For pseudo-elements this should be mostly false, except + // for the weird pseudos in . + // + // We should be able to do better here! + match self + .dependency + .selector + .combinator_at_parse_order(self.offset - 1) + { + Combinator::Descendant | Combinator::LaterSibling | Combinator::PseudoElement => true, + Combinator::Part | + Combinator::SlotAssignment | + Combinator::NextSibling | + Combinator::Child => false, + } + } + + fn kind(&self) -> InvalidationKind { + if self.offset == 0 { + return InvalidationKind::Descendant(DescendantInvalidationKind::Dom); + } + + match self + .dependency + .selector + .combinator_at_parse_order(self.offset - 1) + { + Combinator::Child | Combinator::Descendant | Combinator::PseudoElement => { + InvalidationKind::Descendant(DescendantInvalidationKind::Dom) + }, + Combinator::Part => InvalidationKind::Descendant(DescendantInvalidationKind::Part), + Combinator::SlotAssignment => { + InvalidationKind::Descendant(DescendantInvalidationKind::Slotted) + }, + Combinator::NextSibling | Combinator::LaterSibling => InvalidationKind::Sibling, + } + } +} + +impl<'a> fmt::Debug for Invalidation<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use cssparser::ToCss; + + f.write_str("Invalidation(")?; + for component in self + .dependency + .selector + .iter_raw_parse_order_from(self.offset) + { + if matches!(*component, Component::Combinator(..)) { + break; + } + component.to_css(f)?; + } + f.write_char(')') + } +} + +/// The result of processing a single invalidation for a given element. +struct SingleInvalidationResult { + /// Whether the element itself was invalidated. + invalidated_self: bool, + /// Whether the invalidation matched, either invalidating the element or + /// generating another invalidation. + matched: bool, +} + +/// The result of a whole invalidation process for a given element. +pub struct InvalidationResult { + /// Whether the element itself was invalidated. + invalidated_self: bool, + /// Whether the element's descendants were invalidated. + invalidated_descendants: bool, + /// Whether the element's siblings were invalidated. + invalidated_siblings: bool, +} + +impl InvalidationResult { + /// Create an emtpy result. + pub fn empty() -> Self { + Self { + invalidated_self: false, + invalidated_descendants: false, + invalidated_siblings: false, + } + } + + /// Whether the invalidation has invalidate the element itself. + pub fn has_invalidated_self(&self) -> bool { + self.invalidated_self + } + + /// Whether the invalidation has invalidate desendants. + pub fn has_invalidated_descendants(&self) -> bool { + self.invalidated_descendants + } + + /// Whether the invalidation has invalidate siblings. + pub fn has_invalidated_siblings(&self) -> bool { + self.invalidated_siblings + } +} + +impl<'a, 'b, 'c, E, P: 'a> TreeStyleInvalidator<'a, 'b, 'c, E, P> +where + 'b: 'a, + E: TElement, + P: InvalidationProcessor<'b, 'c, 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 + .processor + .sibling_traversal_map() + .next_sibling_for(&self.element); + let mut any_invalidated = false; + + while let Some(sibling) = current { + let mut sibling_invalidator = + TreeStyleInvalidator::new(sibling, self.stack_limit_checker, self.processor); + + let mut invalidations_for_descendants = DescendantInvalidationLists::default(); + let invalidated_sibling = sibling_invalidator.process_sibling_invalidations( + &mut invalidations_for_descendants, + sibling_invalidations, + ); + + if invalidated_sibling { + sibling_invalidator + .processor + .invalidated_sibling(sibling, self.element); + } + + any_invalidated |= invalidated_sibling; + + any_invalidated |= + sibling_invalidator.invalidate_descendants(&invalidations_for_descendants); + + if sibling_invalidations.is_empty() { + break; + } + + current = self + .processor + .sibling_traversal_map() + .next_sibling_for(&sibling); + } + + 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) => { + let invalidation_kind = p.invalidation_kind(); + match invalidation_kind { + DependencyInvalidationKind::Normal(_) => &**p, + DependencyInvalidationKind::Relative(kind) => { + self.processor.found_relative_selector_invalidation( + self.element, + kind, + &**p, + ); + return SingleInvalidationResult { + invalidated_self: false, + matched: true, + }; + }, + } + }, + }; + + 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.normal_invalidation_kind() == + NormalDependencyInvalidationKind::Element + { + continue; + } + + debug!(" > Generating invalidation"); + break Invalidation::new(cur_dependency, invalidation.scope); + } + }, + CompoundSelectorMatchingResult::Matched { + next_combinator_offset, + } => Invalidation { + dependency: invalidation.dependency, + scope: invalidation.scope, + offset: next_combinator_offset + 1, + matched_by_any_previous: false, + }, + }; + + debug_assert_ne!( + next_invalidation.offset, 0, + "Rightmost selectors shouldn't generate more invalidations", + ); + + let mut invalidated_self = false; + let next_combinator = next_invalidation + .dependency + .selector + .combinator_at_parse_order(next_invalidation.offset - 1); + + if matches!(next_combinator, Combinator::PseudoElement) && + self.processor.invalidates_on_pseudo_element() + { + // We need to invalidate the element whenever pseudos change, for + // two reasons: + // + // * Eager pseudo styles are stored as part of the originating + // element's computed style. + // + // * Lazy pseudo-styles might be cached on the originating + // element's pseudo-style cache. + // + // This could be more fine-grained (perhaps with a RESTYLE_PSEUDOS + // hint?). + // + // Note that we'll also restyle the pseudo-element because it would + // match this invalidation. + // + // FIXME: For non-element-backed pseudos this is still not quite + // correct. For example for ::selection even though we invalidate + // the style properly there's nothing that triggers a repaint + // necessarily. Though this matches old Gecko behavior, and the + // ::selection implementation needs to change significantly anyway + // to implement https://github.com/w3c/csswg-drafts/issues/2474 for + // example. + invalidated_self = true; + } + + debug!( + " > Invalidation matched, next: {:?}, ({:?})", + next_invalidation, next_combinator + ); + + let next_invalidation_kind = next_invalidation.kind(); + + // We can skip pushing under some circumstances, and we should + // because otherwise the invalidation list could grow + // exponentially. + // + // * First of all, both invalidations need to be of the same + // kind. This is because of how we propagate them going to + // the right of the tree for sibling invalidations and going + // down the tree for children invalidations. A sibling + // invalidation that ends up generating a children + // invalidation ends up (correctly) in five different lists, + // not in the same list five different times. + // + // * Then, the invalidation needs to be matched by a previous + // ancestor/sibling, in order to know that this invalidation + // has been generated already. + // + // * Finally, the new invalidation needs to be + // `effective_for_next()`, in order for us to know that it is + // still in the list, since we remove the dependencies that + // aren't from the lists for our children / siblings. + // + // To go through an example, let's imagine we are processing a + // dom subtree like: + // + //
+ // + // And an invalidation list with a single invalidation like: + // + // [div div div] + // + // When we process the invalidation list for the outer div, we + // match it, and generate a `div div` invalidation, so for the + //
child we have: + // + // [div div div, div div] + // + // With the first of them marked as `matched`. + // + // When we process the
child, we don't match any of + // them, so both invalidations go untouched to our children. + // + // When we process the second
, we match _both_ + // invalidations. + // + // However, when matching the first, we can tell it's been + // matched, and not push the corresponding `div div` + // invalidation, since we know it's necessarily already on the + // list. + // + // Thus, without skipping the push, we'll arrive to the + // innermost
with: + // + // [div div div, div div, div div, div] + // + // While skipping it, we won't arrive here with duplicating + // dependencies: + // + // [div div div, div div, div] + // + let can_skip_pushing = next_invalidation_kind == invalidation_kind && + invalidation.matched_by_any_previous && + next_invalidation.effective_for_next(); + + if can_skip_pushing { + debug!( + " > Can avoid push, since the invalidation had \ + already been matched before" + ); + } else { + match next_invalidation_kind { + InvalidationKind::Descendant(DescendantInvalidationKind::Dom) => { + descendant_invalidations + .dom_descendants + .push(next_invalidation); + }, + InvalidationKind::Descendant(DescendantInvalidationKind::Part) => { + descendant_invalidations.parts.push(next_invalidation); + }, + InvalidationKind::Descendant(DescendantInvalidationKind::Slotted) => { + descendant_invalidations + .slotted_descendants + .push(next_invalidation); + }, + InvalidationKind::Sibling => { + sibling_invalidations.push(next_invalidation); + }, + } + } + + SingleInvalidationResult { + invalidated_self, + matched: true, + } + } +} diff --git a/servo/components/style/invalidation/element/mod.rs b/servo/components/style/invalidation/element/mod.rs new file mode 100644 index 0000000000..0ddb9f1863 --- /dev/null +++ b/servo/components/style/invalidation/element/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/. */ + +//! 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 relative_selector; +pub mod restyle_hints; +pub mod state_and_attributes; diff --git a/servo/components/style/invalidation/element/relative_selector.rs b/servo/components/style/invalidation/element/relative_selector.rs new file mode 100644 index 0000000000..ccb48e349f --- /dev/null +++ b/servo/components/style/invalidation/element/relative_selector.rs @@ -0,0 +1,1164 @@ +/* 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 relative selectors. + +use crate::data::ElementData; +use crate::dom::{TElement, TNode}; +use crate::gecko_bindings::structs::ServoElementSnapshotTable; +use crate::invalidation::element::element_wrapper::ElementWrapper; +use crate::invalidation::element::invalidation_map::{ + Dependency, DependencyInvalidationKind, NormalDependencyInvalidationKind, + RelativeDependencyInvalidationKind, RelativeSelectorInvalidationMap, +}; +use crate::invalidation::element::invalidator::{ + DescendantInvalidationLists, Invalidation, InvalidationProcessor, InvalidationResult, + InvalidationVector, SiblingTraversalMap, TreeStyleInvalidator, +}; +use crate::invalidation::element::restyle_hints::RestyleHint; +use crate::invalidation::element::state_and_attributes::{ + check_dependency, dependency_may_be_relevant, invalidated_descendants, invalidated_self, + invalidated_sibling, push_invalidation, should_process_descendants, +}; +use crate::stylist::{CascadeData, Stylist}; +use dom::ElementState; +use fxhash::FxHashMap; +use selectors::matching::{ + matches_compound_selector_from, matches_selector, CompoundSelectorMatchingResult, + ElementSelectorFlags, MatchingContext, MatchingForInvalidation, MatchingMode, + NeedsSelectorFlags, QuirksMode, SelectorCaches, VisitedHandlingMode, +}; +use selectors::parser::{Combinator, SelectorKey}; +use selectors::OpaqueElement; +use smallvec::SmallVec; +use std::ops::DerefMut; + +/// Kind of DOM mutation this relative selector invalidation is being carried out in. +#[derive(Clone, Copy)] +pub enum DomMutationOperation { + /// Insertion operation, can cause side effect, but presumed already happened. + Insert, + /// Append operation, cannot cause side effect. + Append, + /// Removal operation, can cause side effect, but presumed already happened. Sibling relationships are destroyed. + Remove, + /// Invalidating for side effect of a DOM operation, for the previous sibling. + SideEffectPrevSibling, + /// Invalidating for side effect of a DOM operation, for the next sibling. + SideEffectNextSibling, +} + +impl DomMutationOperation { + fn accept(&self, d: &Dependency, e: E) -> bool { + match self { + Self::Insert | Self::Append | Self::Remove => { + e.relative_selector_search_direction().is_some() + }, + // `:has(+ .a + .b)` with `.anchor + .a + .remove + .b` - `.a` would be present + // in the search path. + Self::SideEffectPrevSibling => { + e.relative_selector_search_direction().is_some() && + d.right_combinator_is_next_sibling() + }, + // If an element is being removed and would cause next-sibling match to happen, + // e.g. `:has(+ .a)` with `.anchor + .remove + .a`, `.a` isn't yet searched + // for relative selector matching. + Self::SideEffectNextSibling => d.dependency_is_relative_with_single_next_sibling(), + } + } + + fn is_side_effect(&self) -> bool { + match self { + Self::Insert | Self::Append | Self::Remove => false, + Self::SideEffectPrevSibling | Self::SideEffectNextSibling => true, + } + } +} + +/// Context required to try and optimize away relative dependencies. +struct OptimizationContext<'a, E: TElement> { + sibling_traversal_map: &'a SiblingTraversalMap, + quirks_mode: QuirksMode, + operation: DomMutationOperation, +} + +impl<'a, E: TElement> OptimizationContext<'a, E> { + fn can_be_ignored( + &self, + is_subtree: bool, + element: E, + host: Option, + dependency: &Dependency, + ) -> bool { + if is_subtree { + // Subtree elements don't have unaffected sibling to look at. + return false; + } + debug_assert!( + matches!( + dependency.invalidation_kind(), + DependencyInvalidationKind::Relative(..) + ), + "Non-relative selector being evaluated for optimization" + ); + // This optimization predecates on the fact that there may be a sibling that can readily + // "take over" this element. + let sibling = match self.sibling_traversal_map.prev_sibling_for(&element) { + None => { + if matches!(self.operation, DomMutationOperation::Append) { + return false; + } + match self.sibling_traversal_map.next_sibling_for(&element) { + Some(s) => s, + None => return false, + } + }, + Some(s) => s, + }; + { + // Run through the affected compund. + let mut iter = dependency.selector.iter_from(dependency.selector_offset); + while let Some(c) = iter.next() { + if c.has_indexed_selector_in_subject() { + // We do not calculate indices during invalidation as they're wasteful - as a side effect, + // such selectors always return true, breaking this optimization. Note that we only check + // this compound only because the check to skip compares against this element's sibling. + // i.e. Given `:has(:nth-child(2) .foo)`, we'd try to find `.foo`'s sibling, which + // shares `:nth-child` up the selector. + return false; + } + } + } + let is_rightmost = dependency.selector_offset == 0; + if !is_rightmost { + let combinator = dependency + .selector + .combinator_at_match_order(dependency.selector_offset - 1); + if combinator.is_ancestor() { + // We can safely ignore these, since we're about to traverse the + // rest of the affected tree anyway to find the rightmost invalidated element. + return true; + } + if combinator.is_sibling() && matches!(self.operation, DomMutationOperation::Append) { + // If we're in the subtree, same argument applies as ancestor combinator case. + // If we're at the top of the DOM tree being mutated, we can ignore it if the + // operation is append - we know we'll cover all the later siblings and their descendants. + return true; + } + } + let mut caches = SelectorCaches::default(); + let mut matching_context = MatchingContext::new( + MatchingMode::Normal, + None, + &mut caches, + self.quirks_mode, + NeedsSelectorFlags::No, + MatchingForInvalidation::Yes, + ); + matching_context.current_host = host; + let sibling_matches = matches_selector( + &dependency.selector, + dependency.selector_offset, + None, + &sibling, + &mut matching_context, + ); + if sibling_matches { + // Remember that at this point, we know that the combinator to the right of this + // compound is a sibling combinator. Effectively, we've found a standin for the + // element we're mutating. + // e.g. Given `:has(... .a ~ .b ...)`, we're the mutating element matching `... .a`, + // if we find a sibling that matches the `... .a`, it can stand in for us. + debug_assert!(dependency.parent.is_some(), "No relative selector outer dependency?"); + return dependency.parent.as_ref().map_or(false, |par| { + // ... However, if the standin sibling can be the anchor, we can't skip it, since + // that sibling should be invlidated to become the anchor. + !matches_selector( + &par.selector, + par.selector_offset, + None, + &sibling, + &mut matching_context + ) + }); + } + // Ok, there's no standin element - but would this element have matched the upstream + // selector anyway? If we don't, either the match exists somewhere far from us + // (In which case our mutation doesn't really matter), or it doesn't exist at all, + // so we can just skip the invalidation. + let (combinator, prev_offset) = { + let mut iter = dependency.selector.iter_from(dependency.selector_offset); + let mut o = dependency.selector_offset; + while iter.next().is_some() { + o += 1; + } + let combinator = iter.next_sequence(); + o += 1; + debug_assert!( + combinator.is_some(), + "Should at least see a relative combinator" + ); + (combinator.unwrap(), o) + }; + if combinator.is_sibling() { + if prev_offset >= dependency.selector.len() - 1 { + // Hit the relative combinator - we don't have enough information to + // see if there's going to be a downstream match. + return false; + } + if matches!(self.operation, DomMutationOperation::Remove) { + // This is sad :( The sibling relation of a removed element is lost, and we don't + // propagate sibling traversal map to selector matching context, so we need to do + // manual matching here. TODO(dshin): Worth changing selector matching for this? + + // Try matching this compound, then... + // Note: We'll not hit the leftmost sequence (Since we would have returned early + // if we'd hit the relative selector anchor). + if matches!( + matches_compound_selector_from( + &dependency.selector, + dependency.selector.len() - prev_offset + 1, + &mut matching_context, + &element + ), + CompoundSelectorMatchingResult::NotMatched + ) { + return true; + } + + // ... Match the rest of the selector, manually traversing. + let mut prev_sibling = self.sibling_traversal_map.prev_sibling_for(&element); + while let Some(sib) = prev_sibling { + if matches_selector( + &dependency.selector, + prev_offset, + None, + &sib, + &mut matching_context, + ) { + return false; + } + if matches!(combinator, Combinator::NextSibling) { + break; + } + prev_sibling = self.sibling_traversal_map.prev_sibling_for(&sib); + } + return true; + } + } + !matches_selector( + &dependency.selector, + dependency.selector_offset, + None, + &element, + &mut matching_context, + ) + } +} + +/// Overall invalidator for handling relative selector invalidations. +pub struct RelativeSelectorInvalidator<'a, 'b, E> +where + E: TElement + 'a, +{ + /// Element triggering the invalidation. + pub element: E, + /// Quirks mode of the current invalidation. + pub quirks_mode: QuirksMode, + /// Snapshot containing changes to invalidate against. + /// Can be None if it's a DOM mutation. + pub snapshot_table: Option<&'b ServoElementSnapshotTable>, + /// Callback to trigger when the subject element is invalidated. + pub invalidated: fn(E, &InvalidationResult), + /// The traversal map that should be used to process invalidations. + pub sibling_traversal_map: SiblingTraversalMap, + /// Marker for 'a lifetime. + pub _marker: ::std::marker::PhantomData<&'a ()>, +} + +struct RelativeSelectorInvalidation<'a> { + host: Option, + kind: RelativeDependencyInvalidationKind, + dependency: &'a Dependency, +} + +type ElementDependencies<'a> = SmallVec<[(Option, &'a Dependency); 1]>; +type Dependencies<'a, E> = SmallVec<[(E, ElementDependencies<'a>); 1]>; +type AlreadyInvalidated<'a, E> = SmallVec<[(E, Option, &'a Dependency); 2]>; + +/// Interface for collecting relative selector dependencies. +pub struct RelativeSelectorDependencyCollector<'a, E> +where + E: TElement, +{ + /// Dependencies that need to run through the normal invalidation that may generate + /// a relative selector invalidation. + dependencies: FxHashMap>, + /// Dependencies that created an invalidation right away. + invalidations: AlreadyInvalidated<'a, E>, + /// The top element in the subtree being invalidated. + top: E, + /// Optional context that will be used to try and skip invalidations + /// by running selector matches. + optimization_context: Option>, +} + +type Invalidations<'a> = SmallVec<[RelativeSelectorInvalidation<'a>; 1]>; +type InnerInvalidations<'a, E> = SmallVec<[(E, RelativeSelectorInvalidation<'a>); 1]>; + +struct ToInvalidate<'a, E: TElement + 'a> { + /// Dependencies to run through normal invalidator. + dependencies: Dependencies<'a, E>, + /// Dependencies already invalidated. + invalidations: Invalidations<'a>, +} + +impl<'a, E: TElement + 'a> Default for ToInvalidate<'a, E> { + fn default() -> Self { + Self { + dependencies: Dependencies::default(), + invalidations: Invalidations::default(), + } + } +} + +fn dependency_selectors_match(a: &Dependency, b: &Dependency) -> bool { + if a.invalidation_kind() != b.invalidation_kind() { + return false; + } + if SelectorKey::new(&a.selector) != SelectorKey::new(&b.selector) { + return false; + } + let mut a_parent = a.parent.as_ref(); + let mut b_parent = b.parent.as_ref(); + while let (Some(a_p), Some(b_p)) = (a_parent, b_parent) { + if SelectorKey::new(&a_p.selector) != SelectorKey::new(&b_p.selector) { + return false; + } + a_parent = a_p.parent.as_ref(); + b_parent = b_p.parent.as_ref(); + } + a_parent.is_none() && b_parent.is_none() +} + +impl<'a, E> RelativeSelectorDependencyCollector<'a, E> +where + E: TElement, +{ + fn new(top: E, optimization_context: Option>) -> Self { + Self { + dependencies: FxHashMap::default(), + invalidations: AlreadyInvalidated::default(), + top, + optimization_context, + } + } + + fn insert_invalidation( + &mut self, + element: E, + dependency: &'a Dependency, + host: Option, + ) { + match self + .invalidations + .iter_mut() + .find(|(_, _, d)| dependency_selectors_match(dependency, d)) + { + Some((e, h, d)) => { + // Just keep one. + if d.selector_offset > dependency.selector_offset { + (*e, *h, *d) = (element, host, dependency); + } + }, + None => { + self.invalidations.push((element, host, dependency)); + }, + } + } + + /// Add this dependency, if it is unique (i.e. Different outer dependency or same outer dependency + /// but requires a different invalidation traversal). + pub fn add_dependency( + &mut self, + dependency: &'a Dependency, + element: E, + host: Option, + ) { + match dependency.invalidation_kind() { + DependencyInvalidationKind::Normal(..) => { + self.dependencies + .entry(element) + .and_modify(|v| v.push((host, dependency))) + .or_default() + .push((host, dependency)); + }, + DependencyInvalidationKind::Relative(kind) => { + debug_assert!( + dependency.parent.is_some(), + "Orphaned inner relative selector?" + ); + if element != self.top && + matches!( + kind, + RelativeDependencyInvalidationKind::Parent | + RelativeDependencyInvalidationKind::PrevSibling | + RelativeDependencyInvalidationKind::EarlierSibling + ) + { + return; + } + self.insert_invalidation(element, dependency, host); + }, + }; + } + + /// Get the dependencies in a list format. + fn get(self) -> ToInvalidate<'a, E> { + let mut result = ToInvalidate::default(); + for (element, host, dependency) in self.invalidations { + match dependency.invalidation_kind() { + DependencyInvalidationKind::Normal(_) => { + unreachable!("Inner selector in invalidation?") + }, + DependencyInvalidationKind::Relative(kind) => { + if let Some(context) = self.optimization_context.as_ref() { + if context.can_be_ignored(element != self.top, element, host, dependency) { + continue; + } + } + let dependency = dependency.parent.as_ref().unwrap(); + result.invalidations.push(RelativeSelectorInvalidation { + kind, + host, + dependency, + }); + // We move the invalidation up to the top of the subtree to avoid unnecessary traveral, but + // this means that we need to take ancestor-earlier sibling invalidations into account, as + // they'd look into earlier siblings of the top of the subtree as well. + if element != self.top && + matches!( + kind, + RelativeDependencyInvalidationKind::AncestorEarlierSibling | + RelativeDependencyInvalidationKind::AncestorPrevSibling + ) + { + result.invalidations.push(RelativeSelectorInvalidation { + kind: if matches!( + kind, + RelativeDependencyInvalidationKind::AncestorPrevSibling + ) { + RelativeDependencyInvalidationKind::PrevSibling + } else { + RelativeDependencyInvalidationKind::EarlierSibling + }, + host, + dependency, + }); + } + }, + }; + } + for (key, element_dependencies) in self.dependencies { + // At least for now, we don't try to optimize away dependencies emitted from nested selectors. + result.dependencies.push((key, element_dependencies)); + } + result + } + + fn collect_all_dependencies_for_element( + &mut self, + element: E, + scope: Option, + quirks_mode: QuirksMode, + map: &'a RelativeSelectorInvalidationMap, + operation: DomMutationOperation, + ) { + element + .id() + .map(|v| match map.map.id_to_selector.get(v, quirks_mode) { + Some(v) => { + for dependency in v { + if !operation.accept(dependency, element) { + continue; + } + self.add_dependency(dependency, element, scope); + } + }, + None => (), + }); + element.each_class(|v| match map.map.class_to_selector.get(v, quirks_mode) { + Some(v) => { + for dependency in v { + if !operation.accept(dependency, element) { + continue; + } + self.add_dependency(dependency, element, scope); + } + }, + None => (), + }); + element.each_attr_name( + |v| match map.map.other_attribute_affecting_selectors.get(v) { + Some(v) => { + for dependency in v { + if !operation.accept(dependency, element) { + continue; + } + self.add_dependency(dependency, element, scope); + } + }, + None => (), + }, + ); + let state = element.state(); + map.map.state_affecting_selectors.lookup_with_additional( + element, + quirks_mode, + None, + &[], + ElementState::empty(), + |dependency| { + if !dependency.state.intersects(state) { + return true; + } + if !operation.accept(&dependency.dep, element) { + return true; + } + self.add_dependency(&dependency.dep, element, scope); + true + }, + ); + + if let Some(v) = map.type_to_selector.get(element.local_name()) { + for dependency in v { + if !operation.accept(dependency, element) { + continue; + } + self.add_dependency(dependency, element, scope); + } + } + + for dependency in &map.any_to_selector { + if !operation.accept(dependency, element) { + continue; + } + self.add_dependency(dependency, element, scope); + } + } + + fn is_empty(&self) -> bool { + self.invalidations.is_empty() && self.dependencies.is_empty() + } +} + +impl<'a, 'b, E> RelativeSelectorInvalidator<'a, 'b, E> +where + E: TElement + 'a, +{ + /// Gather relative selector dependencies for the given element, and invalidate as necessary. + #[inline(never)] + pub fn invalidate_relative_selectors_for_this( + self, + stylist: &'a Stylist, + mut gather_dependencies: F, + ) where + F: FnMut( + &E, + Option, + &'a CascadeData, + QuirksMode, + &mut RelativeSelectorDependencyCollector<'a, E>, + ), + { + let mut collector = RelativeSelectorDependencyCollector::new(self.element, None); + stylist.for_each_cascade_data_with_scope(self.element, |data, scope| { + let map = data.relative_selector_invalidation_map(); + if !map.used { + return; + } + gather_dependencies( + &self.element, + scope.map(|e| e.opaque()), + data, + self.quirks_mode, + &mut collector, + ); + }); + if collector.is_empty() { + return; + } + self.invalidate_from_dependencies(collector.get()); + } + + /// Gather relative selector dependencies for the given element (And its subtree) that mutated, and invalidate as necessary. + #[inline(never)] + pub fn invalidate_relative_selectors_for_dom_mutation( + self, + subtree: bool, + stylist: &'a Stylist, + inherited_search_path: ElementSelectorFlags, + operation: DomMutationOperation, + ) { + let mut collector = RelativeSelectorDependencyCollector::new( + self.element, + if operation.is_side_effect() { + None + } else { + Some(OptimizationContext { + sibling_traversal_map: &self.sibling_traversal_map, + quirks_mode: self.quirks_mode, + operation, + }) + }, + ); + let mut traverse_subtree = false; + self.element.apply_selector_flags(inherited_search_path); + stylist.for_each_cascade_data_with_scope(self.element, |data, scope| { + let map = data.relative_selector_invalidation_map(); + if !map.used { + return; + } + traverse_subtree |= map.needs_ancestors_traversal; + collector.collect_all_dependencies_for_element( + self.element, + scope.map(|e| e.opaque()), + self.quirks_mode, + map, + operation, + ); + }); + + if subtree && traverse_subtree { + for node in self.element.as_node().dom_descendants() { + let descendant = match node.as_element() { + Some(e) => e, + None => continue, + }; + descendant.apply_selector_flags(inherited_search_path); + stylist.for_each_cascade_data_with_scope(descendant, |data, scope| { + let map = data.relative_selector_invalidation_map(); + if !map.used { + return; + } + collector.collect_all_dependencies_for_element( + descendant, + scope.map(|e| e.opaque()), + self.quirks_mode, + map, + operation, + ); + }); + } + } + if collector.is_empty() { + return; + } + self.invalidate_from_dependencies(collector.get()); + } + + /// Carry out complete invalidation triggered by a relative selector invalidation. + fn invalidate_from_dependencies(&self, to_invalidate: ToInvalidate<'a, E>) { + for (element, dependencies) in to_invalidate.dependencies { + let mut selector_caches = SelectorCaches::default(); + let mut processor = RelativeSelectorInnerInvalidationProcessor::new( + self.quirks_mode, + self.snapshot_table, + &dependencies, + &mut selector_caches, + &self.sibling_traversal_map, + ); + TreeStyleInvalidator::new(element, None, &mut processor).invalidate(); + for (element, invalidation) in processor.take_invalidations() { + self.invalidate_upwards(element, &invalidation); + } + } + for invalidation in to_invalidate.invalidations { + self.invalidate_upwards(self.element, &invalidation); + } + } + + fn invalidate_upwards(&self, element: E, invalidation: &RelativeSelectorInvalidation<'a>) { + // This contains the main reason for why relative selector invalidation is handled + // separately - It travels ancestor and/or earlier sibling direction. + match invalidation.kind { + RelativeDependencyInvalidationKind::Parent => { + element.parent_element().map(|e| { + if !Self::in_search_direction( + &e, + ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR, + ) { + return; + } + self.handle_anchor(e, invalidation.dependency, invalidation.host); + }); + }, + RelativeDependencyInvalidationKind::Ancestors => { + let mut parent = element.parent_element(); + while let Some(par) = parent { + if !Self::in_search_direction( + &par, + ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR, + ) { + return; + } + self.handle_anchor(par, invalidation.dependency, invalidation.host); + parent = par.parent_element(); + } + }, + RelativeDependencyInvalidationKind::PrevSibling => { + self.sibling_traversal_map + .prev_sibling_for(&element) + .map(|e| { + if !Self::in_search_direction( + &e, + ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_SIBLING, + ) { + return; + } + self.handle_anchor(e, invalidation.dependency, invalidation.host); + }); + }, + RelativeDependencyInvalidationKind::AncestorPrevSibling => { + let mut parent = element.parent_element(); + while let Some(par) = parent { + if !Self::in_search_direction( + &par, + ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR, + ) { + return; + } + par.prev_sibling_element().map(|e| { + if !Self::in_search_direction( + &e, + ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_SIBLING, + ) { + return; + } + self.handle_anchor(e, invalidation.dependency, invalidation.host); + }); + parent = par.parent_element(); + } + }, + RelativeDependencyInvalidationKind::EarlierSibling => { + let mut sibling = self.sibling_traversal_map.prev_sibling_for(&element); + while let Some(sib) = sibling { + if !Self::in_search_direction( + &sib, + ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_SIBLING, + ) { + return; + } + self.handle_anchor(sib, invalidation.dependency, invalidation.host); + sibling = sib.prev_sibling_element(); + } + }, + RelativeDependencyInvalidationKind::AncestorEarlierSibling => { + let mut parent = element.parent_element(); + while let Some(par) = parent { + if !Self::in_search_direction( + &par, + ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR, + ) { + return; + } + let mut sibling = par.prev_sibling_element(); + while let Some(sib) = sibling { + if !Self::in_search_direction( + &sib, + ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_SIBLING, + ) { + return; + } + self.handle_anchor(sib, invalidation.dependency, invalidation.host); + sibling = sib.prev_sibling_element(); + } + parent = par.parent_element(); + } + }, + } + } + + /// Is this element in the direction of the given relative selector search path? + fn in_search_direction(element: &E, desired: ElementSelectorFlags) -> bool { + if let Some(direction) = element.relative_selector_search_direction() { + direction.intersects(desired) + } else { + false + } + } + + /// Handle a potential relative selector anchor. + fn handle_anchor( + &self, + element: E, + outer_dependency: &Dependency, + host: Option, + ) { + let is_rightmost = Self::is_subject(outer_dependency); + if (is_rightmost && + !element.has_selector_flags(ElementSelectorFlags::ANCHORS_RELATIVE_SELECTOR)) || + (!is_rightmost && + !element.has_selector_flags( + ElementSelectorFlags::ANCHORS_RELATIVE_SELECTOR_NON_SUBJECT, + )) + { + // If it was never a relative selector anchor, don't bother. + return; + } + let mut selector_caches = SelectorCaches::default(); + let matching_context = MatchingContext::<'_, E::Impl>::new_for_visited( + MatchingMode::Normal, + None, + &mut selector_caches, + VisitedHandlingMode::AllLinksVisitedAndUnvisited, + self.quirks_mode, + NeedsSelectorFlags::No, + MatchingForInvalidation::Yes, + ); + let mut data = match element.mutate_data() { + Some(data) => data, + None => return, + }; + let mut processor = RelativeSelectorOuterInvalidationProcessor { + element, + host, + data: data.deref_mut(), + dependency: &*outer_dependency, + matching_context, + traversal_map: &self.sibling_traversal_map, + }; + let result = TreeStyleInvalidator::new(element, None, &mut processor).invalidate(); + (self.invalidated)(element, &result); + } + + /// Does this relative selector dependency have its relative selector in the subject position? + fn is_subject(outer_dependency: &Dependency) -> bool { + debug_assert!( + matches!( + outer_dependency.invalidation_kind(), + DependencyInvalidationKind::Normal(_) + ), + "Outer selector of relative selector is relative?" + ); + + if let Some(p) = outer_dependency.parent.as_ref() { + if !Self::is_subject(p.as_ref()) { + // Not subject in outer selector. + return false; + } + } + outer_dependency.selector.is_rightmost(outer_dependency.selector_offset) + } +} + +/// Blindly invalidate everything outside of a relative selector. +/// Consider `:is(.a :has(.b) .c ~ .d) ~ .e .f`, where .b gets deleted. +/// Since the tree mutated, we cannot rely on snapshots. +pub struct RelativeSelectorOuterInvalidationProcessor<'a, 'b, E: TElement> { + /// Element being invalidated. + pub element: E, + /// The current shadow host, if any. + pub host: Option, + /// Data for the element being invalidated. + pub data: &'a mut ElementData, + /// Dependency to be processed. + pub dependency: &'b Dependency, + /// Matching context to use for invalidation. + pub matching_context: MatchingContext<'a, E::Impl>, + /// Traversal map for this invalidation. + pub traversal_map: &'a SiblingTraversalMap, +} + +impl<'a, 'b: 'a, E: 'a> InvalidationProcessor<'b, 'a, E> + for RelativeSelectorOuterInvalidationProcessor<'a, 'b, E> +where + E: TElement, +{ + fn invalidates_on_pseudo_element(&self) -> bool { + true + } + + fn check_outer_dependency(&mut self, _dependency: &Dependency, _element: E) -> bool { + // At this point, we know a relative selector invalidated, and are ignoring them. + true + } + + fn matching_context(&mut self) -> &mut MatchingContext<'a, E::Impl> { + &mut self.matching_context + } + + fn sibling_traversal_map(&self) -> &SiblingTraversalMap { + self.traversal_map + } + + fn collect_invalidations( + &mut self, + element: E, + _self_invalidations: &mut InvalidationVector<'b>, + descendant_invalidations: &mut DescendantInvalidationLists<'b>, + sibling_invalidations: &mut InvalidationVector<'b>, + ) -> bool { + debug_assert_eq!(element, self.element); + debug_assert!( + self.matching_context.matching_for_invalidation(), + "Not matching for invalidation?" + ); + + // Ok, this element can potentially an anchor to the given dependency. + // Before we do the potentially-costly ancestor/earlier sibling traversal, + // See if it can actuall be an anchor by trying to match the "rest" of the selector + // outside and to the left of `:has` in question. + // e.g. Element under consideration can only be the anchor to `:has` in + // `.foo .bar ~ .baz:has()`, iff it matches `.foo .bar ~ .baz`. + let invalidated_self = { + let mut d = self.dependency; + loop { + debug_assert!( + matches!( + d.invalidation_kind(), + DependencyInvalidationKind::Normal(_) + ), + "Unexpected outer relative dependency" + ); + if !dependency_may_be_relevant(d, &element, false) { + break false; + } + if !matches_selector( + &d.selector, + d.selector_offset, + None, + &element, + self.matching_context(), + ) { + break false; + } + let invalidation_kind = d.normal_invalidation_kind(); + if matches!(invalidation_kind, NormalDependencyInvalidationKind::Element) { + if let Some(ref parent) = d.parent { + d = parent; + continue; + } + break true; + } + debug_assert_ne!(d.selector_offset, 0); + debug_assert_ne!(d.selector_offset, d.selector.len()); + let invalidation = Invalidation::new(&d, self.host); + break push_invalidation( + invalidation, + invalidation_kind, + descendant_invalidations, + sibling_invalidations + ); + } + }; + + 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) { + unreachable!("Unexpected recursion limit"); + } + + fn invalidated_descendants(&mut self, element: E, child: E) { + invalidated_descendants(element, child) + } + + fn invalidated_self(&mut self, element: E) { + debug_assert_ne!(element, self.element); + invalidated_self(element); + } + + fn invalidated_sibling(&mut self, element: E, of: E) { + debug_assert_ne!(element, self.element); + invalidated_sibling(element, of); + } +} + +/// Invalidation for the selector(s) inside a relative selector. +pub struct RelativeSelectorInnerInvalidationProcessor<'a, 'b, 'c, E> +where + E: TElement + 'a, +{ + /// Matching context to be used. + matching_context: MatchingContext<'b, E::Impl>, + /// Table of snapshots. + snapshot_table: Option<&'c ServoElementSnapshotTable>, + /// Incoming dependencies to be processed. + dependencies: &'c ElementDependencies<'a>, + /// Generated invalidations. + invalidations: InnerInvalidations<'a, E>, + /// Traversal map for this invalidation. + traversal_map: &'b SiblingTraversalMap, +} + +impl<'a, 'b, 'c, E> RelativeSelectorInnerInvalidationProcessor<'a, 'b, 'c, E> +where + E: TElement + 'a, +{ + fn new( + quirks_mode: QuirksMode, + snapshot_table: Option<&'c ServoElementSnapshotTable>, + dependencies: &'c ElementDependencies<'a>, + selector_caches: &'b mut SelectorCaches, + traversal_map: &'b SiblingTraversalMap, + ) -> Self { + let matching_context = MatchingContext::new_for_visited( + MatchingMode::Normal, + None, + selector_caches, + VisitedHandlingMode::AllLinksVisitedAndUnvisited, + quirks_mode, + NeedsSelectorFlags::No, + MatchingForInvalidation::Yes, + ); + Self { + matching_context, + snapshot_table, + dependencies, + invalidations: InnerInvalidations::default(), + traversal_map, + } + } + + fn note_dependency( + &mut self, + element: E, + scope: Option, + dependency: &'a Dependency, + descendant_invalidations: &mut DescendantInvalidationLists<'a>, + sibling_invalidations: &mut InvalidationVector<'a>, + ) { + match dependency.invalidation_kind() { + DependencyInvalidationKind::Normal(_) => (), + DependencyInvalidationKind::Relative(kind) => { + self.found_relative_selector_invalidation(element, kind, dependency); + return; + }, + } + if matches!( + dependency.normal_invalidation_kind(), + NormalDependencyInvalidationKind::Element + ) { + // Ok, keep heading outwards. + debug_assert!( + dependency.parent.is_some(), + "Orphaned inner selector dependency?" + ); + if let Some(parent) = dependency.parent.as_ref() { + self.note_dependency( + element, + scope, + parent, + descendant_invalidations, + sibling_invalidations, + ); + } + return; + } + let invalidation = Invalidation::new(&dependency, scope); + match dependency.normal_invalidation_kind() { + NormalDependencyInvalidationKind::Descendants => { + // Descendant invalidations are simplified due to pseudo-elements not being available within the relative selector. + descendant_invalidations.dom_descendants.push(invalidation) + }, + NormalDependencyInvalidationKind::Siblings => sibling_invalidations.push(invalidation), + _ => unreachable!(), + } + } + + /// Take the generated invalidations. + fn take_invalidations(self) -> InnerInvalidations<'a, E> { + self.invalidations + } +} + +impl<'a, 'b, 'c, E> InvalidationProcessor<'a, 'b, E> + for RelativeSelectorInnerInvalidationProcessor<'a, 'b, 'c, E> +where + E: TElement + 'a, +{ + fn check_outer_dependency(&mut self, dependency: &Dependency, element: E) -> bool { + if let Some(snapshot_table) = self.snapshot_table { + let wrapper = ElementWrapper::new(element, snapshot_table); + return check_dependency(dependency, &element, &wrapper, &mut self.matching_context); + } + // Just invalidate if we don't have a snapshot. + true + } + + fn matching_context(&mut self) -> &mut MatchingContext<'b, E::Impl> { + return &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 { + for (scope, dependency) in self.dependencies { + self.note_dependency( + element, + *scope, + dependency, + descendant_invalidations, + sibling_invalidations, + ) + } + false + } + + fn should_process_descendants(&mut self, _element: E) -> bool { + true + } + + fn recursion_limit_exceeded(&mut self, _element: E) { + unreachable!("Unexpected recursion limit"); + } + + // Don't do anything for normal invalidations. + fn invalidated_self(&mut self, _element: E) {} + fn invalidated_sibling(&mut self, _sibling: E, _of: E) {} + fn invalidated_descendants(&mut self, _element: E, _child: E) {} + + fn found_relative_selector_invalidation( + &mut self, + element: E, + kind: RelativeDependencyInvalidationKind, + dep: &'a Dependency, + ) { + debug_assert!(dep.parent.is_some(), "Orphaned inners selector?"); + if element.relative_selector_search_direction().is_none() { + return; + } + self.invalidations.push(( + element, + RelativeSelectorInvalidation { + host: self.matching_context.current_host, + kind, + dependency: dep.parent.as_ref().unwrap(), + }, + )); + } + + fn sibling_traversal_map(&self) -> &SiblingTraversalMap { + &self.traversal_map + } +} diff --git a/servo/components/style/invalidation/element/restyle_hints.rs b/servo/components/style/invalidation/element/restyle_hints.rs new file mode 100644 index 0000000000..fe89636e88 --- /dev/null +++ b/servo/components/style/invalidation/element/restyle_hints.rs @@ -0,0 +1,191 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Restyle hints: an optimization to avoid unnecessarily matching selectors. + +use crate::traversal_flags::TraversalFlags; + +bitflags! { + /// The kind of restyle we need to do for a given element. + #[repr(C)] + #[derive(Clone, Copy, Debug)] + pub struct RestyleHint: u16 { + /// Do a selector match of the element. + const RESTYLE_SELF = 1 << 0; + + /// Do a selector match of the element's pseudo-elements. Always to be combined with + /// RESTYLE_SELF. + const RESTYLE_PSEUDOS = 1 << 1; + + /// Do a selector match if the element is a pseudo-element. + const RESTYLE_SELF_IF_PSEUDO = 1 << 2; + + /// Do a selector match of the element's descendants. + const RESTYLE_DESCENDANTS = 1 << 3; + + /// Recascade the current element. + const RECASCADE_SELF = 1 << 4; + + /// Recascade the current element if it inherits any reset style. + const RECASCADE_SELF_IF_INHERIT_RESET_STYLE = 1 << 5; + + /// Recascade all descendant elements. + const RECASCADE_DESCENDANTS = 1 << 6; + + /// Replace the style data coming from CSS transitions without updating + /// any other style data. This hint is only processed in animation-only + /// traversal which is prior to normal traversal. + const RESTYLE_CSS_TRANSITIONS = 1 << 7; + + /// Replace the style data coming from CSS animations without updating + /// any other style data. This hint is only processed in animation-only + /// traversal which is prior to normal traversal. + const RESTYLE_CSS_ANIMATIONS = 1 << 8; + + /// Don't re-run selector-matching on the element, only the style + /// attribute has changed, and this change didn't have any other + /// dependencies. + const RESTYLE_STYLE_ATTRIBUTE = 1 << 9; + + /// Replace the style data coming from SMIL animations without updating + /// any other style data. This hint is only processed in animation-only + /// traversal which is prior to normal traversal. + const RESTYLE_SMIL = 1 << 10; + } +} + +impl RestyleHint { + /// Creates a new `RestyleHint` indicating that the current element and all + /// its descendants must be fully restyled. + pub fn restyle_subtree() -> Self { + RestyleHint::RESTYLE_SELF | RestyleHint::RESTYLE_DESCENDANTS + } + + /// Creates a new `RestyleHint` indicating that the current element and all + /// its descendants must be recascaded. + pub fn recascade_subtree() -> Self { + RestyleHint::RECASCADE_SELF | RestyleHint::RECASCADE_DESCENDANTS + } + + /// Returns whether this hint invalidates the element and all its + /// descendants. + pub fn contains_subtree(&self) -> bool { + self.contains(Self::restyle_subtree()) + } + + /// Returns whether we'll recascade all of the descendants. + pub fn will_recascade_subtree(&self) -> bool { + self.contains_subtree() || self.contains(Self::recascade_subtree()) + } + + /// Returns whether we need to restyle this element. + pub fn has_non_animation_invalidations(&self) -> bool { + !(*self & !Self::for_animations()).is_empty() + } + + /// Propagates this restyle hint to a child element. + pub fn propagate(&mut self, traversal_flags: &TraversalFlags) -> Self { + use std::mem; + + // In the middle of an animation only restyle, we don't need to + // propagate any restyle hints, and we need to remove ourselves. + if traversal_flags.for_animation_only() { + self.remove_animation_hints(); + return Self::empty(); + } + + debug_assert!( + !self.has_animation_hint(), + "There should not be any animation restyle hints \ + during normal traversal" + ); + + // Else we should clear ourselves, and return the propagated hint. + mem::replace(self, Self::empty()).propagate_for_non_animation_restyle() + } + + /// Returns a new `RestyleHint` appropriate for children of the current element. + fn propagate_for_non_animation_restyle(&self) -> Self { + if self.contains(RestyleHint::RESTYLE_DESCENDANTS) { + return Self::restyle_subtree(); + } + let mut result = Self::empty(); + if self.contains(RestyleHint::RESTYLE_PSEUDOS) { + result |= Self::RESTYLE_SELF_IF_PSEUDO; + } + if self.contains(RestyleHint::RECASCADE_DESCENDANTS) { + result |= Self::recascade_subtree(); + } + result + } + + /// Returns a hint that contains all the replacement hints. + pub fn replacements() -> Self { + RestyleHint::RESTYLE_STYLE_ATTRIBUTE | Self::for_animations() + } + + /// The replacements for the animation cascade levels. + #[inline] + pub fn for_animations() -> Self { + RestyleHint::RESTYLE_SMIL | + RestyleHint::RESTYLE_CSS_ANIMATIONS | + RestyleHint::RESTYLE_CSS_TRANSITIONS + } + + /// Returns whether the hint specifies that an animation cascade level must + /// be replaced. + #[inline] + pub fn has_animation_hint(&self) -> bool { + self.intersects(Self::for_animations()) + } + + /// Returns whether the hint specifies that an animation cascade level must + /// be replaced. + #[inline] + pub fn has_animation_hint_or_recascade(&self) -> bool { + self.intersects( + Self::for_animations() | + Self::RECASCADE_SELF | + Self::RECASCADE_SELF_IF_INHERIT_RESET_STYLE, + ) + } + + /// Returns whether the hint specifies some restyle work other than an + /// animation cascade level replacement. + #[inline] + pub fn has_non_animation_hint(&self) -> bool { + !(*self & !Self::for_animations()).is_empty() + } + + /// Returns whether the hint specifies that some cascade levels must be + /// replaced. + #[inline] + pub fn has_replacements(&self) -> bool { + self.intersects(Self::replacements()) + } + + /// Removes all of the animation-related hints. + #[inline] + pub fn remove_animation_hints(&mut self) { + self.remove(Self::for_animations()); + + // While RECASCADE_SELF is not animation-specific, we only ever add and process it during + // traversal. If we are here, removing animation hints, then we are in an animation-only + // traversal, and we know that any RECASCADE_SELF flag must have been set due to changes in + // inherited values after restyling for animations, and thus we want to remove it so that + // we don't later try to restyle the element during a normal restyle. + // (We could have separate RECASCADE_SELF_NORMAL and RECASCADE_SELF_ANIMATIONS flags to + // make it clear, but this isn't currently necessary.) + self.remove(Self::RECASCADE_SELF | Self::RECASCADE_SELF_IF_INHERIT_RESET_STYLE); + } +} + +impl Default for RestyleHint { + fn default() -> Self { + Self::empty() + } +} + +#[cfg(feature = "servo")] +malloc_size_of_is_0!(RestyleHint); diff --git a/servo/components/style/invalidation/element/state_and_attributes.rs b/servo/components/style/invalidation/element/state_and_attributes.rs new file mode 100644 index 0000000000..1c58cddf1e --- /dev/null +++ b/servo/components/style/invalidation/element/state_and_attributes.rs @@ -0,0 +1,601 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! An invalidation processor for style changes due to state and attribute +//! changes. + +use crate::context::SharedStyleContext; +use crate::data::ElementData; +use crate::dom::{TElement, TNode}; +use crate::invalidation::element::element_wrapper::{ElementSnapshot, ElementWrapper}; +use crate::invalidation::element::invalidation_map::*; +use crate::invalidation::element::invalidator::{ + DescendantInvalidationLists, InvalidationVector, SiblingTraversalMap, +}; +use crate::invalidation::element::invalidator::{Invalidation, InvalidationProcessor}; +use crate::invalidation::element::restyle_hints::RestyleHint; +use crate::selector_map::SelectorMap; +use crate::selector_parser::Snapshot; +use crate::stylesheets::origin::OriginSet; +use crate::{Atom, WeakAtom}; +use dom::ElementState; +use selectors::attr::CaseSensitivity; +use selectors::matching::{ + matches_selector, MatchingContext, MatchingForInvalidation, MatchingMode, NeedsSelectorFlags, + SelectorCaches, VisitedHandlingMode, +}; +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>, + traversal_map: SiblingTraversalMap, +} + +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, + selector_caches: &'a mut SelectorCaches, + ) -> Self { + let matching_context = MatchingContext::new_for_visited( + MatchingMode::Normal, + None, + selector_caches, + VisitedHandlingMode::AllLinksVisitedAndUnvisited, + shared_context.quirks_mode(), + NeedsSelectorFlags::No, + MatchingForInvalidation::Yes, + ); + + Self { + shared_context, + element, + data, + matching_context, + traversal_map: SiblingTraversalMap::default(), + } + } +} + +/// Checks a dependency against a given element and wrapper, to see if something +/// changed. +pub fn check_dependency( + dependency: &Dependency, + element: &E, + wrapper: &W, + context: &mut MatchingContext<'_, E::Impl>, +) -> bool +where + E: TElement, + W: selectors::Element, +{ + let matches_now = matches_selector( + &dependency.selector, + dependency.selector_offset, + None, + element, + context, + ); + + let matched_then = matches_selector( + &dependency.selector, + dependency.selector_offset, + None, + wrapper, + context, + ); + + matched_then != matches_now +} + +/// Whether we should process the descendants of a given element for style +/// invalidation. +pub fn should_process_descendants(data: &ElementData) -> bool { + !data.styles.is_display_none() && !data.hint.contains(RestyleHint::RESTYLE_DESCENDANTS) +} + +/// Propagates the bits after invalidating a descendant child. +pub fn propagate_dirty_bit_up_to(ancestor: E, child: E) +where + E: TElement, +{ + // The child may not be a flattened tree child of the current element, + // but may be arbitrarily deep. + // + // Since we keep the traversal flags in terms of the flattened tree, + // we need to propagate it as appropriate. + let mut current = child.traversal_parent(); + while let Some(parent) = current.take() { + unsafe { parent.set_dirty_descendants() }; + current = parent.traversal_parent(); + + if parent == ancestor { + return; + } + } + debug_assert!( + false, + "Should've found {:?} as an ancestor of {:?}", + ancestor, child + ); +} + +/// Propagates the bits after invalidating a descendant child, if needed. +pub fn invalidated_descendants(element: E, child: E) +where + E: TElement, +{ + if !child.has_data() { + return; + } + propagate_dirty_bit_up_to(element, child) +} + +/// Sets the appropriate restyle hint after invalidating the style of a given +/// element. +pub fn invalidated_self(element: E) -> bool +where + E: TElement, +{ + let mut data = match element.mutate_data() { + Some(data) => data, + None => return false, + }; + data.hint.insert(RestyleHint::RESTYLE_SELF); + true +} + +/// Sets the appropriate hint after invalidating the style of a sibling. +pub fn invalidated_sibling(element: E, of: E) +where + E: TElement, +{ + debug_assert_eq!( + element.as_node().parent_node(), + of.as_node().parent_node(), + "Should be siblings" + ); + if !invalidated_self(element) { + return; + } + if element.traversal_parent() != of.traversal_parent() { + let parent = element.as_node().parent_element_or_host(); + debug_assert!( + parent.is_some(), + "How can we have siblings without parent nodes?" + ); + if let Some(e) = parent { + propagate_dirty_bit_up_to(e, element) + } + } +} + +impl<'a, 'b: 'a, E: 'a> InvalidationProcessor<'a, 'a, E> + for StateAndAttrInvalidationProcessor<'a, 'b, E> +where + E: TElement, +{ + /// We need to invalidate style on pseudo-elements, in order to process + /// changes that could otherwise end up in ::before or ::after content being + /// generated, and invalidate lazy pseudo caches. + fn invalidates_on_pseudo_element(&self) -> bool { + true + } + + fn check_outer_dependency(&mut self, dependency: &Dependency, element: E) -> bool { + // We cannot assert about `element` having a snapshot here (in fact it + // most likely won't), because it may be an arbitrary descendant or + // later-sibling of the element we started invalidating with. + let wrapper = ElementWrapper::new(element, &*self.shared_context.snapshot_map); + check_dependency(dependency, &element, &wrapper, &mut self.matching_context) + } + + fn matching_context(&mut self) -> &mut MatchingContext<'a, E::Impl> { + &mut self.matching_context + } + + fn sibling_traversal_map(&self) -> &SiblingTraversalMap { + &self.traversal_map + } + + 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 Some(snapshot) = wrapper.snapshot() else { + return false; + }; + + if !snapshot.has_attrs() && state_changes.is_empty() { + return false; + } + + let mut classes_removed = SmallVec::<[Atom; 8]>::new(); + let mut classes_added = SmallVec::<[Atom; 8]>::new(); + if snapshot.class_changed() { + // TODO(emilio): Do this more efficiently! + snapshot.each_class(|c| { + if !element.has_class(c, CaseSensitivity::CaseSensitive) { + classes_removed.push(c.0.clone()) + } + }); + + element.each_class(|c| { + if !snapshot.has_class(c, CaseSensitivity::CaseSensitive) { + classes_added.push(c.0.clone()) + } + }) + } + + let mut id_removed = None; + let mut id_added = None; + if snapshot.id_changed() { + let old_id = snapshot.id_attr(); + let current_id = element.id(); + + if old_id != current_id { + id_removed = old_id; + id_added = current_id; + } + } + + if log_enabled!(::log::Level::Debug) { + debug!("Collecting changes for: {:?}", element); + if !state_changes.is_empty() { + debug!(" > state: {:?}", state_changes); + } + if snapshot.id_changed() { + debug!(" > id changed: +{:?} -{:?}", id_added, id_removed); + } + if snapshot.class_changed() { + debug!( + " > class changed: +{:?} -{:?}", + classes_added, classes_removed + ); + } + let mut attributes_changed = false; + snapshot.each_attr_changed(|_| { + attributes_changed = true; + }); + if attributes_changed { + debug!( + " > attributes changed, old: {}", + snapshot.debug_list_attributes() + ) + } + } + + let lookup_element = if element.implemented_pseudo_element().is_some() { + element.pseudo_element_originating_element().unwrap() + } else { + element + }; + + let mut shadow_rule_datas = SmallVec::<[_; 3]>::new(); + let matches_document_author_rules = + element.each_applicable_non_document_style_rule_data(|data, host| { + shadow_rule_datas.push((data, host.opaque())) + }); + + let invalidated_self = { + let mut collector = Collector { + wrapper, + lookup_element, + state_changes, + element, + snapshot: &snapshot, + matching_context: &mut self.matching_context, + removed_id: id_removed, + added_id: id_added, + classes_removed: &classes_removed, + classes_added: &classes_added, + descendant_invalidations, + sibling_invalidations, + invalidates_self: false, + }; + + let document_origins = if !matches_document_author_rules { + OriginSet::ORIGIN_USER_AGENT | OriginSet::ORIGIN_USER + } else { + OriginSet::all() + }; + + for (cascade_data, origin) in self.shared_context.stylist.iter_origins() { + if document_origins.contains(origin.into()) { + collector + .collect_dependencies_in_invalidation_map(cascade_data.invalidation_map()); + } + } + + for &(ref data, ref host) in &shadow_rule_datas { + collector.matching_context.current_host = Some(host.clone()); + collector.collect_dependencies_in_invalidation_map(data.invalidation_map()); + } + + collector.invalidates_self + }; + + // If we generated a ton of descendant invalidations, it's probably not + // worth to go ahead and try to process them. + // + // Just restyle the descendants directly. + // + // This number is completely made-up, but the page that made us add this + // code generated 1960+ invalidations (bug 1420741). + // + // We don't look at slotted_descendants because those don't propagate + // down more than one level anyway. + if descendant_invalidations.dom_descendants.len() > 150 { + self.data.hint.insert(RestyleHint::RESTYLE_DESCENDANTS); + } + + if invalidated_self { + self.data.hint.insert(RestyleHint::RESTYLE_SELF); + } + + invalidated_self + } + + fn should_process_descendants(&mut self, element: E) -> bool { + if element == self.element { + return should_process_descendants(&self.data); + } + + match element.borrow_data() { + Some(d) => should_process_descendants(&d), + None => return false, + } + } + + fn recursion_limit_exceeded(&mut self, element: E) { + if element == self.element { + self.data.hint.insert(RestyleHint::RESTYLE_DESCENDANTS); + return; + } + + if let Some(mut data) = element.mutate_data() { + data.hint.insert(RestyleHint::RESTYLE_DESCENDANTS); + } + } + + fn invalidated_descendants(&mut self, element: E, child: E) { + invalidated_descendants(element, child) + } + + fn invalidated_self(&mut self, element: E) { + debug_assert_ne!(element, self.element); + invalidated_self(element); + } + + fn invalidated_sibling(&mut self, element: E, of: E) { + debug_assert_ne!(element, self.element); + invalidated_sibling(element, of); + } +} + +impl<'a, 'b, 'selectors, E> Collector<'a, 'b, 'selectors, E> +where + E: TElement, + 'selectors: 'a, +{ + fn collect_dependencies_in_invalidation_map(&mut self, map: &'selectors InvalidationMap) { + let quirks_mode = self.matching_context.quirks_mode(); + let removed_id = self.removed_id; + if let Some(ref id) = removed_id { + if let Some(deps) = map.id_to_selector.get(id, quirks_mode) { + for dep in deps { + self.scan_dependency(dep); + } + } + } + + let added_id = self.added_id; + if let Some(ref id) = added_id { + if let Some(deps) = map.id_to_selector.get(id, quirks_mode) { + for dep in deps { + self.scan_dependency(dep); + } + } + } + + for class in self.classes_added.iter().chain(self.classes_removed.iter()) { + if let Some(deps) = map.class_to_selector.get(class, quirks_mode) { + for dep in deps { + self.scan_dependency(dep); + } + } + } + + self.snapshot.each_attr_changed(|attribute| { + if let Some(deps) = map.other_attribute_affecting_selectors.get(attribute) { + for dep in deps { + self.scan_dependency(dep); + } + } + }); + + self.collect_state_dependencies(&map.state_affecting_selectors) + } + + fn collect_state_dependencies(&mut self, map: &'selectors SelectorMap) { + if self.state_changes.is_empty() { + return; + } + map.lookup_with_additional( + self.lookup_element, + self.matching_context.quirks_mode(), + self.removed_id, + self.classes_removed, + self.state_changes, + |dependency| { + if !dependency.state.intersects(self.state_changes) { + return true; + } + self.scan_dependency(&dependency.dep); + true + }, + ); + } + + /// Check whether a dependency should be taken into account. + #[inline] + fn check_dependency(&mut self, dependency: &Dependency) -> bool { + check_dependency( + dependency, + &self.element, + &self.wrapper, + &mut self.matching_context, + ) + } + + fn scan_dependency(&mut self, dependency: &'selectors Dependency) { + debug_assert!( + matches!( + dependency.invalidation_kind(), + DependencyInvalidationKind::Normal(_) + ), + "Found relative selector 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.normal_invalidation_kind(); + if matches!(invalidation_kind, NormalDependencyInvalidationKind::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()); + + self.invalidates_self |= push_invalidation( + invalidation, + invalidation_kind, + self.descendant_invalidations, + self.sibling_invalidations, + ); + } + + /// 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.normal_invalidation_kind() { + NormalDependencyInvalidationKind::Element => !self.invalidates_self, + NormalDependencyInvalidationKind::SlottedElements => { + self.element.is_html_slot_element() + }, + NormalDependencyInvalidationKind::Parts => self.element.shadow_root().is_some(), + NormalDependencyInvalidationKind::ElementAndDescendants | + NormalDependencyInvalidationKind::Siblings | + NormalDependencyInvalidationKind::Descendants => true, + } + } +} + +pub(crate) fn push_invalidation<'a>( + invalidation: Invalidation<'a>, + invalidation_kind: NormalDependencyInvalidationKind, + descendant_invalidations: &mut DescendantInvalidationLists<'a>, + sibling_invalidations: &mut InvalidationVector<'a>, +) -> bool { + match invalidation_kind { + NormalDependencyInvalidationKind::Element => unreachable!(), + NormalDependencyInvalidationKind::ElementAndDescendants => { + descendant_invalidations.dom_descendants.push(invalidation); + true + }, + NormalDependencyInvalidationKind::Descendants => { + descendant_invalidations.dom_descendants.push(invalidation); + false + }, + NormalDependencyInvalidationKind::Siblings => { + sibling_invalidations.push(invalidation); + false + }, + NormalDependencyInvalidationKind::Parts => { + descendant_invalidations.parts.push(invalidation); + false + }, + NormalDependencyInvalidationKind::SlottedElements => { + descendant_invalidations + .slotted_descendants + .push(invalidation); + false + }, + } +} + +pub(crate) fn dependency_may_be_relevant( + dependency: &Dependency, + element: &E, + already_invalidated_self: bool, +) -> bool { + match dependency.normal_invalidation_kind() { + NormalDependencyInvalidationKind::Element => !already_invalidated_self, + NormalDependencyInvalidationKind::SlottedElements => element.is_html_slot_element(), + NormalDependencyInvalidationKind::Parts => element.shadow_root().is_some(), + NormalDependencyInvalidationKind::ElementAndDescendants | + NormalDependencyInvalidationKind::Siblings | + NormalDependencyInvalidationKind::Descendants => true, + } +} diff --git a/servo/components/style/invalidation/media_queries.rs b/servo/components/style/invalidation/media_queries.rs new file mode 100644 index 0000000000..6928b29d3d --- /dev/null +++ b/servo/components/style/invalidation/media_queries.rs @@ -0,0 +1,130 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Code related to the invalidation of media-query-affected rules. + +use crate::context::QuirksMode; +use crate::media_queries::Device; +use crate::shared_lock::SharedRwLockReadGuard; +use crate::stylesheets::{DocumentRule, ImportRule, MediaRule}; +use crate::stylesheets::{NestedRuleIterationCondition, StylesheetContents, SupportsRule}; +use fxhash::FxHashSet; + +/// A key for a given media query result. +/// +/// NOTE: It happens to be the case that all the media lists we care about +/// happen to have a stable address, so we can just use an opaque pointer to +/// represent them. +/// +/// Also, note that right now when a rule or stylesheet is removed, we do a full +/// style flush, so there's no need to worry about other item created with the +/// same pointer address. +/// +/// If this changes, though, we may need to remove the item from the cache if +/// present before it goes away. +#[derive(Clone, Copy, Debug, Eq, Hash, MallocSizeOf, PartialEq)] +pub struct MediaListKey(usize); + +impl MediaListKey { + /// Create a MediaListKey from a raw usize. + pub fn from_raw(k: usize) -> Self { + MediaListKey(k) + } +} + +/// A trait to get a given `MediaListKey` for a given item that can hold a +/// `MediaList`. +pub trait ToMediaListKey: Sized { + /// Get a `MediaListKey` for this item. This key needs to uniquely identify + /// the item. + fn to_media_list_key(&self) -> MediaListKey { + MediaListKey(self as *const Self as usize) + } +} + +impl ToMediaListKey for StylesheetContents {} +impl ToMediaListKey for ImportRule {} +impl ToMediaListKey for MediaRule {} + +/// A struct that holds the result of a media query evaluation pass for the +/// media queries that evaluated successfully. +#[derive(Clone, Debug, MallocSizeOf, PartialEq)] +pub struct EffectiveMediaQueryResults { + /// The set of media lists that matched last time. + set: FxHashSet, +} + +impl EffectiveMediaQueryResults { + /// Trivially constructs an empty `EffectiveMediaQueryResults`. + pub fn new() -> Self { + Self { + set: FxHashSet::default(), + } + } + + /// Resets the results, using an empty key. + pub fn clear(&mut self) { + self.set.clear() + } + + /// Returns whether a given item was known to be effective when the results + /// were cached. + pub fn was_effective(&self, item: &T) -> bool + where + T: ToMediaListKey, + { + self.set.contains(&item.to_media_list_key()) + } + + /// Notices that an effective item has been seen, and caches it as matching. + pub fn saw_effective(&mut self, item: &T) + where + T: ToMediaListKey, + { + // NOTE(emilio): We can't assert that we don't cache the same item twice + // because of stylesheet reusing... shrug. + self.set.insert(item.to_media_list_key()); + } +} + +/// A filter that filters over effective rules, but allowing all potentially +/// effective `@media` rules. +pub struct PotentiallyEffectiveMediaRules; + +impl NestedRuleIterationCondition for PotentiallyEffectiveMediaRules { + fn process_import( + _: &SharedRwLockReadGuard, + _: &Device, + _: QuirksMode, + _: &ImportRule, + ) -> bool { + true + } + + fn process_media(_: &SharedRwLockReadGuard, _: &Device, _: QuirksMode, _: &MediaRule) -> bool { + true + } + + /// Whether we should process the nested rules in a given `@-moz-document` rule. + fn process_document( + guard: &SharedRwLockReadGuard, + device: &Device, + quirks_mode: QuirksMode, + rule: &DocumentRule, + ) -> bool { + use crate::stylesheets::EffectiveRules; + EffectiveRules::process_document(guard, device, quirks_mode, rule) + } + + /// Whether we should process the nested rules in a given `@supports` rule. + fn process_supports( + guard: &SharedRwLockReadGuard, + device: &Device, + quirks_mode: QuirksMode, + rule: &SupportsRule, + ) -> bool { + use crate::stylesheets::EffectiveRules; + EffectiveRules::process_supports(guard, device, quirks_mode, rule) + } +} diff --git a/servo/components/style/invalidation/mod.rs b/servo/components/style/invalidation/mod.rs new file mode 100644 index 0000000000..12b0d06853 --- /dev/null +++ b/servo/components/style/invalidation/mod.rs @@ -0,0 +1,10 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Different bits of code related to invalidating style. + +pub mod element; +pub mod media_queries; +pub mod stylesheets; +pub mod viewport_units; diff --git a/servo/components/style/invalidation/stylesheets.rs b/servo/components/style/invalidation/stylesheets.rs new file mode 100644 index 0000000000..d845897aa4 --- /dev/null +++ b/servo/components/style/invalidation/stylesheets.rs @@ -0,0 +1,651 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! A collection of invalidations due to changes in which stylesheets affect a +//! document. + +#![deny(unsafe_code)] + +use crate::context::QuirksMode; +use crate::dom::{TDocument, TElement, TNode}; +use crate::invalidation::element::element_wrapper::{ElementSnapshot, ElementWrapper}; +use crate::invalidation::element::restyle_hints::RestyleHint; +use crate::media_queries::Device; +use crate::selector_map::{MaybeCaseInsensitiveHashMap, PrecomputedHashMap}; +use crate::selector_parser::{SelectorImpl, Snapshot, SnapshotMap}; +use crate::shared_lock::SharedRwLockReadGuard; +use crate::stylesheets::{CssRule, StylesheetInDocument}; +use crate::stylesheets::{EffectiveRules, EffectiveRulesIterator}; +use crate::values::AtomIdent; +use crate::LocalName as SelectorLocalName; +use crate::{Atom, ShrinkIfNeeded}; +use selectors::parser::{Component, LocalName, Selector}; + +/// The kind of change that happened for a given rule. +#[repr(u32)] +#[derive(Clone, Copy, Debug, Eq, Hash, MallocSizeOf, PartialEq)] +pub enum RuleChangeKind { + /// The rule was inserted. + Insertion, + /// The rule was removed. + Removal, + /// Some change in the rule which we don't know about, and could have made + /// the rule change in any way. + Generic, + /// A change in the declarations of a style rule. + StyleRuleDeclarations, +} + +/// A style sheet invalidation represents a kind of element or subtree that may +/// need to be restyled. Whether it represents a whole subtree or just a single +/// element is determined by the given InvalidationKind in +/// StylesheetInvalidationSet's maps. +#[derive(Debug, Eq, Hash, MallocSizeOf, PartialEq)] +enum Invalidation { + /// An element with a given id. + ID(AtomIdent), + /// An element with a given class name. + Class(AtomIdent), + /// An element with a given local name. + LocalName { + name: SelectorLocalName, + lower_name: SelectorLocalName, + }, +} + +impl Invalidation { + fn is_id(&self) -> bool { + matches!(*self, Invalidation::ID(..)) + } + + fn is_id_or_class(&self) -> bool { + matches!(*self, Invalidation::ID(..) | Invalidation::Class(..)) + } +} + +/// Whether we should invalidate just the element, or the whole subtree within +/// it. +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, Ord, PartialEq, PartialOrd)] +enum InvalidationKind { + None = 0, + Element, + Scope, +} + +impl std::ops::BitOrAssign for InvalidationKind { + #[inline] + fn bitor_assign(&mut self, other: Self) { + *self = std::cmp::max(*self, other); + } +} + +impl InvalidationKind { + #[inline] + fn is_scope(self) -> bool { + matches!(self, Self::Scope) + } + + #[inline] + fn add(&mut self, other: Option<&InvalidationKind>) { + if let Some(other) = other { + *self |= *other; + } + } +} + +/// A set of invalidations due to stylesheet additions. +/// +/// TODO(emilio): We might be able to do the same analysis for media query +/// changes too (or even selector changes?). +#[derive(Debug, Default, MallocSizeOf)] +pub struct StylesheetInvalidationSet { + classes: MaybeCaseInsensitiveHashMap, + ids: MaybeCaseInsensitiveHashMap, + local_names: PrecomputedHashMap, + fully_invalid: bool, +} + +impl StylesheetInvalidationSet { + /// Create an empty `StylesheetInvalidationSet`. + pub fn new() -> Self { + Default::default() + } + + /// Mark the DOM tree styles' as fully invalid. + pub fn invalidate_fully(&mut self) { + debug!("StylesheetInvalidationSet::invalidate_fully"); + self.clear(); + self.fully_invalid = true; + } + + fn shrink_if_needed(&mut self) { + if self.fully_invalid { + return; + } + self.classes.shrink_if_needed(); + self.ids.shrink_if_needed(); + self.local_names.shrink_if_needed(); + } + + /// Analyze the given stylesheet, and collect invalidations from their + /// rules, in order to avoid doing a full restyle when we style the document + /// next time. + pub fn collect_invalidations_for( + &mut self, + device: &Device, + stylesheet: &S, + guard: &SharedRwLockReadGuard, + ) where + S: StylesheetInDocument, + { + debug!("StylesheetInvalidationSet::collect_invalidations_for"); + if self.fully_invalid { + debug!(" > Fully invalid already"); + return; + } + + if !stylesheet.enabled() || !stylesheet.is_effective_for_device(device, guard) { + debug!(" > Stylesheet was not effective"); + return; // Nothing to do here. + } + + let quirks_mode = device.quirks_mode(); + for rule in stylesheet.effective_rules(device, guard) { + self.collect_invalidations_for_rule( + rule, + guard, + device, + quirks_mode, + /* is_generic_change = */ false, + ); + if self.fully_invalid { + break; + } + } + + self.shrink_if_needed(); + + debug!(" > resulting class invalidations: {:?}", self.classes); + debug!(" > resulting id invalidations: {:?}", self.ids); + debug!( + " > resulting local name invalidations: {:?}", + self.local_names + ); + debug!(" > fully_invalid: {}", self.fully_invalid); + } + + /// Clears the invalidation set, invalidating elements as needed if + /// `document_element` is provided. + /// + /// Returns true if any invalidations ocurred. + pub fn flush(&mut self, document_element: Option, snapshots: Option<&SnapshotMap>) -> bool + where + E: TElement, + { + debug!( + "Stylist::flush({:?}, snapshots: {})", + document_element, + snapshots.is_some() + ); + let have_invalidations = match document_element { + Some(e) => self.process_invalidations(e, snapshots), + None => false, + }; + self.clear(); + have_invalidations + } + + /// Returns whether there's no invalidation to process. + pub fn is_empty(&self) -> bool { + !self.fully_invalid && + self.classes.is_empty() && + self.ids.is_empty() && + self.local_names.is_empty() + } + + fn invalidation_kind_for( + &self, + element: E, + snapshot: Option<&Snapshot>, + quirks_mode: QuirksMode, + ) -> InvalidationKind + where + E: TElement, + { + debug_assert!(!self.fully_invalid); + + let mut kind = InvalidationKind::None; + + if !self.classes.is_empty() { + element.each_class(|c| { + kind.add(self.classes.get(c, quirks_mode)); + }); + + if kind.is_scope() { + return kind; + } + + if let Some(snapshot) = snapshot { + snapshot.each_class(|c| { + kind.add(self.classes.get(c, quirks_mode)); + }); + + if kind.is_scope() { + return kind; + } + } + } + + if !self.ids.is_empty() { + if let Some(ref id) = element.id() { + kind.add(self.ids.get(id, quirks_mode)); + if kind.is_scope() { + return kind; + } + } + + if let Some(ref old_id) = snapshot.and_then(|s| s.id_attr()) { + kind.add(self.ids.get(old_id, quirks_mode)); + if kind.is_scope() { + return kind; + } + } + } + + if !self.local_names.is_empty() { + kind.add(self.local_names.get(element.local_name())); + } + + kind + } + + /// Clears the invalidation set without processing. + pub fn clear(&mut self) { + self.classes.clear(); + self.ids.clear(); + self.local_names.clear(); + self.fully_invalid = false; + debug_assert!(self.is_empty()); + } + + fn process_invalidations(&self, element: E, snapshots: Option<&SnapshotMap>) -> bool + where + E: TElement, + { + debug!("Stylist::process_invalidations({:?}, {:?})", element, self); + + { + let mut data = match element.mutate_data() { + Some(data) => data, + None => return false, + }; + + if self.fully_invalid { + debug!("process_invalidations: fully_invalid({:?})", element); + data.hint.insert(RestyleHint::restyle_subtree()); + return true; + } + } + + if self.is_empty() { + debug!("process_invalidations: empty invalidation set"); + return false; + } + + let quirks_mode = element.as_node().owner_doc().quirks_mode(); + self.process_invalidations_in_subtree(element, snapshots, quirks_mode) + } + + /// Process style invalidations in a given subtree. This traverses the + /// subtree looking for elements that match the invalidations in our hash + /// map members. + /// + /// Returns whether it invalidated at least one element's style. + #[allow(unsafe_code)] + fn process_invalidations_in_subtree( + &self, + element: E, + snapshots: Option<&SnapshotMap>, + quirks_mode: QuirksMode, + ) -> bool + where + E: TElement, + { + debug!("process_invalidations_in_subtree({:?})", element); + let mut data = match element.mutate_data() { + Some(data) => data, + None => return false, + }; + + if !data.has_styles() { + return false; + } + + if data.hint.contains_subtree() { + debug!( + "process_invalidations_in_subtree: {:?} was already invalid", + element + ); + return false; + } + + let element_wrapper = snapshots.map(|s| ElementWrapper::new(element, s)); + let snapshot = element_wrapper.as_ref().and_then(|e| e.snapshot()); + + match self.invalidation_kind_for(element, snapshot, quirks_mode) { + InvalidationKind::None => {}, + InvalidationKind::Element => { + debug!( + "process_invalidations_in_subtree: {:?} matched self", + element + ); + data.hint.insert(RestyleHint::RESTYLE_SELF); + }, + InvalidationKind::Scope => { + debug!( + "process_invalidations_in_subtree: {:?} matched subtree", + element + ); + data.hint.insert(RestyleHint::restyle_subtree()); + return true; + }, + } + + let mut any_children_invalid = false; + + for child in element.traversal_children() { + let child = match child.as_element() { + Some(e) => e, + None => continue, + }; + + any_children_invalid |= + self.process_invalidations_in_subtree(child, snapshots, quirks_mode); + } + + if any_children_invalid { + debug!( + "Children of {:?} changed, setting dirty descendants", + element + ); + unsafe { element.set_dirty_descendants() } + } + + data.hint.contains(RestyleHint::RESTYLE_SELF) || any_children_invalid + } + + /// TODO(emilio): Reuse the bucket stuff from selectormap? That handles + /// :is() / :where() etc. + fn scan_component( + component: &Component, + invalidation: &mut Option, + ) { + match *component { + Component::LocalName(LocalName { + ref name, + ref lower_name, + }) => { + if invalidation.is_none() { + *invalidation = Some(Invalidation::LocalName { + name: name.clone(), + lower_name: lower_name.clone(), + }); + } + }, + Component::Class(ref class) => { + if invalidation.as_ref().map_or(true, |s| !s.is_id_or_class()) { + *invalidation = Some(Invalidation::Class(class.clone())); + } + }, + Component::ID(ref id) => { + if invalidation.as_ref().map_or(true, |s| !s.is_id()) { + *invalidation = Some(Invalidation::ID(id.clone())); + } + }, + _ => { + // Ignore everything else, at least for now. + }, + } + } + + /// Collect invalidations for a given selector. + /// + /// We look at the outermost local name, class, or ID selector to the left + /// of an ancestor combinator, in order to restyle only a given subtree. + /// + /// If the selector has no ancestor combinator, then we do the same for + /// the only sequence it has, but record it as an element invalidation + /// instead of a subtree invalidation. + /// + /// We prefer IDs to classs, and classes to local names, on the basis + /// that the former should be more specific than the latter. We also + /// prefer to generate subtree invalidations for the outermost part + /// of the selector, to reduce the amount of traversal we need to do + /// when flushing invalidations. + fn collect_invalidations( + &mut self, + selector: &Selector, + quirks_mode: QuirksMode, + ) { + debug!( + "StylesheetInvalidationSet::collect_invalidations({:?})", + selector + ); + + let mut element_invalidation: Option = None; + let mut subtree_invalidation: Option = None; + + let mut scan_for_element_invalidation = true; + let mut scan_for_subtree_invalidation = false; + + let mut iter = selector.iter(); + + loop { + for component in &mut iter { + if scan_for_element_invalidation { + Self::scan_component(component, &mut element_invalidation); + } else if scan_for_subtree_invalidation { + Self::scan_component(component, &mut subtree_invalidation); + } + } + match iter.next_sequence() { + None => break, + Some(combinator) => { + scan_for_subtree_invalidation = combinator.is_ancestor(); + }, + } + scan_for_element_invalidation = false; + } + + if let Some(s) = subtree_invalidation { + debug!(" > Found subtree invalidation: {:?}", s); + if self.insert_invalidation(s, InvalidationKind::Scope, quirks_mode) { + return; + } + } + + if let Some(s) = element_invalidation { + debug!(" > Found element invalidation: {:?}", s); + if self.insert_invalidation(s, InvalidationKind::Element, quirks_mode) { + return; + } + } + + // The selector was of a form that we can't handle. Any element could + // match it, so let's just bail out. + debug!(" > Can't handle selector or OOMd, marking fully invalid"); + self.invalidate_fully() + } + + fn insert_invalidation( + &mut self, + invalidation: Invalidation, + kind: InvalidationKind, + quirks_mode: QuirksMode, + ) -> bool { + match invalidation { + Invalidation::Class(c) => { + let entry = match self.classes.try_entry(c.0, quirks_mode) { + Ok(e) => e, + Err(..) => return false, + }; + *entry.or_insert(InvalidationKind::None) |= kind; + }, + Invalidation::ID(i) => { + let entry = match self.ids.try_entry(i.0, quirks_mode) { + Ok(e) => e, + Err(..) => return false, + }; + *entry.or_insert(InvalidationKind::None) |= kind; + }, + Invalidation::LocalName { name, lower_name } => { + let insert_lower = name != lower_name; + if self.local_names.try_reserve(1).is_err() { + return false; + } + let entry = self.local_names.entry(name); + *entry.or_insert(InvalidationKind::None) |= kind; + if insert_lower { + if self.local_names.try_reserve(1).is_err() { + return false; + } + let entry = self.local_names.entry(lower_name); + *entry.or_insert(InvalidationKind::None) |= kind; + } + }, + } + + true + } + + /// Collects invalidations for a given CSS rule, if not fully invalid + /// already. + /// + /// TODO(emilio): we can't check whether the rule is inside a non-effective + /// subtree, we potentially could do that. + pub fn rule_changed( + &mut self, + stylesheet: &S, + rule: &CssRule, + guard: &SharedRwLockReadGuard, + device: &Device, + quirks_mode: QuirksMode, + change_kind: RuleChangeKind, + ) where + S: StylesheetInDocument, + { + 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. + } + + // If the change is generic, we don't have the old rule information to know e.g., the old + // media condition, or the old selector text, so we might need to invalidate more + // aggressively. That only applies to the changed rules, for other rules we can just + // collect invalidations as normal. + let is_generic_change = change_kind == RuleChangeKind::Generic; + self.collect_invalidations_for_rule(rule, guard, device, quirks_mode, is_generic_change); + if self.fully_invalid { + return; + } + + 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, + /* is_generic_change = */ false, + ); + 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, + is_generic_change: bool, + ) { + use crate::stylesheets::CssRule::*; + debug!("StylesheetInvalidationSet::collect_invalidations_for_rule"); + debug_assert!(!self.fully_invalid, "Not worth being here!"); + + match *rule { + Style(ref lock) => { + 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(); + } + + let style_rule = lock.read_with(guard); + for selector in style_rule.selectors.slice() { + self.collect_invalidations(selector, quirks_mode); + if self.fully_invalid { + return; + } + } + }, + Namespace(..) => { + // It's not clear what handling changes for this correctly would + // look like. + }, + LayerStatement(..) => { + // Layer statement insertions might alter styling order, so we need to always + // invalidate fully. + return self.invalidate_fully(); + }, + Document(..) | Import(..) | Media(..) | Supports(..) | Container(..) | + LayerBlock(..) => { + // Do nothing, relevant nested rules are visited as part of rule iteration. + }, + FontFace(..) => { + // Do nothing, @font-face doesn't affect computed style information on it's own. + // We'll restyle when the font face loads, if needed. + }, + Page(..) | Margin(..) => { + // Do nothing, we don't support OM mutations on print documents, and page rules + // can't affect anything else. + }, + Keyframes(ref lock) => { + if is_generic_change { + return self.invalidate_fully(); + } + 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.invalidate_fully(); + } else { + // Do nothing, this animation can't affect the style of existing elements. + } + }, + CounterStyle(..) | Property(..) | FontFeatureValues(..) | FontPaletteValues(..) => { + debug!(" > Found unsupported rule, marking the whole subtree invalid."); + self.invalidate_fully(); + }, + } + } +} diff --git a/servo/components/style/invalidation/viewport_units.rs b/servo/components/style/invalidation/viewport_units.rs new file mode 100644 index 0000000000..06faeb14c4 --- /dev/null +++ b/servo/components/style/invalidation/viewport_units.rs @@ -0,0 +1,71 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Invalidates style of all elements that depend on viewport units. + +use crate::data::ViewportUnitUsage; +use crate::dom::{TElement, TNode}; +use crate::invalidation::element::restyle_hints::RestyleHint; + +/// Invalidates style of all elements that depend on viewport units. +/// +/// Returns whether any element was invalidated. +pub fn invalidate(root: E) -> bool +where + E: TElement, +{ + debug!("invalidation::viewport_units::invalidate({:?})", root); + invalidate_recursively(root) +} + +fn invalidate_recursively(element: E) -> bool +where + E: TElement, +{ + let mut data = match element.mutate_data() { + Some(data) => data, + None => return false, + }; + + if data.hint.will_recascade_subtree() { + debug!("invalidate_recursively: {:?} was already invalid", element); + return false; + } + + let usage = data.styles.viewport_unit_usage(); + let uses_viewport_units = usage != ViewportUnitUsage::None; + if uses_viewport_units { + debug!( + "invalidate_recursively: {:?} uses viewport units {:?}", + element, usage + ); + } + + match usage { + ViewportUnitUsage::None => {}, + ViewportUnitUsage::FromQuery => { + data.hint.insert(RestyleHint::RESTYLE_SELF); + }, + ViewportUnitUsage::FromDeclaration => { + data.hint.insert(RestyleHint::RECASCADE_SELF); + }, + } + + let mut any_children_invalid = false; + for child in element.traversal_children() { + if let Some(child) = child.as_element() { + any_children_invalid |= invalidate_recursively(child); + } + } + + if any_children_invalid { + debug!( + "invalidate_recursively: Children of {:?} changed, setting dirty descendants", + element + ); + unsafe { element.set_dirty_descendants() } + } + + uses_viewport_units || any_children_invalid +} diff --git a/servo/components/style/lib.rs b/servo/components/style/lib.rs new file mode 100644 index 0000000000..b2a1e20ce8 --- /dev/null +++ b/servo/components/style/lib.rs @@ -0,0 +1,332 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Calculate [specified][specified] and [computed values][computed] from a +//! tree of DOM nodes and a set of stylesheets. +//! +//! [computed]: https://drafts.csswg.org/css-cascade/#computed +//! [specified]: https://drafts.csswg.org/css-cascade/#specified +//! +//! In particular, this crate contains the definitions of supported properties, +//! the code to parse them into specified values and calculate the computed +//! values based on the specified values, as well as the code to serialize both +//! specified and computed values. +//! +//! The main entry point is [`recalc_style_at`][recalc_style_at]. +//! +//! [recalc_style_at]: traversal/fn.recalc_style_at.html +//! +//! Major dependencies are the [cssparser][cssparser] and [selectors][selectors] +//! crates. +//! +//! [cssparser]: ../cssparser/index.html +//! [selectors]: ../selectors/index.html + +#![deny(missing_docs)] + +#[macro_use] +extern crate bitflags; +#[macro_use] +extern crate cssparser; +#[macro_use] +extern crate debug_unreachable; +#[macro_use] +extern crate derive_more; +#[macro_use] +extern crate gecko_profiler; +#[cfg(feature = "gecko")] +#[macro_use] +pub mod gecko_string_cache; +#[cfg(feature = "servo")] +#[macro_use] +extern crate html5ever; +#[macro_use] +extern crate lazy_static; +#[macro_use] +extern crate log; +#[macro_use] +extern crate malloc_size_of; +#[macro_use] +extern crate malloc_size_of_derive; +#[allow(unused_extern_crates)] +#[macro_use] +extern crate matches; +#[cfg(feature = "gecko")] +pub use nsstring; +#[cfg(feature = "gecko")] +extern crate num_cpus; +#[macro_use] +extern crate num_derive; +#[macro_use] +extern crate serde; +pub use servo_arc; +#[cfg(feature = "servo")] +#[macro_use] +extern crate servo_atoms; +#[macro_use] +extern crate static_assertions; +#[macro_use] +extern crate style_derive; +#[macro_use] +extern crate to_shmem_derive; + +#[macro_use] +mod macros; + +pub mod animation; +pub mod applicable_declarations; +#[allow(missing_docs)] // TODO. +#[cfg(feature = "servo")] +pub mod attr; +pub mod author_styles; +pub mod bezier; +pub mod bloom; +pub mod color; +#[path = "properties/computed_value_flags.rs"] +pub mod computed_value_flags; +pub mod context; +pub mod counter_style; +pub mod custom_properties; +pub mod custom_properties_map; +pub mod data; +pub mod dom; +pub mod dom_apis; +pub mod driver; +#[cfg(feature = "servo")] +mod encoding_support; +pub mod error_reporting; +pub mod font_face; +pub mod font_metrics; +#[cfg(feature = "gecko")] +#[allow(unsafe_code)] +pub mod gecko_bindings; +pub mod global_style_data; +pub mod invalidation; +#[allow(missing_docs)] // TODO. +pub mod logical_geometry; +pub mod matching; +pub mod media_queries; +pub mod parallel; +pub mod parser; +pub mod piecewise_linear; +pub mod properties_and_values; +#[macro_use] +pub mod queries; +pub mod rule_cache; +pub mod rule_collector; +pub mod rule_tree; +pub mod scoped_tls; +pub mod selector_map; +pub mod selector_parser; +pub mod shared_lock; +pub mod sharing; +pub mod str; +pub mod style_adjuster; +pub mod style_resolver; +pub mod stylesheet_set; +pub mod stylesheets; +pub mod stylist; +pub mod thread_state; +pub mod traversal; +pub mod traversal_flags; +pub mod use_counters; +#[macro_use] +#[allow(non_camel_case_types)] +pub mod values; + +#[cfg(feature = "gecko")] +pub use crate::gecko_string_cache as string_cache; +#[cfg(feature = "gecko")] +pub use crate::gecko_string_cache::Atom; +/// The namespace prefix type for Gecko, which is just an atom. +#[cfg(feature = "gecko")] +pub type Prefix = crate::values::AtomIdent; +/// The local name of an element for Gecko, which is just an atom. +#[cfg(feature = "gecko")] +pub type LocalName = crate::values::AtomIdent; +#[cfg(feature = "gecko")] +pub use crate::gecko_string_cache::Namespace; + +#[cfg(feature = "servo")] +pub use servo_atoms::Atom; + +#[cfg(feature = "servo")] +#[allow(missing_docs)] +pub type LocalName = crate::values::GenericAtomIdent; +#[cfg(feature = "servo")] +#[allow(missing_docs)] +pub type Namespace = crate::values::GenericAtomIdent; +#[cfg(feature = "servo")] +#[allow(missing_docs)] +pub type Prefix = crate::values::GenericAtomIdent; + +pub use style_traits::arc_slice::ArcSlice; +pub use style_traits::owned_slice::OwnedSlice; +pub use style_traits::owned_str::OwnedStr; + +use std::hash::{BuildHasher, Hash}; + +pub mod properties; + +#[cfg(feature = "gecko")] +#[allow(unsafe_code)] +pub mod gecko; + +// uses a macro from properties +#[cfg(feature = "servo")] +#[allow(unsafe_code)] +pub mod servo; + +macro_rules! reexport_computed_values { + ( $( { $name: ident } )+ ) => { + /// Types for [computed values][computed]. + /// + /// [computed]: https://drafts.csswg.org/css-cascade/#computed + pub mod computed_values { + $( + pub use crate::properties::longhands::$name::computed_value as $name; + )+ + // Don't use a side-specific name needlessly: + pub use crate::properties::longhands::border_top_style::computed_value as border_style; + } + } +} +longhand_properties_idents!(reexport_computed_values); +#[cfg(feature = "gecko")] +use crate::gecko_string_cache::WeakAtom; +#[cfg(feature = "servo")] +use servo_atoms::Atom as WeakAtom; + +/// Extension methods for selectors::attr::CaseSensitivity +pub trait CaseSensitivityExt { + /// Return whether two atoms compare equal according to this case sensitivity. + fn eq_atom(self, a: &WeakAtom, b: &WeakAtom) -> bool; +} + +impl CaseSensitivityExt for selectors::attr::CaseSensitivity { + #[inline] + fn eq_atom(self, a: &WeakAtom, b: &WeakAtom) -> bool { + match self { + selectors::attr::CaseSensitivity::CaseSensitive => a == b, + selectors::attr::CaseSensitivity::AsciiCaseInsensitive => a.eq_ignore_ascii_case(b), + } + } +} + +/// A trait pretty much similar to num_traits::Zero, but without the need of +/// implementing `Add`. +pub trait Zero { + /// Returns the zero value. + fn zero() -> Self; + + /// Returns whether this value is zero. + fn is_zero(&self) -> bool; +} + +impl Zero for T +where + T: num_traits::Zero, +{ + fn zero() -> Self { + ::zero() + } + + fn is_zero(&self) -> bool { + ::is_zero(self) + } +} + +/// A trait implementing a function to tell if the number is zero without a percent +pub trait ZeroNoPercent { + /// So, `0px` should return `true`, but `0%` or `1px` should return `false` + fn is_zero_no_percent(&self) -> bool; +} + +/// A trait pretty much similar to num_traits::One, but without the need of +/// implementing `Mul`. +pub trait One { + /// Reutrns the one value. + fn one() -> Self; + + /// Returns whether this value is one. + fn is_one(&self) -> bool; +} + +impl One for T +where + T: num_traits::One + PartialEq, +{ + fn one() -> Self { + ::one() + } + + fn is_one(&self) -> bool { + *self == One::one() + } +} + +/// An allocation error. +/// +/// TODO(emilio): Would be nice to have more information here, or for SmallVec +/// to return the standard error type (and then we can just return that). +/// +/// But given we use these mostly to bail out and ignore them, it's not a big +/// deal. +#[derive(Debug)] +pub struct AllocErr; + +impl From for AllocErr { + #[inline] + fn from(_: smallvec::CollectionAllocErr) -> Self { + Self + } +} + +impl From for AllocErr { + #[inline] + fn from(_: std::collections::TryReserveError) -> Self { + Self + } +} + +/// Shrink the capacity of the collection if needed. +pub(crate) trait ShrinkIfNeeded { + fn shrink_if_needed(&mut self); +} + +/// We shrink the capacity of a collection if we're wasting more than a 25% of +/// its capacity, and if the collection is arbitrarily big enough +/// (>= CAPACITY_THRESHOLD entries). +#[inline] +fn should_shrink(len: usize, capacity: usize) -> bool { + const CAPACITY_THRESHOLD: usize = 64; + capacity >= CAPACITY_THRESHOLD && len + capacity / 4 < capacity +} + +impl ShrinkIfNeeded for std::collections::HashMap +where + K: Eq + Hash, + H: BuildHasher, +{ + fn shrink_if_needed(&mut self) { + if should_shrink(self.len(), self.capacity()) { + self.shrink_to_fit(); + } + } +} + +impl ShrinkIfNeeded for std::collections::HashSet +where + T: Eq + Hash, + H: BuildHasher, +{ + fn shrink_if_needed(&mut self) { + if should_shrink(self.len(), self.capacity()) { + self.shrink_to_fit(); + } + } +} + +// TODO(emilio): Measure and see if we're wasting a lot of memory on Vec / +// SmallVec, and if so consider shrinking those as well. diff --git a/servo/components/style/logical_geometry.rs b/servo/components/style/logical_geometry.rs new file mode 100644 index 0000000000..03272ae545 --- /dev/null +++ b/servo/components/style/logical_geometry.rs @@ -0,0 +1,1629 @@ +/* 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() +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, Serialize)] +#[repr(C)] +pub struct WritingMode(u8); +bitflags!( + impl WritingMode: u8 { + /// A vertical writing mode; writing-mode is vertical-rl, + /// vertical-lr, sideways-lr, or sideways-rl. + const VERTICAL = 1 << 0; + /// The inline flow direction is reversed against the physical + /// direction (i.e. right-to-left or bottom-to-top); writing-mode is + /// sideways-lr or direction is rtl (but not both). + /// + /// (This bit can be derived from the others, but we store it for + /// convenience.) + const INLINE_REVERSED = 1 << 1; + /// A vertical writing mode whose block progression direction is left- + /// to-right; writing-mode is vertical-lr or sideways-lr. + /// + /// Never set without VERTICAL. + const VERTICAL_LR = 1 << 2; + /// The line-over/line-under sides are inverted with respect to the + /// block-start/block-end edge; writing-mode is vertical-lr. + /// + /// Never set without VERTICAL and VERTICAL_LR. + const LINE_INVERTED = 1 << 3; + /// direction is rtl. + const RTL = 1 << 4; + /// All text within a vertical writing mode is displayed sideways + /// and runs top-to-bottom or bottom-to-top; set in these cases: + /// + /// * writing-mode: sideways-rl; + /// * writing-mode: sideways-lr; + /// + /// Never set without VERTICAL. + const VERTICAL_SIDEWAYS = 1 << 5; + /// Similar to VERTICAL_SIDEWAYS, but is set via text-orientation; + /// set in these cases: + /// + /// * writing-mode: vertical-rl; text-orientation: sideways; + /// * writing-mode: vertical-lr; text-orientation: sideways; + /// + /// Never set without VERTICAL. + const TEXT_SIDEWAYS = 1 << 6; + /// Horizontal text within a vertical writing mode is displayed with each + /// glyph upright; set in these cases: + /// + /// * writing-mode: vertical-rl; text-orientation: upright; + /// * writing-mode: vertical-lr: text-orientation: upright; + /// + /// Never set without VERTICAL. + const UPRIGHT = 1 << 7; + } +); + +impl WritingMode { + /// Return a WritingMode bitflags from the relevant CSS properties. + pub fn new(inheritedbox_style: &style_structs::InheritedBox) -> Self { + use crate::properties::longhands::direction::computed_value::T as Direction; + use crate::properties::longhands::writing_mode::computed_value::T as SpecifiedWritingMode; + + let mut flags = WritingMode::empty(); + + let direction = inheritedbox_style.clone_direction(); + let writing_mode = inheritedbox_style.clone_writing_mode(); + + match direction { + Direction::Ltr => {}, + Direction::Rtl => { + flags.insert(WritingMode::RTL); + }, + } + + match writing_mode { + SpecifiedWritingMode::HorizontalTb => { + if direction == Direction::Rtl { + flags.insert(WritingMode::INLINE_REVERSED); + } + }, + SpecifiedWritingMode::VerticalRl => { + flags.insert(WritingMode::VERTICAL); + if direction == Direction::Rtl { + flags.insert(WritingMode::INLINE_REVERSED); + } + }, + SpecifiedWritingMode::VerticalLr => { + flags.insert(WritingMode::VERTICAL); + flags.insert(WritingMode::VERTICAL_LR); + flags.insert(WritingMode::LINE_INVERTED); + if direction == Direction::Rtl { + flags.insert(WritingMode::INLINE_REVERSED); + } + }, + #[cfg(feature = "gecko")] + SpecifiedWritingMode::SidewaysRl => { + flags.insert(WritingMode::VERTICAL); + flags.insert(WritingMode::VERTICAL_SIDEWAYS); + if direction == Direction::Rtl { + flags.insert(WritingMode::INLINE_REVERSED); + } + }, + #[cfg(feature = "gecko")] + SpecifiedWritingMode::SidewaysLr => { + flags.insert(WritingMode::VERTICAL); + flags.insert(WritingMode::VERTICAL_LR); + flags.insert(WritingMode::VERTICAL_SIDEWAYS); + if direction == Direction::Ltr { + flags.insert(WritingMode::INLINE_REVERSED); + } + }, + } + + #[cfg(feature = "gecko")] + { + use crate::properties::longhands::text_orientation::computed_value::T as TextOrientation; + + // text-orientation only has an effect for vertical-rl and + // vertical-lr values of writing-mode. + match writing_mode { + SpecifiedWritingMode::VerticalRl | SpecifiedWritingMode::VerticalLr => { + match inheritedbox_style.clone_text_orientation() { + TextOrientation::Mixed => {}, + TextOrientation::Upright => { + flags.insert(WritingMode::UPRIGHT); + + // https://drafts.csswg.org/css-writing-modes-3/#valdef-text-orientation-upright: + // + // > This value causes the used value of direction + // > to be ltr, and for the purposes of bidi + // > reordering, causes all characters to be treated + // > as strong LTR. + flags.remove(WritingMode::RTL); + flags.remove(WritingMode::INLINE_REVERSED); + }, + TextOrientation::Sideways => { + flags.insert(WritingMode::TEXT_SIDEWAYS); + }, + } + }, + _ => {}, + } + } + + flags + } + + /// Returns the `horizontal-tb` value. + pub fn horizontal_tb() -> Self { + Self::empty() + } + + #[inline] + pub fn is_vertical(&self) -> bool { + self.intersects(WritingMode::VERTICAL) + } + + #[inline] + pub fn is_horizontal(&self) -> bool { + !self.is_vertical() + } + + /// Assuming .is_vertical(), does the block direction go left to right? + #[inline] + pub fn is_vertical_lr(&self) -> bool { + self.intersects(WritingMode::VERTICAL_LR) + } + + /// Assuming .is_vertical(), does the inline direction go top to bottom? + #[inline] + pub fn is_inline_tb(&self) -> bool { + // https://drafts.csswg.org/css-writing-modes-3/#logical-to-physical + !self.intersects(WritingMode::INLINE_REVERSED) + } + + #[inline] + pub fn is_bidi_ltr(&self) -> bool { + !self.intersects(WritingMode::RTL) + } + + #[inline] + pub fn is_sideways(&self) -> bool { + self.intersects(WritingMode::VERTICAL_SIDEWAYS | WritingMode::TEXT_SIDEWAYS) + } + + #[inline] + pub fn is_upright(&self) -> bool { + self.intersects(WritingMode::UPRIGHT) + } + + /// https://drafts.csswg.org/css-writing-modes/#logical-to-physical + /// + /// | Return | line-left is… | line-right is… | + /// |---------|---------------|----------------| + /// | `true` | inline-start | inline-end | + /// | `false` | inline-end | inline-start | + #[inline] + pub fn line_left_is_inline_start(&self) -> bool { + // https://drafts.csswg.org/css-writing-modes/#inline-start + // “For boxes with a used direction value of ltr, this means the line-left side. + // For boxes with a used direction value of rtl, this means the line-right side.” + self.is_bidi_ltr() + } + + #[inline] + pub fn inline_start_physical_side(&self) -> PhysicalSide { + match (self.is_vertical(), self.is_inline_tb(), self.is_bidi_ltr()) { + (false, _, true) => PhysicalSide::Left, + (false, _, false) => PhysicalSide::Right, + (true, true, _) => PhysicalSide::Top, + (true, false, _) => PhysicalSide::Bottom, + } + } + + #[inline] + pub fn inline_end_physical_side(&self) -> PhysicalSide { + match (self.is_vertical(), self.is_inline_tb(), self.is_bidi_ltr()) { + (false, _, true) => PhysicalSide::Right, + (false, _, false) => PhysicalSide::Left, + (true, true, _) => PhysicalSide::Bottom, + (true, false, _) => PhysicalSide::Top, + } + } + + #[inline] + pub fn block_start_physical_side(&self) -> PhysicalSide { + match (self.is_vertical(), self.is_vertical_lr()) { + (false, _) => PhysicalSide::Top, + (true, true) => PhysicalSide::Left, + (true, false) => PhysicalSide::Right, + } + } + + #[inline] + pub fn block_end_physical_side(&self) -> PhysicalSide { + match (self.is_vertical(), self.is_vertical_lr()) { + (false, _) => PhysicalSide::Bottom, + (true, true) => PhysicalSide::Right, + (true, false) => PhysicalSide::Left, + } + } + + #[inline] + pub fn start_start_physical_corner(&self) -> PhysicalCorner { + PhysicalCorner::from_sides( + self.block_start_physical_side(), + self.inline_start_physical_side(), + ) + } + + #[inline] + pub fn start_end_physical_corner(&self) -> PhysicalCorner { + PhysicalCorner::from_sides( + self.block_start_physical_side(), + self.inline_end_physical_side(), + ) + } + + #[inline] + pub fn end_start_physical_corner(&self) -> PhysicalCorner { + PhysicalCorner::from_sides( + self.block_end_physical_side(), + self.inline_start_physical_side(), + ) + } + + #[inline] + pub fn end_end_physical_corner(&self) -> PhysicalCorner { + PhysicalCorner::from_sides( + 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() + } + } + + #[inline] + /// Is the text layout vertical? + pub fn is_text_vertical(&self) -> bool { + self.is_vertical() && !self.is_sideways() + } +} + +impl fmt::Display for WritingMode { + fn fmt(&self, formatter: &mut Formatter) -> Result<(), Error> { + if self.is_vertical() { + write!(formatter, "V")?; + if self.is_vertical_lr() { + write!(formatter, " LR")?; + } else { + write!(formatter, " RL")?; + } + if self.is_sideways() { + write!(formatter, " Sideways")?; + } + if self.intersects(WritingMode::LINE_INVERTED) { + write!(formatter, " Inverted")?; + } + } else { + write!(formatter, "H")?; + } + if self.is_bidi_ltr() { + write!(formatter, " LTR") + } else { + write!(formatter, " RTL") + } + } +} + +/// Wherever logical geometry is used, the writing mode is known based on context: +/// every method takes a `mode` parameter. +/// However, this context is easy to get wrong. +/// In debug builds only, logical geometry objects store their writing mode +/// (in addition to taking it as a parameter to methods) and check it. +/// In non-debug builds, make this storage zero-size and the checks no-ops. +#[cfg(not(debug_assertions))] +#[derive(Clone, Copy, Eq, PartialEq)] +#[cfg_attr(feature = "servo", derive(Serialize))] +struct DebugWritingMode; + +#[cfg(debug_assertions)] +#[derive(Clone, Copy, Eq, PartialEq)] +#[cfg_attr(feature = "servo", derive(Serialize))] +struct DebugWritingMode { + mode: WritingMode, +} + +#[cfg(not(debug_assertions))] +impl DebugWritingMode { + #[inline] + fn check(&self, _other: WritingMode) {} + + #[inline] + fn check_debug(&self, _other: DebugWritingMode) {} + + #[inline] + fn new(_mode: WritingMode) -> DebugWritingMode { + DebugWritingMode + } +} + +#[cfg(debug_assertions)] +impl DebugWritingMode { + #[inline] + fn check(&self, other: WritingMode) { + assert_eq!(self.mode, other) + } + + #[inline] + fn check_debug(&self, other: DebugWritingMode) { + assert_eq!(self.mode, other.mode) + } + + #[inline] + fn new(mode: WritingMode) -> DebugWritingMode { + DebugWritingMode { mode } + } +} + +impl Debug for DebugWritingMode { + #[cfg(not(debug_assertions))] + fn fmt(&self, formatter: &mut Formatter) -> Result<(), Error> { + write!(formatter, "?") + } + + #[cfg(debug_assertions)] + fn fmt(&self, formatter: &mut Formatter) -> Result<(), Error> { + write!(formatter, "{}", self.mode) + } +} + +// Used to specify the logical direction. +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "servo", derive(Serialize))] +pub enum Direction { + Inline, + Block, +} + +/// A 2D size in flow-relative dimensions +#[derive(Clone, Copy, Eq, PartialEq)] +#[cfg_attr(feature = "servo", derive(Serialize))] +pub struct LogicalSize { + pub inline: T, // inline-size, a.k.a. logical width, a.k.a. measure + pub block: T, // block-size, a.k.a. logical height, a.k.a. extent + debug_writing_mode: DebugWritingMode, +} + +impl Debug for LogicalSize { + fn fmt(&self, formatter: &mut Formatter) -> Result<(), Error> { + write!( + formatter, + "LogicalSize({:?}, i{:?}×b{:?})", + self.debug_writing_mode, self.inline, self.block + ) + } +} + +// Can not implement the Zero trait: its zero() method does not have the `mode` parameter. +impl LogicalSize { + #[inline] + pub fn zero(mode: WritingMode) -> LogicalSize { + LogicalSize { + inline: Zero::zero(), + block: Zero::zero(), + debug_writing_mode: DebugWritingMode::new(mode), + } + } +} + +impl LogicalSize { + #[inline] + pub fn new(mode: WritingMode, inline: T, block: T) -> LogicalSize { + LogicalSize { + inline: inline, + block: block, + debug_writing_mode: DebugWritingMode::new(mode), + } + } + + #[inline] + pub fn from_physical(mode: WritingMode, size: Size2D) -> LogicalSize { + if mode.is_vertical() { + LogicalSize::new(mode, size.height, size.width) + } else { + LogicalSize::new(mode, size.width, size.height) + } + } +} + +impl LogicalSize { + #[inline] + pub fn width(&self, mode: WritingMode) -> T { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + self.block + } else { + self.inline + } + } + + #[inline] + pub fn set_width(&mut self, mode: WritingMode, width: T) { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + self.block = width + } else { + self.inline = width + } + } + + #[inline] + pub fn height(&self, mode: WritingMode) -> T { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + self.inline + } else { + self.block + } + } + + #[inline] + pub fn set_height(&mut self, mode: WritingMode, height: T) { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + self.inline = height + } else { + self.block = height + } + } + + #[inline] + pub fn to_physical(&self, mode: WritingMode) -> Size2D { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + Size2D::new(self.block, self.inline) + } else { + Size2D::new(self.inline, self.block) + } + } + + #[inline] + pub fn convert(&self, mode_from: WritingMode, mode_to: WritingMode) -> LogicalSize { + if mode_from == mode_to { + self.debug_writing_mode.check(mode_from); + *self + } else { + LogicalSize::from_physical(mode_to, self.to_physical(mode_from)) + } + } +} + +impl> Add for LogicalSize { + type Output = LogicalSize; + + #[inline] + fn add(self, other: LogicalSize) -> LogicalSize { + self.debug_writing_mode + .check_debug(other.debug_writing_mode); + LogicalSize { + debug_writing_mode: self.debug_writing_mode, + inline: self.inline + other.inline, + block: self.block + other.block, + } + } +} + +impl> Sub for LogicalSize { + type Output = LogicalSize; + + #[inline] + fn sub(self, other: LogicalSize) -> LogicalSize { + self.debug_writing_mode + .check_debug(other.debug_writing_mode); + LogicalSize { + debug_writing_mode: self.debug_writing_mode, + inline: self.inline - other.inline, + block: self.block - other.block, + } + } +} + +/// A 2D point in flow-relative dimensions +#[derive(Clone, Copy, Eq, PartialEq)] +#[cfg_attr(feature = "servo", derive(Serialize))] +pub struct LogicalPoint { + /// inline-axis coordinate + pub i: T, + /// block-axis coordinate + pub b: T, + debug_writing_mode: DebugWritingMode, +} + +impl Debug for LogicalPoint { + fn fmt(&self, formatter: &mut Formatter) -> Result<(), Error> { + write!( + formatter, + "LogicalPoint({:?} (i{:?}, b{:?}))", + self.debug_writing_mode, self.i, self.b + ) + } +} + +// Can not implement the Zero trait: its zero() method does not have the `mode` parameter. +impl LogicalPoint { + #[inline] + pub fn zero(mode: WritingMode) -> LogicalPoint { + LogicalPoint { + i: Zero::zero(), + b: Zero::zero(), + debug_writing_mode: DebugWritingMode::new(mode), + } + } +} + +impl LogicalPoint { + #[inline] + pub fn new(mode: WritingMode, i: T, b: T) -> LogicalPoint { + LogicalPoint { + i: i, + b: b, + debug_writing_mode: DebugWritingMode::new(mode), + } + } +} + +impl> LogicalPoint { + #[inline] + pub fn from_physical( + mode: WritingMode, + point: Point2D, + container_size: Size2D, + ) -> LogicalPoint { + if mode.is_vertical() { + LogicalPoint { + i: if mode.is_inline_tb() { + point.y + } else { + container_size.height - point.y + }, + b: if mode.is_vertical_lr() { + point.x + } else { + container_size.width - point.x + }, + debug_writing_mode: DebugWritingMode::new(mode), + } + } else { + LogicalPoint { + i: if mode.is_bidi_ltr() { + point.x + } else { + container_size.width - point.x + }, + b: point.y, + debug_writing_mode: DebugWritingMode::new(mode), + } + } + } + + #[inline] + pub fn x(&self, mode: WritingMode, container_size: Size2D) -> T { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + if mode.is_vertical_lr() { + self.b + } else { + container_size.width - self.b + } + } else { + if mode.is_bidi_ltr() { + self.i + } else { + container_size.width - self.i + } + } + } + + #[inline] + pub fn set_x(&mut self, mode: WritingMode, x: T, container_size: Size2D) { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + self.b = if mode.is_vertical_lr() { + x + } else { + container_size.width - x + } + } else { + self.i = if mode.is_bidi_ltr() { + x + } else { + container_size.width - x + } + } + } + + #[inline] + pub fn y(&self, mode: WritingMode, container_size: Size2D) -> T { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + if mode.is_inline_tb() { + self.i + } else { + container_size.height - self.i + } + } else { + self.b + } + } + + #[inline] + pub fn set_y(&mut self, mode: WritingMode, y: T, container_size: Size2D) { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + self.i = if mode.is_inline_tb() { + y + } else { + container_size.height - y + } + } else { + self.b = y + } + } + + #[inline] + pub fn to_physical(&self, mode: WritingMode, container_size: Size2D) -> Point2D { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + Point2D::new( + if mode.is_vertical_lr() { + self.b + } else { + container_size.width - self.b + }, + if mode.is_inline_tb() { + self.i + } else { + container_size.height - self.i + }, + ) + } else { + Point2D::new( + if mode.is_bidi_ltr() { + self.i + } else { + container_size.width - self.i + }, + self.b, + ) + } + } + + #[inline] + pub fn convert( + &self, + mode_from: WritingMode, + mode_to: WritingMode, + container_size: Size2D, + ) -> LogicalPoint { + if mode_from == mode_to { + self.debug_writing_mode.check(mode_from); + *self + } else { + LogicalPoint::from_physical( + mode_to, + self.to_physical(mode_from, container_size), + container_size, + ) + } + } +} + +impl> LogicalPoint { + /// This doesn’t really makes sense, + /// but happens when dealing with multiple origins. + #[inline] + pub fn add_point(&self, other: &LogicalPoint) -> LogicalPoint { + self.debug_writing_mode + .check_debug(other.debug_writing_mode); + LogicalPoint { + debug_writing_mode: self.debug_writing_mode, + i: self.i + other.i, + b: self.b + other.b, + } + } +} + +impl> Add> for LogicalPoint { + type Output = LogicalPoint; + + #[inline] + fn add(self, other: LogicalSize) -> LogicalPoint { + self.debug_writing_mode + .check_debug(other.debug_writing_mode); + LogicalPoint { + debug_writing_mode: self.debug_writing_mode, + i: self.i + other.inline, + b: self.b + other.block, + } + } +} + +impl> Sub> for LogicalPoint { + type Output = LogicalPoint; + + #[inline] + fn sub(self, other: LogicalSize) -> LogicalPoint { + self.debug_writing_mode + .check_debug(other.debug_writing_mode); + LogicalPoint { + debug_writing_mode: self.debug_writing_mode, + i: self.i - other.inline, + b: self.b - other.block, + } + } +} + +/// A "margin" in flow-relative dimensions +/// Represents the four sides of the margins, borders, or padding of a CSS box, +/// or a combination of those. +/// A positive "margin" can be added to a rectangle to obtain a bigger rectangle. +#[derive(Clone, Copy, Eq, PartialEq)] +#[cfg_attr(feature = "servo", derive(Serialize))] +pub struct LogicalMargin { + pub block_start: T, + pub inline_end: T, + pub block_end: T, + pub inline_start: T, + debug_writing_mode: DebugWritingMode, +} + +impl Debug for LogicalMargin { + fn fmt(&self, formatter: &mut Formatter) -> Result<(), Error> { + let writing_mode_string = if cfg!(debug_assertions) { + format!("{:?}, ", self.debug_writing_mode) + } else { + "".to_owned() + }; + + write!( + formatter, + "LogicalMargin({}i:{:?}..{:?} b:{:?}..{:?})", + writing_mode_string, + self.inline_start, + self.inline_end, + self.block_start, + self.block_end + ) + } +} + +impl LogicalMargin { + #[inline] + pub fn zero(mode: WritingMode) -> LogicalMargin { + LogicalMargin { + block_start: Zero::zero(), + inline_end: Zero::zero(), + block_end: Zero::zero(), + inline_start: Zero::zero(), + debug_writing_mode: DebugWritingMode::new(mode), + } + } +} + +impl LogicalMargin { + #[inline] + pub fn new( + mode: WritingMode, + block_start: T, + inline_end: T, + block_end: T, + inline_start: T, + ) -> LogicalMargin { + LogicalMargin { + block_start, + inline_end, + block_end, + inline_start, + debug_writing_mode: DebugWritingMode::new(mode), + } + } + + #[inline] + pub fn from_physical(mode: WritingMode, offsets: SideOffsets2D) -> LogicalMargin { + let block_start; + let inline_end; + let block_end; + let inline_start; + if mode.is_vertical() { + if mode.is_vertical_lr() { + block_start = offsets.left; + block_end = offsets.right; + } else { + block_start = offsets.right; + block_end = offsets.left; + } + if mode.is_inline_tb() { + inline_start = offsets.top; + inline_end = offsets.bottom; + } else { + inline_start = offsets.bottom; + inline_end = offsets.top; + } + } else { + block_start = offsets.top; + block_end = offsets.bottom; + if mode.is_bidi_ltr() { + inline_start = offsets.left; + inline_end = offsets.right; + } else { + inline_start = offsets.right; + inline_end = offsets.left; + } + } + LogicalMargin::new(mode, block_start, inline_end, block_end, inline_start) + } +} + +impl LogicalMargin { + #[inline] + pub fn new_all_same(mode: WritingMode, value: T) -> LogicalMargin { + LogicalMargin::new(mode, value, value, value, value) + } + + #[inline] + pub fn top(&self, mode: WritingMode) -> T { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + if mode.is_inline_tb() { + self.inline_start + } else { + self.inline_end + } + } else { + self.block_start + } + } + + #[inline] + pub fn set_top(&mut self, mode: WritingMode, top: T) { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + if mode.is_inline_tb() { + self.inline_start = top + } else { + self.inline_end = top + } + } else { + self.block_start = top + } + } + + #[inline] + pub fn right(&self, mode: WritingMode) -> T { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + if mode.is_vertical_lr() { + self.block_end + } else { + self.block_start + } + } else { + if mode.is_bidi_ltr() { + self.inline_end + } else { + self.inline_start + } + } + } + + #[inline] + pub fn set_right(&mut self, mode: WritingMode, right: T) { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + if mode.is_vertical_lr() { + self.block_end = right + } else { + self.block_start = right + } + } else { + if mode.is_bidi_ltr() { + self.inline_end = right + } else { + self.inline_start = right + } + } + } + + #[inline] + pub fn bottom(&self, mode: WritingMode) -> T { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + if mode.is_inline_tb() { + self.inline_end + } else { + self.inline_start + } + } else { + self.block_end + } + } + + #[inline] + pub fn set_bottom(&mut self, mode: WritingMode, bottom: T) { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + if mode.is_inline_tb() { + self.inline_end = bottom + } else { + self.inline_start = bottom + } + } else { + self.block_end = bottom + } + } + + #[inline] + pub fn left(&self, mode: WritingMode) -> T { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + if mode.is_vertical_lr() { + self.block_start + } else { + self.block_end + } + } else { + if mode.is_bidi_ltr() { + self.inline_start + } else { + self.inline_end + } + } + } + + #[inline] + pub fn set_left(&mut self, mode: WritingMode, left: T) { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + if mode.is_vertical_lr() { + self.block_start = left + } else { + self.block_end = left + } + } else { + if mode.is_bidi_ltr() { + self.inline_start = left + } else { + self.inline_end = left + } + } + } + + #[inline] + pub fn to_physical(&self, mode: WritingMode) -> SideOffsets2D { + self.debug_writing_mode.check(mode); + let top; + let right; + let bottom; + let left; + if mode.is_vertical() { + if mode.is_vertical_lr() { + left = self.block_start; + right = self.block_end; + } else { + right = self.block_start; + left = self.block_end; + } + if mode.is_inline_tb() { + top = self.inline_start; + bottom = self.inline_end; + } else { + bottom = self.inline_start; + top = self.inline_end; + } + } else { + top = self.block_start; + bottom = self.block_end; + if mode.is_bidi_ltr() { + left = self.inline_start; + right = self.inline_end; + } else { + right = self.inline_start; + left = self.inline_end; + } + } + SideOffsets2D::new(top, right, bottom, left) + } + + #[inline] + pub fn convert(&self, mode_from: WritingMode, mode_to: WritingMode) -> LogicalMargin { + if mode_from == mode_to { + self.debug_writing_mode.check(mode_from); + *self + } else { + LogicalMargin::from_physical(mode_to, self.to_physical(mode_from)) + } + } +} + +impl LogicalMargin { + #[inline] + pub fn is_zero(&self) -> bool { + self.block_start == Zero::zero() && + self.inline_end == Zero::zero() && + self.block_end == Zero::zero() && + self.inline_start == Zero::zero() + } +} + +impl> LogicalMargin { + #[inline] + pub fn inline_start_end(&self) -> T { + self.inline_start + self.inline_end + } + + #[inline] + pub fn block_start_end(&self) -> T { + self.block_start + self.block_end + } + + #[inline] + pub fn start_end(&self, direction: Direction) -> T { + match direction { + Direction::Inline => self.inline_start + self.inline_end, + Direction::Block => self.block_start + self.block_end, + } + } + + #[inline] + pub fn top_bottom(&self, mode: WritingMode) -> T { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + self.inline_start_end() + } else { + self.block_start_end() + } + } + + #[inline] + pub fn left_right(&self, mode: WritingMode) -> T { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + self.block_start_end() + } else { + self.inline_start_end() + } + } +} + +impl> Add for LogicalMargin { + type Output = LogicalMargin; + + #[inline] + fn add(self, other: LogicalMargin) -> LogicalMargin { + self.debug_writing_mode + .check_debug(other.debug_writing_mode); + LogicalMargin { + debug_writing_mode: self.debug_writing_mode, + block_start: self.block_start + other.block_start, + inline_end: self.inline_end + other.inline_end, + block_end: self.block_end + other.block_end, + inline_start: self.inline_start + other.inline_start, + } + } +} + +impl> Sub for LogicalMargin { + type Output = LogicalMargin; + + #[inline] + fn sub(self, other: LogicalMargin) -> LogicalMargin { + self.debug_writing_mode + .check_debug(other.debug_writing_mode); + LogicalMargin { + debug_writing_mode: self.debug_writing_mode, + block_start: self.block_start - other.block_start, + inline_end: self.inline_end - other.inline_end, + block_end: self.block_end - other.block_end, + inline_start: self.inline_start - other.inline_start, + } + } +} + +/// A rectangle in flow-relative dimensions +#[derive(Clone, Copy, Eq, PartialEq)] +#[cfg_attr(feature = "servo", derive(Serialize))] +pub struct LogicalRect { + pub start: LogicalPoint, + pub size: LogicalSize, + debug_writing_mode: DebugWritingMode, +} + +impl Debug for LogicalRect { + fn fmt(&self, formatter: &mut Formatter) -> Result<(), Error> { + let writing_mode_string = if cfg!(debug_assertions) { + format!("{:?}, ", self.debug_writing_mode) + } else { + "".to_owned() + }; + + write!( + formatter, + "LogicalRect({}i{:?}×b{:?}, @ (i{:?},b{:?}))", + writing_mode_string, self.size.inline, self.size.block, self.start.i, self.start.b + ) + } +} + +impl LogicalRect { + #[inline] + pub fn zero(mode: WritingMode) -> LogicalRect { + LogicalRect { + start: LogicalPoint::zero(mode), + size: LogicalSize::zero(mode), + debug_writing_mode: DebugWritingMode::new(mode), + } + } +} + +impl LogicalRect { + #[inline] + pub fn new( + mode: WritingMode, + inline_start: T, + block_start: T, + inline: T, + block: T, + ) -> LogicalRect { + LogicalRect { + start: LogicalPoint::new(mode, inline_start, block_start), + size: LogicalSize::new(mode, inline, block), + debug_writing_mode: DebugWritingMode::new(mode), + } + } + + #[inline] + pub fn from_point_size( + mode: WritingMode, + start: LogicalPoint, + size: LogicalSize, + ) -> LogicalRect { + start.debug_writing_mode.check(mode); + size.debug_writing_mode.check(mode); + LogicalRect { + start: start, + size: size, + debug_writing_mode: DebugWritingMode::new(mode), + } + } +} + +impl + Sub> LogicalRect { + #[inline] + pub fn from_physical( + mode: WritingMode, + rect: Rect, + container_size: Size2D, + ) -> LogicalRect { + let inline_start; + let block_start; + let inline; + let block; + if mode.is_vertical() { + inline = rect.size.height; + block = rect.size.width; + if mode.is_vertical_lr() { + block_start = rect.origin.x; + } else { + block_start = container_size.width - (rect.origin.x + rect.size.width); + } + if mode.is_inline_tb() { + inline_start = rect.origin.y; + } else { + inline_start = container_size.height - (rect.origin.y + rect.size.height); + } + } else { + inline = rect.size.width; + block = rect.size.height; + block_start = rect.origin.y; + if mode.is_bidi_ltr() { + inline_start = rect.origin.x; + } else { + inline_start = container_size.width - (rect.origin.x + rect.size.width); + } + } + LogicalRect { + start: LogicalPoint::new(mode, inline_start, block_start), + size: LogicalSize::new(mode, inline, block), + debug_writing_mode: DebugWritingMode::new(mode), + } + } + + #[inline] + pub fn inline_end(&self) -> T { + self.start.i + self.size.inline + } + + #[inline] + pub fn block_end(&self) -> T { + self.start.b + self.size.block + } + + #[inline] + pub fn to_physical(&self, mode: WritingMode, container_size: Size2D) -> Rect { + self.debug_writing_mode.check(mode); + let x; + let y; + let width; + let height; + if mode.is_vertical() { + width = self.size.block; + height = self.size.inline; + if mode.is_vertical_lr() { + x = self.start.b; + } else { + x = container_size.width - self.block_end(); + } + if mode.is_inline_tb() { + y = self.start.i; + } else { + y = container_size.height - self.inline_end(); + } + } else { + width = self.size.inline; + height = self.size.block; + y = self.start.b; + if mode.is_bidi_ltr() { + x = self.start.i; + } else { + x = container_size.width - self.inline_end(); + } + } + Rect { + origin: Point2D::new(x, y), + size: Size2D::new(width, height), + } + } + + #[inline] + pub fn convert( + &self, + mode_from: WritingMode, + mode_to: WritingMode, + container_size: Size2D, + ) -> LogicalRect { + if mode_from == mode_to { + self.debug_writing_mode.check(mode_from); + *self + } else { + LogicalRect::from_physical( + mode_to, + self.to_physical(mode_from, container_size), + container_size, + ) + } + } + + pub fn translate_by_size(&self, offset: LogicalSize) -> LogicalRect { + LogicalRect { + start: self.start + offset, + ..*self + } + } + + pub fn translate(&self, offset: &LogicalPoint) -> LogicalRect { + LogicalRect { + start: self.start + + LogicalSize { + inline: offset.i, + block: offset.b, + debug_writing_mode: offset.debug_writing_mode, + }, + size: self.size, + debug_writing_mode: self.debug_writing_mode, + } + } +} + +impl + Sub> LogicalRect { + #[inline] + pub fn union(&self, other: &LogicalRect) -> LogicalRect { + self.debug_writing_mode + .check_debug(other.debug_writing_mode); + + let inline_start = min(self.start.i, other.start.i); + let block_start = min(self.start.b, other.start.b); + LogicalRect { + start: LogicalPoint { + i: inline_start, + b: block_start, + debug_writing_mode: self.debug_writing_mode, + }, + size: LogicalSize { + inline: max(self.inline_end(), other.inline_end()) - inline_start, + block: max(self.block_end(), other.block_end()) - block_start, + debug_writing_mode: self.debug_writing_mode, + }, + debug_writing_mode: self.debug_writing_mode, + } + } +} + +impl + Sub> Add> for LogicalRect { + type Output = LogicalRect; + + #[inline] + fn add(self, other: LogicalMargin) -> LogicalRect { + self.debug_writing_mode + .check_debug(other.debug_writing_mode); + LogicalRect { + start: LogicalPoint { + // Growing a rectangle on the start side means pushing its + // start point on the negative direction. + i: self.start.i - other.inline_start, + b: self.start.b - other.block_start, + debug_writing_mode: self.debug_writing_mode, + }, + size: LogicalSize { + inline: self.size.inline + other.inline_start_end(), + block: self.size.block + other.block_start_end(), + debug_writing_mode: self.debug_writing_mode, + }, + debug_writing_mode: self.debug_writing_mode, + } + } +} + +impl + Sub> Sub> for LogicalRect { + type Output = LogicalRect; + + #[inline] + fn sub(self, other: LogicalMargin) -> LogicalRect { + self.debug_writing_mode + .check_debug(other.debug_writing_mode); + LogicalRect { + start: LogicalPoint { + // Shrinking a rectangle on the start side means pushing its + // start point on the positive direction. + i: self.start.i + other.inline_start, + b: self.start.b + other.block_start, + debug_writing_mode: self.debug_writing_mode, + }, + size: LogicalSize { + inline: self.size.inline - other.inline_start_end(), + block: self.size.block - other.block_start_end(), + debug_writing_mode: self.debug_writing_mode, + }, + debug_writing_mode: self.debug_writing_mode, + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +#[repr(u8)] +pub enum LogicalAxis { + Block = 0, + Inline, +} + +impl LogicalAxis { + #[inline] + pub fn to_physical(self, wm: WritingMode) -> PhysicalAxis { + if wm.is_horizontal() == (self == Self::Inline) { + PhysicalAxis::Horizontal + } else { + PhysicalAxis::Vertical + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +#[repr(u8)] +pub enum LogicalSide { + BlockStart = 0, + BlockEnd, + InlineStart, + InlineEnd, +} + +impl LogicalSide { + fn is_block(self) -> bool { + matches!(self, Self::BlockStart | Self::BlockEnd) + } + + #[inline] + pub fn to_physical(self, wm: WritingMode) -> PhysicalSide { + // Block mapping depends only on vertical+vertical-lr + static BLOCK_MAPPING: [[PhysicalSide; 2]; 4] = [ + [PhysicalSide::Top, PhysicalSide::Bottom], // horizontal-tb + [PhysicalSide::Right, PhysicalSide::Left], // vertical-rl + [PhysicalSide::Bottom, PhysicalSide::Top], // (horizontal-bt) + [PhysicalSide::Left, PhysicalSide::Right], // vertical-lr + ]; + + if self.is_block() { + let vertical = wm.is_vertical(); + let lr = wm.is_vertical_lr(); + let index = (vertical as usize) | ((lr as usize) << 1); + return BLOCK_MAPPING[index][self as usize]; + } + + // start = 0, end = 1 + let edge = self as usize - 2; + // Inline axis sides depend on all three of writing-mode, text-orientation and direction, + // which are encoded in the VERTICAL, INLINE_REVERSED, VERTICAL_LR and LINE_INVERTED bits. + // + // bit 0 = the VERTICAL value + // bit 1 = the INLINE_REVERSED value + // bit 2 = the VERTICAL_LR value + // bit 3 = the LINE_INVERTED value + // + // Note that not all of these combinations can actually be specified via CSS: there is no + // horizontal-bt writing-mode, and no text-orientation value that produces "inverted" + // text. (The former 'sideways-left' value, no longer in the spec, would have produced + // this in vertical-rl mode.) + static INLINE_MAPPING: [[PhysicalSide; 2]; 16] = [ + [PhysicalSide::Left, PhysicalSide::Right], // horizontal-tb ltr + [PhysicalSide::Top, PhysicalSide::Bottom], // vertical-rl ltr + [PhysicalSide::Right, PhysicalSide::Left], // horizontal-tb rtl + [PhysicalSide::Bottom, PhysicalSide::Top], // vertical-rl rtl + [PhysicalSide::Right, PhysicalSide::Left], // (horizontal-bt) (inverted) ltr + [PhysicalSide::Top, PhysicalSide::Bottom], // sideways-lr rtl + [PhysicalSide::Left, PhysicalSide::Right], // (horizontal-bt) (inverted) rtl + [PhysicalSide::Bottom, PhysicalSide::Top], // sideways-lr ltr + [PhysicalSide::Left, PhysicalSide::Right], // horizontal-tb (inverted) rtl + [PhysicalSide::Top, PhysicalSide::Bottom], // vertical-rl (inverted) rtl + [PhysicalSide::Right, PhysicalSide::Left], // horizontal-tb (inverted) ltr + [PhysicalSide::Bottom, PhysicalSide::Top], // vertical-rl (inverted) ltr + [PhysicalSide::Left, PhysicalSide::Right], // (horizontal-bt) ltr + [PhysicalSide::Top, PhysicalSide::Bottom], // vertical-lr ltr + [PhysicalSide::Right, PhysicalSide::Left], // (horizontal-bt) rtl + [PhysicalSide::Bottom, PhysicalSide::Top], // vertical-lr rtl + ]; + + debug_assert!( + WritingMode::VERTICAL.bits() == 0x01 && + WritingMode::INLINE_REVERSED.bits() == 0x02 && + WritingMode::VERTICAL_LR.bits() == 0x04 && + WritingMode::LINE_INVERTED.bits() == 0x08 + ); + let index = (wm.bits() & 0xF) as usize; + INLINE_MAPPING[index][edge] + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +#[repr(u8)] +pub enum LogicalCorner { + StartStart = 0, + StartEnd, + EndStart, + EndEnd, +} + +impl LogicalCorner { + #[inline] + pub fn to_physical(self, wm: WritingMode) -> PhysicalCorner { + static CORNER_TO_SIDES: [[LogicalSide; 2]; 4] = [ + [LogicalSide::BlockStart, LogicalSide::InlineStart], + [LogicalSide::BlockStart, LogicalSide::InlineEnd], + [LogicalSide::BlockEnd, LogicalSide::InlineStart], + [LogicalSide::BlockEnd, LogicalSide::InlineEnd], + ]; + + let [block, inline] = CORNER_TO_SIDES[self as usize]; + let block = block.to_physical(wm); + let inline = inline.to_physical(wm); + PhysicalCorner::from_sides(block, inline) + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +#[repr(u8)] +pub enum PhysicalAxis { + Vertical = 0, + Horizontal, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +#[repr(u8)] +pub enum PhysicalSide { + Top = 0, + Right, + Bottom, + Left, +} + +impl PhysicalSide { + fn orthogonal_to(self, other: Self) -> bool { + matches!(self, Self::Top | Self::Bottom) != matches!(other, Self::Top | Self::Bottom) + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +#[repr(u8)] +pub enum PhysicalCorner { + TopLeft = 0, + TopRight, + BottomRight, + BottomLeft, +} + +impl PhysicalCorner { + fn from_sides(a: PhysicalSide, b: PhysicalSide) -> Self { + debug_assert!(a.orthogonal_to(b), "Sides should be orthogonal"); + // Only some of these are possible, since we expect only orthogonal values. If the two + // sides were to be parallel, we fall back to returning TopLeft. + const IMPOSSIBLE: PhysicalCorner = PhysicalCorner::TopLeft; + static SIDES_TO_CORNER: [[PhysicalCorner; 4]; 4] = [ + [ + IMPOSSIBLE, + PhysicalCorner::TopRight, + IMPOSSIBLE, + PhysicalCorner::TopLeft, + ], + [ + PhysicalCorner::TopRight, + IMPOSSIBLE, + PhysicalCorner::BottomRight, + IMPOSSIBLE, + ], + [ + IMPOSSIBLE, + PhysicalCorner::BottomRight, + IMPOSSIBLE, + PhysicalCorner::BottomLeft, + ], + [ + PhysicalCorner::TopLeft, + IMPOSSIBLE, + PhysicalCorner::BottomLeft, + IMPOSSIBLE, + ], + ]; + SIDES_TO_CORNER[a as usize][b as usize] + } +} diff --git a/servo/components/style/macros.rs b/servo/components/style/macros.rs new file mode 100644 index 0000000000..4795345185 --- /dev/null +++ b/servo/components/style/macros.rs @@ -0,0 +1,98 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Various macro helpers. + +macro_rules! exclusive_value { + (($value:ident, $set:expr) => $ident:path) => { + if $value.intersects($set) { + return Err(()); + } else { + $ident + } + }; +} + +#[cfg(feature = "gecko")] +macro_rules! impl_gecko_keyword_conversions { + ($name:ident, $utype:ty) => { + impl From<$utype> for $name { + fn from(bits: $utype) -> $name { + $name::from_gecko_keyword(bits) + } + } + + impl From<$name> for $utype { + fn from(v: $name) -> $utype { + v.to_gecko_keyword() + } + } + }; +} + +macro_rules! trivial_to_computed_value { + ($name:ty) => { + impl $crate::values::computed::ToComputedValue for $name { + type ComputedValue = $name; + + fn to_computed_value(&self, _: &$crate::values::computed::Context) -> Self { + self.clone() + } + + fn from_computed_value(other: &Self) -> Self { + other.clone() + } + } + }; +} + +/// A macro to parse an identifier, or return an `UnexpectedIdent` error +/// otherwise. +/// +/// FIXME(emilio): The fact that `UnexpectedIdent` is a `SelectorParseError` +/// doesn't make a lot of sense to me. +macro_rules! try_match_ident_ignore_ascii_case { + ($input:expr, $( $match_body:tt )*) => {{ + let location = $input.current_source_location(); + let ident = $input.expect_ident()?; + match_ignore_ascii_case! { &ident, + $( $match_body )* + _ => return Err(location.new_custom_error( + ::selectors::parser::SelectorParseErrorKind::UnexpectedIdent(ident.clone()) + )) + } + }} +} + +#[cfg(feature = "servo")] +macro_rules! local_name { + ($s:tt) => { + $crate::values::GenericAtomIdent(html5ever::local_name!($s)) + }; +} + +#[cfg(feature = "servo")] +macro_rules! ns { + () => { + $crate::values::GenericAtomIdent(html5ever::ns!()) + }; + ($s:tt) => { + $crate::values::GenericAtomIdent(html5ever::ns!($s)) + }; +} + +#[cfg(feature = "gecko")] +macro_rules! local_name { + ($s:tt) => { + $crate::values::AtomIdent(atom!($s)) + }; +} + +/// Asserts the size of a type at compile time. +macro_rules! size_of_test { + ($t: ty, $expected_size: expr) => { + #[cfg(target_pointer_width = "64")] + const_assert_eq!(std::mem::size_of::<$t>(), $expected_size); + }; +} diff --git a/servo/components/style/matching.rs b/servo/components/style/matching.rs new file mode 100644 index 0000000000..646d08a9e3 --- /dev/null +++ b/servo/components/style/matching.rs @@ -0,0 +1,1128 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! High-level interface to CSS selector matching. + +#![allow(unsafe_code)] +#![deny(missing_docs)] + +use crate::computed_value_flags::ComputedValueFlags; +use crate::context::{CascadeInputs, ElementCascadeInputs, QuirksMode}; +use crate::context::{SharedStyleContext, StyleContext}; +use crate::data::{ElementData, ElementStyles}; +use crate::dom::TElement; +#[cfg(feature = "servo")] +use crate::dom::TNode; +use crate::invalidation::element::restyle_hints::RestyleHint; +use crate::properties::longhands::display::computed_value::T as Display; +use crate::properties::ComputedValues; +use crate::properties::PropertyDeclarationBlock; +use crate::rule_tree::{CascadeLevel, StrongRuleNode}; +use crate::selector_parser::{PseudoElement, RestyleDamage}; +use crate::shared_lock::Locked; +use crate::style_resolver::ResolvedElementStyles; +use crate::style_resolver::{PseudoElementResolution, StyleResolverForElement}; +use crate::stylesheets::layer_rule::LayerOrder; +use crate::stylist::RuleInclusion; +use crate::traversal_flags::TraversalFlags; +use servo_arc::{Arc, ArcBorrow}; + +/// Represents the result of comparing an element's old and new style. +#[derive(Debug)] +pub struct StyleDifference { + /// The resulting damage. + pub damage: RestyleDamage, + + /// Whether any styles changed. + pub change: StyleChange, +} + +/// Represents whether or not the style of an element has changed. +#[derive(Clone, Copy, Debug)] +pub enum StyleChange { + /// The style hasn't changed. + Unchanged, + /// The style has changed. + Changed { + /// Whether only reset structs changed. + reset_only: bool, + }, +} + +/// Whether or not newly computed values for an element need to be cascaded to +/// children (or children might need to be re-matched, e.g., for container +/// queries). +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub enum ChildRestyleRequirement { + /// Old and new computed values were the same, or we otherwise know that + /// we won't bother recomputing style for children, so we can skip cascading + /// the new values into child elements. + CanSkipCascade = 0, + /// The same as `MustCascadeChildren`, but we only need to actually + /// recascade if the child inherits any explicit reset style. + MustCascadeChildrenIfInheritResetStyle = 1, + /// Old and new computed values were different, so we must cascade the + /// new values to children. + MustCascadeChildren = 2, + /// The same as `MustCascadeChildren`, but for the entire subtree. This is + /// used to handle root font-size updates needing to recascade the whole + /// document. + MustCascadeDescendants = 3, + /// We need to re-match the whole subttree. This is used to handle container + /// query relative unit changes for example. Container query size changes + /// also trigger re-match, but after layout. + MustMatchDescendants = 4, +} + +/// Determines which styles are being cascaded currently. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum CascadeVisitedMode { + /// Cascade the regular, unvisited styles. + Unvisited, + /// Cascade the styles used when an element's relevant link is visited. A + /// "relevant link" is the element being matched if it is a link or the + /// nearest ancestor link. + Visited, +} + +trait PrivateMatchMethods: TElement { + fn replace_single_rule_node( + context: &SharedStyleContext, + level: CascadeLevel, + layer_order: LayerOrder, + pdb: Option>>, + path: &mut StrongRuleNode, + ) -> bool { + let stylist = &context.stylist; + let guards = &context.guards; + + let mut important_rules_changed = false; + let new_node = stylist.rule_tree().update_rule_at_level( + level, + layer_order, + pdb, + path, + guards, + &mut important_rules_changed, + ); + if let Some(n) = new_node { + *path = n; + } + important_rules_changed + } + + /// Updates the rule nodes without re-running selector matching, using just + /// the rule tree, for a specific visited mode. + /// + /// Returns true if an !important rule was replaced. + fn replace_rules_internal( + &self, + replacements: RestyleHint, + context: &mut StyleContext, + cascade_visited: CascadeVisitedMode, + cascade_inputs: &mut ElementCascadeInputs, + ) -> bool { + debug_assert!( + replacements.intersects(RestyleHint::replacements()) && + (replacements & !RestyleHint::replacements()).is_empty() + ); + + let primary_rules = match cascade_visited { + CascadeVisitedMode::Unvisited => cascade_inputs.primary.rules.as_mut(), + CascadeVisitedMode::Visited => cascade_inputs.primary.visited_rules.as_mut(), + }; + + let primary_rules = match primary_rules { + Some(r) => r, + None => return false, + }; + + if !context.shared.traversal_flags.for_animation_only() { + let mut result = false; + if replacements.contains(RestyleHint::RESTYLE_STYLE_ATTRIBUTE) { + let style_attribute = self.style_attribute(); + result |= Self::replace_single_rule_node( + context.shared, + CascadeLevel::same_tree_author_normal(), + LayerOrder::root(), + style_attribute, + primary_rules, + ); + result |= Self::replace_single_rule_node( + context.shared, + CascadeLevel::same_tree_author_important(), + LayerOrder::root(), + style_attribute, + primary_rules, + ); + // FIXME(emilio): Still a hack! + self.unset_dirty_style_attribute(); + } + return result; + } + + // Animation restyle hints are processed prior to other restyle + // hints in the animation-only traversal. + // + // Non-animation restyle hints will be processed in a subsequent + // normal traversal. + if replacements.intersects(RestyleHint::for_animations()) { + debug_assert!(context.shared.traversal_flags.for_animation_only()); + + if replacements.contains(RestyleHint::RESTYLE_SMIL) { + Self::replace_single_rule_node( + context.shared, + CascadeLevel::SMILOverride, + LayerOrder::root(), + self.smil_override(), + primary_rules, + ); + } + + if replacements.contains(RestyleHint::RESTYLE_CSS_TRANSITIONS) { + Self::replace_single_rule_node( + context.shared, + CascadeLevel::Transitions, + LayerOrder::root(), + self.transition_rule(&context.shared) + .as_ref() + .map(|a| a.borrow_arc()), + primary_rules, + ); + } + + if replacements.contains(RestyleHint::RESTYLE_CSS_ANIMATIONS) { + Self::replace_single_rule_node( + context.shared, + CascadeLevel::Animations, + LayerOrder::root(), + self.animation_rule(&context.shared) + .as_ref() + .map(|a| a.borrow_arc()), + primary_rules, + ); + } + } + + false + } + + /// If there is no transition rule in the ComputedValues, it returns None. + fn after_change_style( + &self, + context: &mut StyleContext, + primary_style: &Arc, + ) -> Option> { + let rule_node = primary_style.rules(); + let without_transition_rules = context + .shared + .stylist + .rule_tree() + .remove_transition_rule_if_applicable(rule_node); + if without_transition_rules == *rule_node { + // We don't have transition rule in this case, so return None to let + // the caller use the original ComputedValues. + return None; + } + + // FIXME(bug 868975): We probably need to transition visited style as + // well. + let inputs = CascadeInputs { + rules: Some(without_transition_rules), + visited_rules: primary_style.visited_rules().cloned(), + flags: primary_style.flags.for_cascade_inputs(), + }; + + // Actually `PseudoElementResolution` doesn't really matter. + let style = StyleResolverForElement::new( + *self, + context, + RuleInclusion::All, + PseudoElementResolution::IfApplicable, + ) + .cascade_style_and_visited_with_default_parents(inputs); + + Some(style.0) + } + + fn needs_animations_update( + &self, + context: &mut StyleContext, + old_style: Option<&ComputedValues>, + new_style: &ComputedValues, + pseudo_element: Option, + ) -> bool { + let new_ui_style = new_style.get_ui(); + let new_style_specifies_animations = new_ui_style.specifies_animations(); + + let has_animations = self.has_css_animations(&context.shared, pseudo_element); + if !new_style_specifies_animations && !has_animations { + return false; + } + + let old_style = match old_style { + Some(old) => old, + // If we have no old style but have animations, we may be a + // pseudo-element which was re-created without style changes. + // + // This can happen when we reframe the pseudo-element without + // restyling it (due to content insertion on a flex container or + // such, for example). See bug 1564366. + // + // FIXME(emilio): The really right fix for this is keeping the + // pseudo-element itself around on reframes, but that's a bit + // harder. If we do that we can probably remove quite a lot of the + // EffectSet complexity though, since right now it's stored on the + // parent element for pseudo-elements given we need to keep it + // around... + None => { + return new_style_specifies_animations || new_style.is_pseudo_style(); + }, + }; + + let old_ui_style = old_style.get_ui(); + + let keyframes_could_have_changed = context + .shared + .traversal_flags + .contains(TraversalFlags::ForCSSRuleChanges); + + // If the traversal is triggered due to changes in CSS rules changes, we + // need to try to update all CSS animations on the element if the + // element has or will have CSS animation style regardless of whether + // the animation is running or not. + // + // TODO: We should check which @keyframes were added/changed/deleted and + // update only animations corresponding to those @keyframes. + if keyframes_could_have_changed { + return true; + } + + // If the animations changed, well... + if !old_ui_style.animations_equals(new_ui_style) { + return true; + } + + let old_display = old_style.clone_display(); + let new_display = new_style.clone_display(); + + // If we were display: none, we may need to trigger animations. + if old_display == Display::None && new_display != Display::None { + return new_style_specifies_animations; + } + + // If we are becoming display: none, we may need to stop animations. + if old_display != Display::None && new_display == Display::None { + return has_animations; + } + + // We might need to update animations if writing-mode or direction + // changed, and any of the animations contained logical properties. + // + // We may want to be more granular, but it's probably not worth it. + if new_style.writing_mode != old_style.writing_mode { + return has_animations; + } + + false + } + + fn might_need_transitions_update( + &self, + context: &StyleContext, + old_style: Option<&ComputedValues>, + new_style: &ComputedValues, + pseudo_element: Option, + ) -> bool { + let old_style = match old_style { + Some(v) => v, + None => return false, + }; + + if !self.has_css_transitions(context.shared, pseudo_element) && + !new_style.get_ui().specifies_transitions() + { + return false; + } + + if old_style.clone_display().is_none() { + return false; + } + + return true; + } + + /// Create a SequentialTask for resolving descendants in a SMIL display + /// property animation if the display property changed from none. + #[cfg(feature = "gecko")] + fn handle_display_change_for_smil_if_needed( + &self, + context: &mut StyleContext, + old_values: Option<&ComputedValues>, + new_values: &ComputedValues, + restyle_hints: RestyleHint, + ) { + use crate::context::PostAnimationTasks; + + if !restyle_hints.intersects(RestyleHint::RESTYLE_SMIL) { + return; + } + + if new_values.is_display_property_changed_from_none(old_values) { + // When display value is changed from none to other, we need to + // traverse descendant elements in a subsequent normal + // traversal (we can't traverse them in this animation-only restyle + // since we have no way to know whether the decendants + // need to be traversed at the beginning of the animation-only + // restyle). + let task = crate::context::SequentialTask::process_post_animation( + *self, + PostAnimationTasks::DISPLAY_CHANGED_FROM_NONE_FOR_SMIL, + ); + context.thread_local.tasks.push(task); + } + } + + #[cfg(feature = "gecko")] + fn process_animations( + &self, + context: &mut StyleContext, + old_styles: &mut ElementStyles, + new_styles: &mut ResolvedElementStyles, + restyle_hint: RestyleHint, + important_rules_changed: bool, + ) { + use crate::context::UpdateAnimationsTasks; + + let new_values = new_styles.primary_style_mut(); + let old_values = &old_styles.primary; + if context.shared.traversal_flags.for_animation_only() { + self.handle_display_change_for_smil_if_needed( + context, + old_values.as_deref(), + new_values, + restyle_hint, + ); + return; + } + + // Bug 868975: These steps should examine and update the visited styles + // in addition to the unvisited styles. + + let mut tasks = UpdateAnimationsTasks::empty(); + + if old_values.as_deref().map_or_else( + || new_values.get_ui().specifies_scroll_timelines(), + |old| !old.get_ui().scroll_timelines_equals(new_values.get_ui()), + ) { + tasks.insert(UpdateAnimationsTasks::SCROLL_TIMELINES); + } + + if old_values.as_deref().map_or_else( + || new_values.get_ui().specifies_view_timelines(), + |old| !old.get_ui().view_timelines_equals(new_values.get_ui()), + ) { + tasks.insert(UpdateAnimationsTasks::VIEW_TIMELINES); + } + + if self.needs_animations_update( + context, + old_values.as_deref(), + new_values, + /* pseudo_element = */ None, + ) { + tasks.insert(UpdateAnimationsTasks::CSS_ANIMATIONS); + } + + let before_change_style = if self.might_need_transitions_update( + context, + old_values.as_deref(), + new_values, + /* pseudo_element = */ None, + ) { + let after_change_style = + if self.has_css_transitions(context.shared, /* pseudo_element = */ None) { + self.after_change_style(context, new_values) + } else { + None + }; + + // In order to avoid creating a SequentialTask for transitions which + // may not be updated, we check it per property to make sure Gecko + // side will really update transition. + let needs_transitions_update = { + // We borrow new_values here, so need to add a scope to make + // sure we release it before assigning a new value to it. + let after_change_style_ref = after_change_style.as_ref().unwrap_or(&new_values); + + self.needs_transitions_update(old_values.as_ref().unwrap(), after_change_style_ref) + }; + + if needs_transitions_update { + if let Some(values_without_transitions) = after_change_style { + *new_values = values_without_transitions; + } + tasks.insert(UpdateAnimationsTasks::CSS_TRANSITIONS); + + // We need to clone old_values into SequentialTask, so we can + // use it later. + old_values.clone() + } else { + None + } + } else { + None + }; + + if self.has_animations(&context.shared) { + tasks.insert(UpdateAnimationsTasks::EFFECT_PROPERTIES); + if important_rules_changed { + tasks.insert(UpdateAnimationsTasks::CASCADE_RESULTS); + } + if new_values.is_display_property_changed_from_none(old_values.as_deref()) { + tasks.insert(UpdateAnimationsTasks::DISPLAY_CHANGED_FROM_NONE); + } + } + + if !tasks.is_empty() { + let task = crate::context::SequentialTask::update_animations( + *self, + before_change_style, + tasks, + ); + context.thread_local.tasks.push(task); + } + } + + #[cfg(feature = "servo")] + fn process_animations( + &self, + context: &mut StyleContext, + old_styles: &mut ElementStyles, + new_resolved_styles: &mut ResolvedElementStyles, + _restyle_hint: RestyleHint, + _important_rules_changed: bool, + ) { + use crate::animation::AnimationSetKey; + use crate::dom::TDocument; + + let style_changed = self.process_animations_for_style( + context, + &mut old_styles.primary, + new_resolved_styles.primary_style_mut(), + /* pseudo_element = */ None, + ); + + // If we have modified animation or transitions, we recascade style for this node. + if style_changed { + let mut rule_node = new_resolved_styles.primary_style().rules().clone(); + let declarations = context.shared.animations.get_all_declarations( + &AnimationSetKey::new_for_non_pseudo(self.as_node().opaque()), + context.shared.current_time_for_animations, + self.as_node().owner_doc().shared_lock(), + ); + Self::replace_single_rule_node( + &context.shared, + CascadeLevel::Transitions, + declarations.transitions.as_ref().map(|a| a.borrow_arc()), + &mut rule_node, + ); + Self::replace_single_rule_node( + &context.shared, + CascadeLevel::Animations, + declarations.animations.as_ref().map(|a| a.borrow_arc()), + &mut rule_node, + ); + + if rule_node != *new_resolved_styles.primary_style().rules() { + let inputs = CascadeInputs { + rules: Some(rule_node), + visited_rules: new_resolved_styles.primary_style().visited_rules().cloned(), + }; + + new_resolved_styles.primary.style = StyleResolverForElement::new( + *self, + context, + RuleInclusion::All, + PseudoElementResolution::IfApplicable, + ) + .cascade_style_and_visited_with_default_parents(inputs); + } + } + + self.process_animations_for_pseudo( + context, + old_styles, + new_resolved_styles, + PseudoElement::Before, + ); + self.process_animations_for_pseudo( + context, + old_styles, + new_resolved_styles, + PseudoElement::After, + ); + } + + #[cfg(feature = "servo")] + fn process_animations_for_pseudo( + &self, + context: &mut StyleContext, + old_styles: &mut ElementStyles, + new_resolved_styles: &mut ResolvedElementStyles, + pseudo_element: PseudoElement, + ) { + use crate::animation::AnimationSetKey; + use crate::dom::TDocument; + + let key = AnimationSetKey::new_for_pseudo(self.as_node().opaque(), pseudo_element.clone()); + let mut style = match new_resolved_styles.pseudos.get(&pseudo_element) { + Some(style) => Arc::clone(style), + None => { + context + .shared + .animations + .cancel_all_animations_for_key(&key); + return; + }, + }; + + let mut old_style = old_styles.pseudos.get(&pseudo_element).cloned(); + self.process_animations_for_style( + context, + &mut old_style, + &mut style, + Some(pseudo_element.clone()), + ); + + let declarations = context.shared.animations.get_all_declarations( + &key, + context.shared.current_time_for_animations, + self.as_node().owner_doc().shared_lock(), + ); + if declarations.is_empty() { + return; + } + + let mut rule_node = style.rules().clone(); + Self::replace_single_rule_node( + &context.shared, + CascadeLevel::Transitions, + LayerOrder::root(), + declarations.transitions.as_ref().map(|a| a.borrow_arc()), + &mut rule_node, + ); + Self::replace_single_rule_node( + &context.shared, + CascadeLevel::Animations, + LayerOrder::root(), + declarations.animations.as_ref().map(|a| a.borrow_arc()), + &mut rule_node, + ); + if rule_node == *style.rules() { + return; + } + + let inputs = CascadeInputs { + rules: Some(rule_node), + visited_rules: style.visited_rules().cloned(), + }; + + let new_style = StyleResolverForElement::new( + *self, + context, + RuleInclusion::All, + PseudoElementResolution::IfApplicable, + ) + .cascade_style_and_visited_for_pseudo_with_default_parents( + inputs, + &pseudo_element, + &new_resolved_styles.primary, + ); + + new_resolved_styles + .pseudos + .set(&pseudo_element, new_style.0); + } + + #[cfg(feature = "servo")] + fn process_animations_for_style( + &self, + context: &mut StyleContext, + old_values: &mut Option>, + new_values: &mut Arc, + pseudo_element: Option, + ) -> bool { + use crate::animation::{AnimationSetKey, AnimationState}; + + // We need to call this before accessing the `ElementAnimationSet` from the + // map because this call will do a RwLock::read(). + let needs_animations_update = self.needs_animations_update( + context, + old_values.as_deref(), + new_values, + pseudo_element, + ); + + let might_need_transitions_update = self.might_need_transitions_update( + context, + old_values.as_deref(), + new_values, + pseudo_element, + ); + + let mut after_change_style = None; + if might_need_transitions_update { + after_change_style = self.after_change_style(context, new_values); + } + + let key = AnimationSetKey::new(self.as_node().opaque(), pseudo_element); + let shared_context = context.shared; + let mut animation_set = shared_context + .animations + .sets + .write() + .remove(&key) + .unwrap_or_default(); + + // Starting animations is expensive, because we have to recalculate the style + // for all the keyframes. We only want to do this if we think that there's a + // chance that the animations really changed. + if needs_animations_update { + let mut resolver = StyleResolverForElement::new( + *self, + context, + RuleInclusion::All, + PseudoElementResolution::IfApplicable, + ); + + animation_set.update_animations_for_new_style::( + *self, + &shared_context, + &new_values, + &mut resolver, + ); + } + + animation_set.update_transitions_for_new_style( + might_need_transitions_update, + &shared_context, + old_values.as_ref(), + after_change_style.as_ref().unwrap_or(new_values), + ); + + // We clear away any finished transitions, but retain animations, because they + // might still be used for proper calculation of `animation-fill-mode`. This + // should change the computed values in the style, so we don't need to mark + // this set as dirty. + animation_set + .transitions + .retain(|transition| transition.state != AnimationState::Finished); + + // If the ElementAnimationSet is empty, and don't store it in order to + // save memory and to avoid extra processing later. + let changed_animations = animation_set.dirty; + if !animation_set.is_empty() { + animation_set.dirty = false; + shared_context + .animations + .sets + .write() + .insert(key, animation_set); + } + + changed_animations + } + + /// Computes and applies non-redundant damage. + fn accumulate_damage_for( + &self, + shared_context: &SharedStyleContext, + damage: &mut RestyleDamage, + old_values: &ComputedValues, + new_values: &ComputedValues, + pseudo: Option<&PseudoElement>, + ) -> ChildRestyleRequirement { + debug!("accumulate_damage_for: {:?}", self); + debug_assert!(!shared_context + .traversal_flags + .contains(TraversalFlags::FinalAnimationTraversal)); + + let difference = self.compute_style_difference(old_values, new_values, pseudo); + + *damage |= difference.damage; + + debug!(" > style difference: {:?}", difference); + + // We need to cascade the children in order to ensure the correct + // propagation of inherited computed value flags. + if old_values.flags.maybe_inherited() != new_values.flags.maybe_inherited() { + debug!( + " > flags changed: {:?} != {:?}", + old_values.flags, new_values.flags + ); + return ChildRestyleRequirement::MustCascadeChildren; + } + + if old_values.effective_zoom != new_values.effective_zoom { + // Zoom changes need to get propagated to children. + debug!( + " > zoom changed: {:?} != {:?}", + old_values.effective_zoom, new_values.effective_zoom + ); + return ChildRestyleRequirement::MustCascadeChildren; + } + + match difference.change { + StyleChange::Unchanged => return ChildRestyleRequirement::CanSkipCascade, + StyleChange::Changed { reset_only } => { + // If inherited properties changed, the best we can do is + // cascade the children. + if !reset_only { + return ChildRestyleRequirement::MustCascadeChildren; + } + }, + } + + let old_display = old_values.clone_display(); + let new_display = new_values.clone_display(); + + if old_display != new_display { + // If we used to be a display: none element, and no longer are, our + // children need to be restyled because they're unstyled. + if old_display == Display::None { + return ChildRestyleRequirement::MustCascadeChildren; + } + // Blockification of children may depend on our display value, + // so we need to actually do the recascade. We could potentially + // do better, but it doesn't seem worth it. + if old_display.is_item_container() != new_display.is_item_container() { + return ChildRestyleRequirement::MustCascadeChildren; + } + // We may also need to blockify and un-blockify descendants if our + // display goes from / to display: contents, since the "layout + // parent style" changes. + if old_display.is_contents() || new_display.is_contents() { + return ChildRestyleRequirement::MustCascadeChildren; + } + // Line break suppression may also be affected if the display + // type changes from ruby to non-ruby. + #[cfg(feature = "gecko")] + { + if old_display.is_ruby_type() != new_display.is_ruby_type() { + return ChildRestyleRequirement::MustCascadeChildren; + } + } + } + + // Children with justify-items: auto may depend on our + // justify-items property value. + // + // Similarly, we could potentially do better, but this really + // seems not common enough to care about. + #[cfg(feature = "gecko")] + { + use crate::values::specified::align::AlignFlags; + + let old_justify_items = old_values.get_position().clone_justify_items(); + let new_justify_items = new_values.get_position().clone_justify_items(); + + let was_legacy_justify_items = + old_justify_items.computed.0.contains(AlignFlags::LEGACY); + + let is_legacy_justify_items = new_justify_items.computed.0.contains(AlignFlags::LEGACY); + + if is_legacy_justify_items != was_legacy_justify_items { + return ChildRestyleRequirement::MustCascadeChildren; + } + + if was_legacy_justify_items && old_justify_items.computed != new_justify_items.computed + { + return ChildRestyleRequirement::MustCascadeChildren; + } + } + + #[cfg(feature = "servo")] + { + // We may need to set or propagate the CAN_BE_FRAGMENTED bit + // on our children. + if old_values.is_multicol() != new_values.is_multicol() { + return ChildRestyleRequirement::MustCascadeChildren; + } + } + + // We could prove that, if our children don't inherit reset + // properties, we can stop the cascade. + ChildRestyleRequirement::MustCascadeChildrenIfInheritResetStyle + } +} + +impl PrivateMatchMethods for E {} + +/// The public API that elements expose for selector matching. +pub trait MatchMethods: TElement { + /// Returns the closest parent element that doesn't have a display: contents + /// style (and thus generates a box). + /// + /// This is needed to correctly handle blockification of flex and grid + /// items. + /// + /// Returns itself if the element has no parent. In practice this doesn't + /// happen because the root element is blockified per spec, but it could + /// happen if we decide to not blockify for roots of disconnected subtrees, + /// which is a kind of dubious behavior. + fn layout_parent(&self) -> Self { + let mut current = self.clone(); + loop { + current = match current.traversal_parent() { + Some(el) => el, + None => return current, + }; + + let is_display_contents = current + .borrow_data() + .unwrap() + .styles + .primary() + .is_display_contents(); + + if !is_display_contents { + return current; + } + } + } + + /// Updates the styles with the new ones, diffs them, and stores the restyle + /// damage. + fn finish_restyle( + &self, + context: &mut StyleContext, + data: &mut ElementData, + mut new_styles: ResolvedElementStyles, + important_rules_changed: bool, + ) -> ChildRestyleRequirement { + use std::cmp; + + self.process_animations( + context, + &mut data.styles, + &mut new_styles, + data.hint, + important_rules_changed, + ); + + // First of all, update the styles. + let old_styles = data.set_styles(new_styles); + + let new_primary_style = data.styles.primary.as_ref().unwrap(); + + let mut restyle_requirement = ChildRestyleRequirement::CanSkipCascade; + let is_root = new_primary_style + .flags + .contains(ComputedValueFlags::IS_ROOT_ELEMENT_STYLE); + let is_container = !new_primary_style + .get_box() + .clone_container_type() + .is_normal(); + if is_root || is_container { + let device = context.shared.stylist.device(); + let old_style = old_styles.primary.as_ref(); + let new_font_size = new_primary_style.get_font().clone_font_size(); + let old_font_size = old_style.map(|s| s.get_font().clone_font_size()); + + if old_font_size != Some(new_font_size) { + if is_root { + debug_assert!(self.owner_doc_matches_for_testing(device)); + device.set_root_font_size(new_font_size.computed_size().into()); + if device.used_root_font_size() { + // If the root font-size changed since last time, and something + // in the document did use rem units, ensure we recascade the + // entire tree. + restyle_requirement = ChildRestyleRequirement::MustCascadeDescendants; + } + } + + if is_container && old_font_size.is_some() { + // TODO(emilio): Maybe only do this if we were matched + // against relative font sizes? + // Also, maybe we should do this as well for font-family / + // etc changes (for ex/ch/ic units to work correctly)? We + // should probably do the optimization mentioned above if + // so. + restyle_requirement = ChildRestyleRequirement::MustMatchDescendants; + } + } + + // For line-height, we want the fully resolved value, as `normal` also depends on other font properties. + let new_line_height = device + .calc_line_height( + &new_primary_style.get_font(), + new_primary_style.writing_mode, + None, + ) + .0; + let old_line_height = old_style.map(|s| { + device + .calc_line_height(&s.get_font(), s.writing_mode, None) + .0 + }); + + if restyle_requirement != ChildRestyleRequirement::MustMatchDescendants && + old_line_height != Some(new_line_height) + { + if is_root { + debug_assert!(self.owner_doc_matches_for_testing(device)); + device.set_root_line_height(new_line_height.into()); + if device.used_root_line_height() { + restyle_requirement = ChildRestyleRequirement::MustCascadeDescendants; + } + } + + if is_container && old_line_height.is_some() { + restyle_requirement = ChildRestyleRequirement::MustMatchDescendants; + } + } + } + + if context.shared.stylist.quirks_mode() == QuirksMode::Quirks { + if self.is_html_document_body_element() { + // NOTE(emilio): We _could_ handle dynamic changes to it if it + // changes and before we reach our children the cascade stops, + // but we don't track right now whether we use the document body + // color, and nobody else handles that properly anyway. + let device = context.shared.stylist.device(); + + // Needed for the "inherit from body" quirk. + let text_color = new_primary_style.get_inherited_text().clone_color(); + device.set_body_text_color(text_color); + } + } + + // Don't accumulate damage if we're in the final animation traversal. + if context + .shared + .traversal_flags + .contains(TraversalFlags::FinalAnimationTraversal) + { + return ChildRestyleRequirement::MustCascadeChildren; + } + + // Also, don't do anything if there was no style. + let old_primary_style = match old_styles.primary { + Some(s) => s, + None => return ChildRestyleRequirement::MustCascadeChildren, + }; + + let old_container_type = old_primary_style.clone_container_type(); + let new_container_type = new_primary_style.clone_container_type(); + if old_container_type != new_container_type && !new_container_type.is_size_container_type() + { + // Stopped being a size container. Re-evaluate container queries and units on all our descendants. + // Changes into and between different size containment is handled in `UpdateContainerQueryStyles`. + restyle_requirement = ChildRestyleRequirement::MustMatchDescendants; + } else if old_container_type.is_size_container_type() && + !old_primary_style.is_display_contents() && + new_primary_style.is_display_contents() + { + // Also re-evaluate when a container gets 'display: contents', since size queries will now evaluate to unknown. + // Other displays like 'inline' will keep generating a box, so they are handled in `UpdateContainerQueryStyles`. + restyle_requirement = ChildRestyleRequirement::MustMatchDescendants; + } + + restyle_requirement = cmp::max( + restyle_requirement, + self.accumulate_damage_for( + context.shared, + &mut data.damage, + &old_primary_style, + new_primary_style, + None, + ), + ); + + if data.styles.pseudos.is_empty() && old_styles.pseudos.is_empty() { + // This is the common case; no need to examine pseudos here. + return restyle_requirement; + } + + let pseudo_styles = old_styles + .pseudos + .as_array() + .iter() + .zip(data.styles.pseudos.as_array().iter()); + + for (i, (old, new)) in pseudo_styles.enumerate() { + match (old, new) { + (&Some(ref old), &Some(ref new)) => { + self.accumulate_damage_for( + context.shared, + &mut data.damage, + old, + new, + Some(&PseudoElement::from_eager_index(i)), + ); + }, + (&None, &None) => {}, + _ => { + // It's possible that we're switching from not having + // ::before/::after at all to having styles for them but not + // actually having a useful pseudo-element. Check for that + // case. + let pseudo = PseudoElement::from_eager_index(i); + let new_pseudo_should_exist = + new.as_ref().map_or(false, |s| pseudo.should_exist(s)); + let old_pseudo_should_exist = + old.as_ref().map_or(false, |s| pseudo.should_exist(s)); + if new_pseudo_should_exist != old_pseudo_should_exist { + data.damage |= RestyleDamage::reconstruct(); + return restyle_requirement; + } + }, + } + } + + restyle_requirement + } + + /// Updates the rule nodes without re-running selector matching, using just + /// the rule tree. + /// + /// Returns true if an !important rule was replaced. + fn replace_rules( + &self, + replacements: RestyleHint, + context: &mut StyleContext, + cascade_inputs: &mut ElementCascadeInputs, + ) -> bool { + let mut result = false; + result |= self.replace_rules_internal( + replacements, + context, + CascadeVisitedMode::Unvisited, + cascade_inputs, + ); + result |= self.replace_rules_internal( + replacements, + context, + CascadeVisitedMode::Visited, + cascade_inputs, + ); + result + } + + /// Given the old and new style of this element, and whether it's a + /// pseudo-element, compute the restyle damage used to determine which + /// kind of layout or painting operations we'll need. + fn compute_style_difference( + &self, + old_values: &ComputedValues, + new_values: &ComputedValues, + pseudo: Option<&PseudoElement>, + ) -> StyleDifference { + debug_assert!(pseudo.map_or(true, |p| p.is_eager())); + RestyleDamage::compute_style_difference(old_values, new_values) + } +} + +impl MatchMethods for E {} diff --git a/servo/components/style/media_queries/media_list.rs b/servo/components/style/media_queries/media_list.rs new file mode 100644 index 0000000000..3c2ba9ee5c --- /dev/null +++ b/servo/components/style/media_queries/media_list.rs @@ -0,0 +1,150 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! A media query list: +//! +//! https://drafts.csswg.org/mediaqueries/#typedef-media-query-list + +use super::{Device, MediaQuery, Qualifier}; +use crate::context::QuirksMode; +use crate::error_reporting::ContextualParseError; +use crate::parser::ParserContext; +use crate::queries::condition::KleeneValue; +use crate::values::computed; +use cssparser::{Delimiter, Parser}; +use cssparser::{ParserInput, Token}; + +/// A type that encapsulates a media query list. +#[derive(Clone, MallocSizeOf, ToCss, ToShmem)] +#[css(comma, derive_debug)] +pub struct MediaList { + /// The list of media queries. + #[css(iterable)] + pub media_queries: Vec, +} + +impl MediaList { + /// Parse a media query list from CSS. + /// + /// Always returns a media query list. If any invalid media query is + /// found, the media query list is only filled with the equivalent of + /// "not all", see: + /// + /// + pub fn parse(context: &ParserContext, input: &mut Parser) -> Self { + if input.is_exhausted() { + return Self::empty(); + } + + let mut media_queries = vec![]; + loop { + let start_position = input.position(); + match input.parse_until_before(Delimiter::Comma, |i| MediaQuery::parse(context, i)) { + Ok(mq) => { + media_queries.push(mq); + }, + Err(err) => { + media_queries.push(MediaQuery::never_matching()); + let location = err.location; + let error = ContextualParseError::InvalidMediaRule( + input.slice_from(start_position), + err, + ); + context.log_css_error(location, error); + }, + } + + match input.next() { + Ok(&Token::Comma) => {}, + Ok(_) => unreachable!(), + Err(_) => break, + } + } + + MediaList { media_queries } + } + + /// Create an empty MediaList. + pub fn empty() -> Self { + MediaList { + media_queries: vec![], + } + } + + /// Evaluate a whole `MediaList` against `Device`. + pub fn evaluate(&self, device: &Device, quirks_mode: QuirksMode) -> bool { + // Check if it is an empty media query list or any queries match. + // https://drafts.csswg.org/mediaqueries-4/#mq-list + if self.media_queries.is_empty() { + return true; + } + + computed::Context::for_media_query_evaluation(device, quirks_mode, |context| { + self.media_queries.iter().any(|mq| { + let mut query_match = if mq.media_type.matches(device.media_type()) { + mq.condition + .as_ref() + .map_or(KleeneValue::True, |c| c.matches(context)) + } else { + KleeneValue::False + }; + + // Apply the logical NOT qualifier to the result + if matches!(mq.qualifier, Some(Qualifier::Not)) { + query_match = !query_match; + } + query_match.to_bool(/* unknown = */ false) + }) + }) + } + + /// Whether this `MediaList` contains no media queries. + pub fn is_empty(&self) -> bool { + self.media_queries.is_empty() + } + + /// Whether this `MediaList` depends on the viewport size. + pub fn is_viewport_dependent(&self) -> bool { + self.media_queries.iter().any(|q| q.is_viewport_dependent()) + } + + /// Append a new media query item to the media list. + /// + /// + /// Returns true if added, false if fail to parse the medium string. + pub fn append_medium(&mut self, context: &ParserContext, new_medium: &str) -> bool { + let mut input = ParserInput::new(new_medium); + let mut parser = Parser::new(&mut input); + let new_query = match MediaQuery::parse(&context, &mut parser) { + Ok(query) => query, + Err(_) => { + return false; + }, + }; + // This algorithm doesn't actually matches the current spec, + // but it matches the behavior of Gecko and Edge. + // See https://github.com/w3c/csswg-drafts/issues/697 + self.media_queries.retain(|query| query != &new_query); + self.media_queries.push(new_query); + true + } + + /// Delete a media query from the media list. + /// + /// + /// Returns true if found and deleted, false otherwise. + pub fn delete_medium(&mut self, context: &ParserContext, old_medium: &str) -> bool { + let mut input = ParserInput::new(old_medium); + let mut parser = Parser::new(&mut input); + let old_query = match MediaQuery::parse(context, &mut parser) { + Ok(query) => query, + Err(_) => { + return false; + }, + }; + let old_len = self.media_queries.len(); + self.media_queries.retain(|query| query != &old_query); + old_len != self.media_queries.len() + } +} diff --git a/servo/components/style/media_queries/media_query.rs b/servo/components/style/media_queries/media_query.rs new file mode 100644 index 0000000000..c30a445393 --- /dev/null +++ b/servo/components/style/media_queries/media_query.rs @@ -0,0 +1,193 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! A media query: +//! +//! https://drafts.csswg.org/mediaqueries/#typedef-media-query + +use crate::parser::ParserContext; +use crate::queries::{FeatureFlags, FeatureType, QueryCondition}; +use crate::str::string_as_ascii_lowercase; +use crate::values::CustomIdent; +use crate::Atom; +use cssparser::Parser; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError, ToCss}; + +/// +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, ToCss, ToShmem)] +pub enum Qualifier { + /// Hide a media query from legacy UAs: + /// + Only, + /// Negate a media query: + /// + Not, +} + +/// +#[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)] +pub struct MediaType(pub CustomIdent); + +impl MediaType { + /// The `screen` media type. + pub fn screen() -> Self { + MediaType(CustomIdent(atom!("screen"))) + } + + /// The `print` media type. + pub fn print() -> Self { + MediaType(CustomIdent(atom!("print"))) + } + + fn parse(name: &str) -> Result { + // From https://drafts.csswg.org/mediaqueries/#mq-syntax: + // + // The production does not include the keywords only, not, and, or, and layer. + // + // Here we also perform the to-ascii-lowercase part of the serialization + // algorithm: https://drafts.csswg.org/cssom/#serializing-media-queries + match_ignore_ascii_case! { name, + "not" | "or" | "and" | "only" | "layer" => Err(()), + _ => Ok(MediaType(CustomIdent(Atom::from(string_as_ascii_lowercase(name))))), + } + } +} + +/// A [media query][mq]. +/// +/// [mq]: https://drafts.csswg.org/mediaqueries/ +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] +pub struct MediaQuery { + /// The qualifier for this query. + pub qualifier: Option, + /// The media type for this query, that can be known, unknown, or "all". + pub media_type: MediaQueryType, + /// The condition that this media query contains. This cannot have `or` + /// in the first level. + pub condition: Option, +} + +impl ToCss for MediaQuery { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: Write, + { + if let Some(qual) = self.qualifier { + qual.to_css(dest)?; + dest.write_char(' ')?; + } + + match self.media_type { + MediaQueryType::All => { + // We need to print "all" if there's a qualifier, or there's + // just an empty list of expressions. + // + // Otherwise, we'd serialize media queries like "(min-width: + // 40px)" in "all (min-width: 40px)", which is unexpected. + if self.qualifier.is_some() || self.condition.is_none() { + dest.write_str("all")?; + } + }, + MediaQueryType::Concrete(MediaType(ref desc)) => desc.to_css(dest)?, + } + + let condition = match self.condition { + Some(ref c) => c, + None => return Ok(()), + }; + + if self.media_type != MediaQueryType::All || self.qualifier.is_some() { + dest.write_str(" and ")?; + } + + condition.to_css(dest) + } +} + +impl MediaQuery { + /// Return a media query that never matches, used for when we fail to parse + /// a given media query. + pub fn never_matching() -> Self { + Self { + qualifier: Some(Qualifier::Not), + media_type: MediaQueryType::All, + condition: None, + } + } + + /// Returns whether this media query depends on the viewport. + pub fn is_viewport_dependent(&self) -> bool { + self.condition.as_ref().map_or(false, |c| { + return c + .cumulative_flags() + .contains(FeatureFlags::VIEWPORT_DEPENDENT); + }) + } + + /// Parse a media query given css input. + /// + /// Returns an error if any of the expressions is unknown. + pub fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + let (qualifier, explicit_media_type) = input + .try_parse(|input| -> Result<_, ()> { + let qualifier = input.try_parse(Qualifier::parse).ok(); + let ident = input.expect_ident().map_err(|_| ())?; + let media_type = MediaQueryType::parse(&ident)?; + Ok((qualifier, Some(media_type))) + }) + .unwrap_or_default(); + + let condition = if explicit_media_type.is_none() { + Some(QueryCondition::parse(context, input, FeatureType::Media)?) + } else if input.try_parse(|i| i.expect_ident_matching("and")).is_ok() { + Some(QueryCondition::parse_disallow_or( + context, + input, + FeatureType::Media, + )?) + } else { + None + }; + + let media_type = explicit_media_type.unwrap_or(MediaQueryType::All); + Ok(Self { + qualifier, + media_type, + condition, + }) + } +} + +/// +#[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)] +pub enum MediaQueryType { + /// A media type that matches every device. + All, + /// A specific media type. + Concrete(MediaType), +} + +impl MediaQueryType { + fn parse(ident: &str) -> Result { + match_ignore_ascii_case! { ident, + "all" => return Ok(MediaQueryType::All), + _ => (), + }; + + // If parseable, accept this type as a concrete type. + MediaType::parse(ident).map(MediaQueryType::Concrete) + } + + /// Returns whether this media query type matches a MediaType. + pub fn matches(&self, other: MediaType) -> bool { + match *self { + MediaQueryType::All => true, + MediaQueryType::Concrete(ref known_type) => *known_type == other, + } + } +} diff --git a/servo/components/style/media_queries/mod.rs b/servo/components/style/media_queries/mod.rs new file mode 100644 index 0000000000..833f6f53cb --- /dev/null +++ b/servo/components/style/media_queries/mod.rs @@ -0,0 +1,18 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! [Media queries][mq]. +//! +//! [mq]: https://drafts.csswg.org/mediaqueries/ + +mod media_list; +mod media_query; + +pub use self::media_list::MediaList; +pub use self::media_query::{MediaQuery, MediaQueryType, MediaType, Qualifier}; + +#[cfg(feature = "gecko")] +pub use crate::gecko::media_queries::Device; +#[cfg(feature = "servo")] +pub use crate::servo::media_queries::Device; diff --git a/servo/components/style/parallel.rs b/servo/components/style/parallel.rs new file mode 100644 index 0000000000..0b2ccf46d1 --- /dev/null +++ b/servo/components/style/parallel.rs @@ -0,0 +1,194 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Implements parallel traversal over the DOM tree. +//! +//! This traversal is based on Rayon, and therefore its safety is largely +//! verified by the type system. +//! +//! The primary trickiness and fine print for the above relates to the +//! thread safety of the DOM nodes themselves. Accessing a DOM element +//! concurrently on multiple threads is actually mostly "safe", since all +//! the mutable state is protected by an AtomicRefCell, and so we'll +//! generally panic if something goes wrong. Still, we try to to enforce our +//! thread invariants at compile time whenever possible. As such, TNode and +//! TElement are not Send, so ordinary style system code cannot accidentally +//! share them with other threads. In the parallel traversal, we explicitly +//! invoke |unsafe { SendNode::new(n) }| to put nodes in containers that may +//! be sent to other threads. This occurs in only a handful of places and is +//! easy to grep for. At the time of this writing, there is no other unsafe +//! code in the parallel traversal. + +#![deny(missing_docs)] + +use crate::context::{StyleContext, ThreadLocalStyleContext}; +use crate::dom::{OpaqueNode, SendNode, TElement}; +use crate::scoped_tls::ScopedTLS; +use crate::traversal::{DomTraversal, PerLevelTraversalData}; +use rayon; +use std::collections::VecDeque; + +/// 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; + +/// 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)] +pub(crate) fn create_thread_local_context<'scope, E>(slot: &mut Option>) +where + E: TElement + 'scope, +{ + *slot = Some(ThreadLocalStyleContext::new()); +} + +// Sends one chunk of work to the thread-pool. +fn distribute_one_chunk<'a, 'scope, E, D>( + items: VecDeque>, + traversal_root: OpaqueNode, + work_unit_max: usize, + traversal_data: PerLevelTraversalData, + scope: &'a rayon::ScopeFifo<'scope>, + traversal: &'scope D, + tls: &'scope ScopedTLS<'scope, ThreadLocalStyleContext>, +) where + E: TElement + 'scope, + D: DomTraversal, +{ + scope.spawn_fifo(move |scope| { + gecko_profiler_label!(Layout, StyleComputation); + let mut tlc = tls.ensure(create_thread_local_context); + let mut context = StyleContext { + shared: traversal.shared_context(), + thread_local: &mut *tlc, + }; + style_trees( + &mut context, + items, + traversal_root, + work_unit_max, + traversal_data, + Some(scope), + traversal, + tls, + ); + }) +} + +/// Distributes all items into the thread pool, in `work_unit_max` chunks. +fn distribute_work<'a, 'scope, E, D>( + mut items: VecDeque>, + traversal_root: OpaqueNode, + work_unit_max: usize, + traversal_data: PerLevelTraversalData, + scope: &'a rayon::ScopeFifo<'scope>, + traversal: &'scope D, + tls: &'scope ScopedTLS<'scope, ThreadLocalStyleContext>, +) where + E: TElement + 'scope, + D: DomTraversal, +{ + while items.len() > work_unit_max { + let rest = items.split_off(work_unit_max); + distribute_one_chunk( + items, + traversal_root, + work_unit_max, + traversal_data, + scope, + traversal, + tls, + ); + items = rest; + } + distribute_one_chunk( + items, + traversal_root, + work_unit_max, + traversal_data, + scope, + traversal, + tls, + ); +} + +/// Processes `discovered` items, possibly spawning work in other threads as needed. +#[inline] +pub fn style_trees<'a, 'scope, E, D>( + context: &mut StyleContext, + mut discovered: VecDeque>, + traversal_root: OpaqueNode, + work_unit_max: usize, + mut traversal_data: PerLevelTraversalData, + scope: Option<&'a rayon::ScopeFifo<'scope>>, + traversal: &'scope D, + tls: &'scope ScopedTLS<'scope, ThreadLocalStyleContext>, +) where + E: TElement + 'scope, + D: DomTraversal, +{ + let local_queue_size = if tls.current_thread_index() == 0 { + static_prefs::pref!("layout.css.stylo-local-work-queue.in-main-thread") + } else { + static_prefs::pref!("layout.css.stylo-local-work-queue.in-worker") + } as usize; + + let mut nodes_remaining_at_current_depth = discovered.len(); + while let Some(node) = discovered.pop_front() { + let mut children_to_process = 0isize; + traversal.process_preorder(&traversal_data, context, *node, |n| { + children_to_process += 1; + discovered.push_back(unsafe { SendNode::new(n) }); + }); + + traversal.handle_postorder_traversal(context, traversal_root, *node, children_to_process); + + nodes_remaining_at_current_depth -= 1; + + // If we have enough children at the next depth in the DOM, spawn them to a different job + // relatively soon, while keeping always at least `local_queue_size` worth of work for + // ourselves. + let discovered_children = discovered.len() - nodes_remaining_at_current_depth; + if discovered_children >= work_unit_max && + discovered.len() >= local_queue_size + work_unit_max && + scope.is_some() + { + let kept_work = std::cmp::max(nodes_remaining_at_current_depth, local_queue_size); + let mut traversal_data_copy = traversal_data.clone(); + traversal_data_copy.current_dom_depth += 1; + distribute_work( + discovered.split_off(kept_work), + traversal_root, + work_unit_max, + traversal_data_copy, + scope.unwrap(), + traversal, + tls, + ); + } + + if nodes_remaining_at_current_depth == 0 { + traversal_data.current_dom_depth += 1; + nodes_remaining_at_current_depth = discovered.len(); + } + } +} diff --git a/servo/components/style/parser.rs b/servo/components/style/parser.rs new file mode 100644 index 0000000000..893625854f --- /dev/null +++ b/servo/components/style/parser.rs @@ -0,0 +1,178 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! The context within which CSS code is parsed. + +use crate::context::QuirksMode; +use crate::error_reporting::{ContextualParseError, ParseErrorReporter}; +use crate::stylesheets::{CssRuleType, CssRuleTypes, Namespaces, Origin, UrlExtraData}; +use crate::use_counters::UseCounters; +use cssparser::{Parser, SourceLocation, UnicodeRange}; +use std::borrow::Cow; +use style_traits::{OneOrMoreSeparated, ParseError, ParsingMode, Separator}; + +/// The data that the parser needs from outside in order to parse a stylesheet. +pub struct ParserContext<'a> { + /// The `Origin` of the stylesheet, whether it's a user, author or + /// user-agent stylesheet. + pub stylesheet_origin: Origin, + /// The extra data we need for resolving url values. + pub url_data: &'a UrlExtraData, + /// The current rule types, if any. + pub rule_types: CssRuleTypes, + /// The mode to use when parsing. + pub parsing_mode: ParsingMode, + /// The quirks mode of this stylesheet. + pub quirks_mode: QuirksMode, + /// The active error reporter, or none if error reporting is disabled. + error_reporter: Option<&'a dyn ParseErrorReporter>, + /// The currently active namespaces. + pub namespaces: Cow<'a, Namespaces>, + /// The use counters we want to record while parsing style rules, if any. + pub use_counters: Option<&'a UseCounters>, +} + +impl<'a> ParserContext<'a> { + /// Create a parser context. + #[inline] + pub fn new( + stylesheet_origin: Origin, + url_data: &'a UrlExtraData, + rule_type: Option, + parsing_mode: ParsingMode, + quirks_mode: QuirksMode, + namespaces: Cow<'a, Namespaces>, + error_reporter: Option<&'a dyn ParseErrorReporter>, + use_counters: Option<&'a UseCounters>, + ) -> Self { + Self { + stylesheet_origin, + url_data, + rule_types: rule_type.map(CssRuleTypes::from).unwrap_or_default(), + parsing_mode, + quirks_mode, + error_reporter, + namespaces, + use_counters, + } + } + + /// Temporarily sets the rule_type and executes the callback function, returning its result. + pub fn nest_for_rule( + &mut self, + rule_type: CssRuleType, + cb: impl FnOnce(&mut Self) -> R, + ) -> R { + let old_rule_types = self.rule_types; + self.rule_types.insert(rule_type); + let r = cb(self); + self.rule_types = old_rule_types; + r + } + + /// Whether we're in a @page rule. + #[inline] + pub fn in_page_rule(&self) -> bool { + self.rule_types.contains(CssRuleType::Page) + } + + /// Get the rule type, which assumes that one is available. + pub fn rule_types(&self) -> CssRuleTypes { + self.rule_types + } + + /// Returns whether CSS error reporting is enabled. + #[inline] + pub fn error_reporting_enabled(&self) -> bool { + self.error_reporter.is_some() + } + + /// Record a CSS parse error with this context’s error reporting. + pub fn log_css_error(&self, location: SourceLocation, error: ContextualParseError) { + let error_reporter = match self.error_reporter { + Some(r) => r, + None => return, + }; + + error_reporter.report_error(self.url_data, location, error) + } + + /// Whether we're in a user-agent stylesheet. + #[inline] + pub fn in_ua_sheet(&self) -> bool { + self.stylesheet_origin == Origin::UserAgent + } + + /// Returns whether chrome-only rules should be parsed. + #[inline] + pub fn chrome_rules_enabled(&self) -> bool { + self.url_data.chrome_rules_enabled() || self.stylesheet_origin != Origin::Author + } +} + +/// A trait to abstract parsing of a specified value given a `ParserContext` and +/// CSS input. +/// +/// This can be derived on keywords with `#[derive(Parse)]`. +/// +/// The derive code understands the following attributes on each of the variants: +/// +/// * `#[parse(aliases = "foo,bar")]` can be used to alias a value with another +/// at parse-time. +/// +/// * `#[parse(condition = "function")]` can be used to make the parsing of the +/// value conditional on `function`, which needs to fulfill +/// `fn(&ParserContext) -> bool`. +pub trait Parse: Sized { + /// Parse a value of this type. + /// + /// Returns an error on failure. + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result>; +} + +impl Parse for Vec +where + T: Parse + OneOrMoreSeparated, + ::S: Separator, +{ + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + ::S::parse(input, |i| T::parse(context, i)) + } +} + +impl Parse for Box +where + T: Parse, +{ + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + T::parse(context, input).map(Box::new) + } +} + +impl Parse for crate::OwnedStr { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + Ok(input.expect_string()?.as_ref().to_owned().into()) + } +} + +impl Parse for UnicodeRange { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + Ok(UnicodeRange::parse(input)?) + } +} diff --git a/servo/components/style/piecewise_linear.rs b/servo/components/style/piecewise_linear.rs new file mode 100644 index 0000000000..1cabd01ea1 --- /dev/null +++ b/servo/components/style/piecewise_linear.rs @@ -0,0 +1,281 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! A piecewise linear function, following CSS linear easing +use crate::values::computed::Percentage; +use core::slice::Iter; +/// draft as in https://github.com/w3c/csswg-drafts/pull/6533. +use euclid::approxeq::ApproxEq; +use itertools::Itertools; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ToCss}; + +use crate::values::CSSFloat; + +type ValueType = CSSFloat; +/// a single entry in a piecewise linear function. +#[allow(missing_docs)] +#[derive( + Clone, + Copy, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToResolvedValue, + ToShmem, + Serialize, + Deserialize, +)] +#[repr(C)] +pub struct PiecewiseLinearFunctionEntry { + pub x: ValueType, + pub y: ValueType, +} + +impl ToCss for PiecewiseLinearFunctionEntry { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: fmt::Write, + { + self.y.to_css(dest)?; + dest.write_char(' ')?; + Percentage(self.x).to_css(dest) + } +} + +/// Representation of a piecewise linear function, a series of linear functions. +#[derive( + Default, + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToResolvedValue, + ToCss, + ToShmem, + Serialize, + Deserialize, +)] +#[repr(C)] +#[css(comma)] +pub struct PiecewiseLinearFunction { + #[css(iterable)] + #[ignore_malloc_size_of = "Arc"] + #[shmem(field_bound)] + entries: crate::ArcSlice, +} + +/// Parameters to define one linear stop. +pub type PiecewiseLinearFunctionBuildParameters = (CSSFloat, Option); + +impl PiecewiseLinearFunction { + /// Interpolate y value given x and two points. The linear function will be rooted at the asymptote. + fn interpolate( + x: ValueType, + prev: PiecewiseLinearFunctionEntry, + next: PiecewiseLinearFunctionEntry, + asymptote: &PiecewiseLinearFunctionEntry, + ) -> ValueType { + // Short circuit if the x is on prev or next. + // `next` point is preferred as per spec. + if x.approx_eq(&next.x) { + return next.y; + } + if x.approx_eq(&prev.x) { + return prev.y; + } + // Avoid division by zero. + if prev.x.approx_eq(&next.x) { + return next.y; + } + let slope = (next.y - prev.y) / (next.x - prev.x); + return slope * (x - asymptote.x) + asymptote.y; + } + + /// Get the y value of the piecewise linear function given the x value, as per + /// https://drafts.csswg.org/css-easing-2/#linear-easing-function-output + pub fn at(&self, x: ValueType) -> ValueType { + if !x.is_finite() { + return if x > 0.0 { 1.0 } else { 0.0 }; + } + if self.entries.is_empty() { + // Implied y = x, as per spec. + return x; + } + if self.entries.len() == 1 { + // Implied y = , as per spec. + return self.entries[0].y; + } + // Spec dictates the valid input domain is [0, 1]. Outside of this range, the output + // should be calculated as if the slopes at start and end extend to infinity. However, if the + // start/end have two points of the same position, the line should extend along the x-axis. + // The function doesn't have to cover the input domain, in which case the extension logic + // applies even if the input falls in the input domain. + // Also, we're guaranteed to have at least two elements at this point. + if x < self.entries[0].x { + return Self::interpolate(x, self.entries[0], self.entries[1], &self.entries[0]); + } + let mut rev_iter = self.entries.iter().rev(); + let last = rev_iter.next().unwrap(); + if x >= last.x { + let second_last = rev_iter.next().unwrap(); + return Self::interpolate(x, *second_last, *last, last); + } + + // Now we know the input sits within the domain explicitly defined by our function. + for (point_b, point_a) in self.entries.iter().rev().tuple_windows() { + // Need to let point A be the _last_ point where its x is less than the input x, + // hence the reverse traversal. + if x < point_a.x { + continue; + } + return Self::interpolate(x, *point_a, *point_b, point_a); + } + unreachable!("Input is supposed to be within the entries' min & max!"); + } + + #[allow(missing_docs)] + pub fn iter(&self) -> Iter { + self.entries.iter() + } +} + +/// Entry of a piecewise linear function while building, where the calculation of x value can be deferred. +#[derive(Clone, Copy)] +struct BuildEntry { + x: Option, + y: ValueType, +} + +/// Builder object to generate a linear function. +#[derive(Default)] +pub struct PiecewiseLinearFunctionBuilder { + largest_x: Option, + smallest_x: Option, + entries: Vec, +} + +impl PiecewiseLinearFunctionBuilder { + /// Create a builder for a known amount of linear stop entries. + pub fn with_capacity(len: usize) -> Self { + PiecewiseLinearFunctionBuilder { + largest_x: None, + smallest_x: None, + entries: Vec::with_capacity(len), + } + } + + fn create_entry(&mut self, y: ValueType, x: Option) { + let x = match x { + Some(x) if x.is_finite() => x, + _ if self.entries.is_empty() => 0.0, // First x is 0 if not specified (Or not finite) + _ => { + self.entries.push(BuildEntry { x: None, y }); + return; + }, + }; + // Specified x value cannot regress, as per spec. + let x = match self.largest_x { + Some(largest_x) => x.max(largest_x), + None => x, + }; + self.largest_x = Some(x); + // Whatever we see the earliest is the smallest value. + if self.smallest_x.is_none() { + self.smallest_x = Some(x); + } + self.entries.push(BuildEntry { x: Some(x), y }); + } + + /// Add a new entry into the piecewise linear function with specified y value. + /// If the start x value is given, that is where the x value will be. Otherwise, + /// the x value is calculated later. If the end x value is specified, a flat segment + /// is generated. If start x value is not specified but end x is, it is treated as + /// start x. + pub fn push(&mut self, y: CSSFloat, x_start: Option) { + self.create_entry(y, x_start) + } + + /// Finish building the piecewise linear function by resolving all undefined x values, + /// then return the result. + pub fn build(mut self) -> PiecewiseLinearFunction { + if self.entries.is_empty() { + return PiecewiseLinearFunction::default(); + } + if self.entries.len() == 1 { + // Don't bother resolving anything. + return PiecewiseLinearFunction { + entries: crate::ArcSlice::from_iter(std::iter::once( + PiecewiseLinearFunctionEntry { + x: 0., + y: self.entries[0].y, + }, + )), + }; + } + // Guaranteed at least two elements. + // Start element's x value should've been assigned when the first value was pushed. + debug_assert!( + self.entries[0].x.is_some(), + "Expected an entry with x defined!" + ); + // Spec asserts that if the last entry does not have an x value, it is assigned the largest seen x value. + self.entries + .last_mut() + .unwrap() + .x + .get_or_insert(self.largest_x.filter(|x| x > &1.0).unwrap_or(1.0)); + // Now we have at least two elements with x values, with start & end x values guaranteed. + + let mut result = Vec::with_capacity(self.entries.len()); + result.push(PiecewiseLinearFunctionEntry { + x: self.entries[0].x.unwrap(), + y: self.entries[0].y, + }); + for (i, e) in self.entries.iter().enumerate().skip(1) { + if e.x.is_none() { + // Need to calculate x values by first finding an entry with the first + // defined x value (Guaranteed to exist as the list end has it defined). + continue; + } + // x is defined for this element. + let divisor = i - result.len() + 1; + // Any element(s) with undefined x to assign? + if divisor != 1 { + // Have at least one element in result at all times. + let start_x = result.last().unwrap().x; + let increment = (e.x.unwrap() - start_x) / divisor as ValueType; + // Grab every element with undefined x to this point, which starts at the end of the result + // array, and ending right before the current index. Then, assigned the evenly divided + // x values. + result.extend( + self.entries[result.len()..i] + .iter() + .enumerate() + .map(|(j, e)| { + debug_assert!(e.x.is_none(), "Expected an entry with x undefined!"); + PiecewiseLinearFunctionEntry { + x: increment * (j + 1) as ValueType + start_x, + y: e.y, + } + }), + ); + } + result.push(PiecewiseLinearFunctionEntry { + x: e.x.unwrap(), + y: e.y, + }); + } + debug_assert_eq!( + result.len(), + self.entries.len(), + "Should've mapped one-to-one!" + ); + PiecewiseLinearFunction { + entries: crate::ArcSlice::from_iter(result.into_iter()), + } + } +} diff --git a/servo/components/style/properties/Mako-1.1.2-py2.py3-none-any.whl b/servo/components/style/properties/Mako-1.1.2-py2.py3-none-any.whl new file mode 100644 index 0000000000..9593025a47 Binary files /dev/null and b/servo/components/style/properties/Mako-1.1.2-py2.py3-none-any.whl differ diff --git a/servo/components/style/properties/build.py b/servo/components/style/properties/build.py new file mode 100644 index 0000000000..6c3ee0cf66 --- /dev/null +++ b/servo/components/style/properties/build.py @@ -0,0 +1,176 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +import json +import os.path +import re +import sys + +BASE = os.path.dirname(__file__.replace("\\", "/")) +sys.path.insert(0, os.path.join(BASE, "Mako-1.1.2-py2.py3-none-any.whl")) +sys.path.insert(0, BASE) # For importing `data.py` + +from mako import exceptions +from mako.lookup import TemplateLookup +from mako.template import Template + +import data + +RE_PYTHON_ADDR = re.compile(r"<.+? object at 0x[0-9a-fA-F]+>") + +OUT_DIR = os.environ.get("OUT_DIR", "") + +STYLE_STRUCT_LIST = [ + "background", + "border", + "box", + "column", + "counters", + "effects", + "font", + "inherited_box", + "inherited_svg", + "inherited_table", + "inherited_text", + "inherited_ui", + "list", + "margin", + "outline", + "page", + "padding", + "position", + "svg", + "table", + "text", + "ui", + "xul", +] + + +def main(): + usage = ( + "Usage: %s [ servo-2013 | servo-2020 | gecko ] [ style-crate | geckolib