From 4c17ed10e86df695ec3f3433db9818ba35fb9ae5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Sun, 20 Sep 2026 07:57:27 +0200 Subject: [PATCH 1/4] feat(input): add touch-action contact panning --- .../code-oss-css-touch-action-799.md | 41 ++++ .../CMakeLists.txt | 8 + .../native/css_property_metadata.json | 9 +- .../webscene_css_property_identity.inc | 6 + .../generated/webscene_css_property_ids.inc | 1 + .../webscene_css_supported_properties.inc | 3 +- .../native/webscene_css_application.h | 1 + .../native/webscene_css_cascade_reset.h | 1 + .../native/webscene_css_interaction_values.h | 60 ++++++ .../native/webscene_css_reset.h | 4 + .../native/webscene_css_specified_coverage.h | 1 + .../native/webscene_native_dom.h | 12 ++ .../native/webscene_native_engine.h | 24 ++- .../native/webscene_native_engine_input.inc | 1 + .../native/webscene_pointer_input.h | 66 ++++++ .../native/webscene_v8_runtime.cpp | 1 + .../webscene_v8_runtime_browser_apis.inc | 24 ++- .../native/webscene_v8_runtime_dom_core.inc | 14 +- .../native/webscene_v8_runtime_navigation.inc | 2 + .../native/webscene_v8_runtime_state.inc | 11 + .../native/webscene_v8_runtime_style.inc | 23 ++ .../native/webscene_v8_runtime_support.inc | 3 + .../native/webscene_v8_runtime_tasks.inc | 199 +++++++++++++++--- .../tests/css_property_identity_tests.cpp | 14 +- .../native_v8_runtime_css_layout_tests.inc | 58 +++++ .../tests/native_v8_runtime_input_tests.inc | 90 ++++++++ .../tests/native_v8_runtime_tests.cpp | 9 + .../tests/pointer_input_metadata_tests.cpp | 36 ++++ src/WebScene.Css/CssPropertyCatalog.cs | 3 + .../CssPropertyMetadata.Generated.cs | 3 + tests/WebPlatformSubset/capabilities.json | 8 + .../contracts/css-touch-action.html | 34 +++ .../webscene-component-profile.json | 7 + .../CssMutationInvalidationPlannerTests.cs | 1 + .../CssPropertyCatalogTests.cs | 4 + 35 files changed, 739 insertions(+), 43 deletions(-) create mode 100644 docs/validation/code-oss-css-touch-action-799.md create mode 100644 experiments/WebScene.NativeEngine.Probe/native/webscene_pointer_input.h create mode 100644 experiments/WebScene.NativeEngine.Probe/tests/pointer_input_metadata_tests.cpp create mode 100644 tests/WebPlatformSubset/contracts/css-touch-action.html diff --git a/docs/validation/code-oss-css-touch-action-799.md b/docs/validation/code-oss-css-touch-action-799.md new file mode 100644 index 000000000..7c67c3307 --- /dev/null +++ b/docs/validation/code-oss-css-touch-action-799.md @@ -0,0 +1,41 @@ +# Code OSS touch-action and contact pointer implementation (#799) + +## Product trigger + +Unchanged Code OSS authors 52 `touch-action` declarations across 27 files on +editors, tabs, split views, sliders, terminal, and Chat surfaces. WebScene +previously accepted only legacy mouse input and did not retain a native contact +gesture policy. + +## Implemented slice + +- Adds generated native/managed CSS identity and CSSOM for non-inherited + `touch-action`: `auto`, `none`, `manipulation`, `pan-x`, `pan-y`, and the + two-axis combination, including CSS-wide values, mutation, and removal. +- Decodes the versioned AppScene #298 pointer metadata from unused input flag + bits while preserving the 48-byte ABI and legacy primary-mouse behavior. +- Exposes stable 1..15 pointer IDs, mouse/touch/pen identity, primary state, + pointer capture by ID, and terminal `pointercancel` teardown. +- Freezes the intersected ancestor policy at contact start and performs + axis-qualified retained scrolling without synthetic mouse events. +- Uses a fixed 16-entry gesture array. CSS mutation is interaction-only, adds + no layout invalidation, document scan, scene node, timer, browser shell, or + Code OSS patch. + +## Authored gates + +- `webscene_pointer_input_metadata_tests`: legacy fallback, touch identity, + stable ID, malformed input, and future-version rejection. +- `contracts/css-touch-action.html`: non-inheritance, CSSOM, canonical axis + order, invalid-write preservation, CSS-wide inheritance, and removal. +- Native input contract: frozen `pan-y`, touch/pen event fields, capture, + cancellation, no mouse compatibility events, and `none` suppression. +- `css-touch-action-performance`: 4,096 nodes, 40,960 CSS mutations, 1,000 + gestures, no layout passes, bounded scene publication, no animation-frame + leak, and a five-second guardrail. + +## Evidence state + +Implementation and gates are authored from source review. Per the active fast +merge direction, no build, test, WPT, visual, package, memory, lifecycle, or +performance command was executed. Evidence remains zero until full validation. diff --git a/experiments/WebScene.NativeEngine.Probe/CMakeLists.txt b/experiments/WebScene.NativeEngine.Probe/CMakeLists.txt index 060a13495..f05ec85e3 100644 --- a/experiments/WebScene.NativeEngine.Probe/CMakeLists.txt +++ b/experiments/WebScene.NativeEngine.Probe/CMakeLists.txt @@ -5,6 +5,14 @@ include(CTest) include(FetchContent) if(BUILD_TESTING) + add_executable(webscene_pointer_input_metadata_tests + tests/pointer_input_metadata_tests.cpp) + target_compile_features(webscene_pointer_input_metadata_tests PRIVATE cxx_std_20) + target_include_directories(webscene_pointer_input_metadata_tests PRIVATE native) + add_test(NAME webscene_pointer_input_metadata_tests + COMMAND webscene_pointer_input_metadata_tests) + set_tests_properties(webscene_pointer_input_metadata_tests PROPERTIES + LABELS "native;input;pointer;abi;performance" TIMEOUT 10) add_executable(webscene_semantic_delta_header_tests tests/semantic_delta_header_tests.c) target_compile_features(webscene_semantic_delta_header_tests PRIVATE c_std_11) diff --git a/experiments/WebScene.NativeEngine.Probe/native/css_property_metadata.json b/experiments/WebScene.NativeEngine.Probe/native/css_property_metadata.json index 5291b7528..8387f38a8 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/css_property_metadata.json +++ b/experiments/WebScene.NativeEngine.Probe/native/css_property_metadata.json @@ -159,7 +159,8 @@ {"id": "isolation", "name": "isolation"}, {"id": "will_change", "name": "will-change", "aliases": ["willchange"]}, {"id": "text_wrap", "name": "text-wrap", "aliases": ["textwrap"]}, - {"id": "caret_color", "name": "caret-color", "aliases": ["caretcolor"]} + {"id": "caret_color", "name": "caret-color", "aliases": ["caretcolor"]}, + {"id": "touch_action", "name": "touch-action", "aliases": ["touchaction"]} ], "nativeStorageOnlyProperties": [ "accent-color", "align-content", "animation-direction", "animation-fill-mode", "animation-play-state", @@ -181,7 +182,7 @@ "justify_content", "box_sizing", "vertical_align", "grid_auto_flow", "border_collapse", "table_layout", "border_style", "background_repeat", "visibility", "pointer_events", "text_anchor", "font_smoothing", "text_align", "text_transform", "white_space", "word_break", "overflow_wrap", "list_style_position", "list_style_type", "scrollbar_width", "container", "container_type", "content_visibility", "object_fit", "user_select", - "overscroll_behavior", "overscroll_behavior_x", "overscroll_behavior_y", "isolation", "will_change", "text_wrap" + "overscroll_behavior", "overscroll_behavior_x", "overscroll_behavior_y", "isolation", "will_change", "text_wrap", "touch_action" ], "componentList": [ "container_name", "background_position", "background_size", "transition", "transition_property", "transition_duration", @@ -225,7 +226,7 @@ "outline", "outline_width", "outline_color", "animation", "animation_name", "animation_duration", "animation_delay", "animation_timing_function", "animation_iteration_count", "background_repeat", "background_position", "background_size", "font", "text_transform", "word_break", "overflow_wrap", "list_style", "list_style_position", "list_style_type", - "object_fit", "object_position", "user_select", "overscroll_behavior", "overscroll_behavior_x", "overscroll_behavior_y", "isolation", "will_change", "text_wrap", "caret_color" + "object_fit", "object_position", "user_select", "overscroll_behavior", "overscroll_behavior_x", "overscroll_behavior_y", "isolation", "will_change", "text_wrap", "caret_color", "touch_action" ], "managedKnownProperties": [ "align-content", "align-items", "align-self", "all", "background", "background-color", @@ -247,7 +248,7 @@ "top", "transform", "visibility", "white-space", "word-break", "overflow-wrap", "width", "word-spacing", "z-index", "outline", "outline-color", "outline-offset", "outline-style", "outline-width", "grid-template-areas", "aspect-ratio", "object-fit", "object-position", "user-select", - "overscroll-behavior", "overscroll-behavior-x", "overscroll-behavior-y", "isolation", "will-change", "text-wrap", "caret-color" + "overscroll-behavior", "overscroll-behavior-x", "overscroll-behavior-y", "isolation", "will-change", "text-wrap", "caret-color", "touch-action" ], "supportedPropertyExtras": [ "animation", "animation-delay", "animation-direction", "animation-duration", "animation-fill-mode", diff --git a/experiments/WebScene.NativeEngine.Probe/native/generated/webscene_css_property_identity.inc b/experiments/WebScene.NativeEngine.Probe/native/generated/webscene_css_property_identity.inc index 6e3ad9a6a..5401b57bd 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/generated/webscene_css_property_identity.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/generated/webscene_css_property_identity.inc @@ -238,6 +238,8 @@ inline constexpr std::array native_typed_property_identity_catalog{ native_typed_property_identity{"textwrap", css_property_id::text_wrap}, native_typed_property_identity{"caret-color", css_property_id::caret_color}, native_typed_property_identity{"caretcolor", css_property_id::caret_color}, + native_typed_property_identity{"touch-action", css_property_id::touch_action}, + native_typed_property_identity{"touchaction", css_property_id::touch_action}, }; inline constexpr std::array native_storage_only_property_catalog{ @@ -465,6 +467,7 @@ inline constexpr std::array native_modeled_property_mask_catalog{ false, false, false, + false, }; inline constexpr bool generated_property_has_modeled_mask( @@ -673,6 +676,7 @@ inline constexpr std::array native_property_grammar_catalog{ native_property_grammar::keyword, native_property_grammar::keyword, native_property_grammar::complex, + native_property_grammar::keyword, }; inline constexpr native_property_grammar generated_property_grammar( @@ -919,5 +923,7 @@ inline css_property_id generated_property_id_lowercase(std::string_view name) no if (name == "textwrap") return css_property_id::text_wrap; if (name == "caret-color") return css_property_id::caret_color; if (name == "caretcolor") return css_property_id::caret_color; + if (name == "touch-action") return css_property_id::touch_action; + if (name == "touchaction") return css_property_id::touch_action; return css_property_id::unknown; } diff --git a/experiments/WebScene.NativeEngine.Probe/native/generated/webscene_css_property_ids.inc b/experiments/WebScene.NativeEngine.Probe/native/generated/webscene_css_property_ids.inc index 17d75b74c..0fcd3b1f4 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/generated/webscene_css_property_ids.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/generated/webscene_css_property_ids.inc @@ -158,3 +158,4 @@ isolation, will_change, text_wrap, caret_color, +touch_action, diff --git a/experiments/WebScene.NativeEngine.Probe/native/generated/webscene_css_supported_properties.inc b/experiments/WebScene.NativeEngine.Probe/native/generated/webscene_css_supported_properties.inc index 54ccd36a5..3c7cf9f39 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/generated/webscene_css_supported_properties.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/generated/webscene_css_supported_properties.inc @@ -239,6 +239,7 @@ inline constexpr std::array cssom_supported_property_catalog{ cssom_supported_property_metadata{"text-transform", "textTransform"}, cssom_supported_property_metadata{"text-wrap", "textWrap"}, cssom_supported_property_metadata{"top", "top"}, + cssom_supported_property_metadata{"touch-action", "touchAction"}, cssom_supported_property_metadata{"transform", "transform"}, cssom_supported_property_metadata{"transform-origin", "transformOrigin"}, cssom_supported_property_metadata{"transition", "transition"}, @@ -263,4 +264,4 @@ inline constexpr std::array cssom_supported_property_catalog{ cssom_supported_property_metadata{"zoom", "zoom"}, }; -inline constexpr auto cssom_style_template_property_accessor_count = 468U; +inline constexpr auto cssom_style_template_property_accessor_count = 470U; diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_application.h b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_application.h index 1661e5663..c030924d7 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_application.h +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_application.h @@ -260,6 +260,7 @@ void apply_resolved_declaration(native_document& document,dom_node& node, } else if (css::apply_replaced_value(node,name,value,decision)) { } else if (css::apply_user_select_value(node,name,value,decision)) { } else if (css::apply_overscroll_behavior_value(node,name,value,decision)) { + } else if (css::apply_touch_action_value(node,name,value,decision)) { } else if (css::apply_isolation_value(node,name,value,decision)) { } else if (css::apply_will_change_value(node,name,value,decision)) { } else if (css::apply_text_value(node,name,value,decision,is_inline)) { diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_cascade_reset.h b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_cascade_reset.h index 2ebf46d77..e2b347ad2 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_cascade_reset.h +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_cascade_reset.h @@ -233,6 +233,7 @@ inline void reset_cascaded_style(dom_node& node, textual->will_change.clear(); textual->overscroll_x = overscroll_behavior::automatic; textual->overscroll_y = overscroll_behavior::automatic; + textual->touch_action_value = touch_action::automatic; } if ((node.style.inline_property_mask & inline_font_weight) == 0U) node.style.font_weight = 0; node.style.isolation_stacking_context = false; diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_interaction_values.h b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_interaction_values.h index a260b284d..0520291e0 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_interaction_values.h +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_interaction_values.h @@ -5,6 +5,66 @@ #include namespace webscene_native::css { +inline std::string_view serialize_touch_action(touch_action value) noexcept +{ + switch (value) { + case touch_action::none: return "none"; + case touch_action::manipulation: return "manipulation"; + case touch_action::pan_x: return "pan-x"; + case touch_action::pan_y: return "pan-y"; + case touch_action::pan_x_y: return "pan-x pan-y"; + default: return "auto"; + } +} + +inline uint8_t touch_action_axes(touch_action value) noexcept +{ + switch (value) { + case touch_action::none: return 0U; + case touch_action::pan_x: return 1U; + case touch_action::pan_y: return 2U; + default: return 3U; + } +} + +inline std::optional parse_touch_action( + std::string_view value) noexcept +{ + if (value == "auto") return touch_action::automatic; + if (value == "none") return touch_action::none; + if (value == "manipulation") return touch_action::manipulation; + if (value == "pan-x") return touch_action::pan_x; + if (value == "pan-y") return touch_action::pan_y; + if (value == "pan-x pan-y" || value == "pan-y pan-x") + return touch_action::pan_x_y; + return std::nullopt; +} + +template +bool apply_touch_action_value(dom_node& node,const std::string& name, + const std::string& raw_value,Decision& decision) +{ + if (canonical_property_name(name) != "touch-action") return false; + auto value = ascii_lower(trim_value(raw_value)); + if (value == "inherit") { + node.style.mutable_textual().touch_action_value = node.parent == nullptr + ? touch_action::automatic + : node.parent->style.textual().touch_action_value; + return true; + } + if (value == "initial" || value == "unset" || value == "revert" + || value == "revert-layer") value = "auto"; + const auto parsed = parse_touch_action(value); + if (!parsed.has_value()) { + decision.classification = "invalid-authoring"; + return true; + } + if (*parsed == touch_action::automatic && !node.style.has_textual_data()) + return true; + node.style.mutable_textual().touch_action_value = *parsed; + return true; +} + template bool apply_user_select_value(dom_node& node,const std::string& name, const std::string& raw_value,Decision& decision) diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_reset.h b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_reset.h index abd508f35..2a6485e89 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_reset.h +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_reset.h @@ -251,6 +251,10 @@ inline void apply_all_unset( if (has_inline({"overscroll-behavior", "overscroll-behavior-y"})) { reset.mutable_textual().overscroll_y = previous.textual().overscroll_y; } + if (has_inline({"touch-action"})) { + reset.mutable_textual().touch_action_value = + previous.textual().touch_action_value; + } if (has_inline({"isolation"})) { reset.isolation_stacking_context = previous.isolation_stacking_context; } diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_specified_coverage.h b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_specified_coverage.h index 57d5df2e2..95fc4f36c 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_specified_coverage.h +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_specified_coverage.h @@ -15,6 +15,7 @@ inline constexpr auto specified_property_samples = std::to_array +#include + +namespace webscene_native { + +struct pointer_input_metadata final { + webscene_pointer_device_kind device{WEBSCENE_POINTER_DEVICE_MOUSE}; + std::uint32_t pointer_id{1U}; + bool primary{true}; + bool legacy{}; +}; + +inline std::optional decode_pointer_input_metadata( + std::uint32_t flags) noexcept +{ + const auto version = (flags & WEBSCENE_INPUT_POINTER_METADATA_VERSION_MASK) + >> WEBSCENE_INPUT_POINTER_METADATA_VERSION_SHIFT; + if (version == 0U) { + return pointer_input_metadata{ + WEBSCENE_POINTER_DEVICE_MOUSE, 1U, true, true}; + } + if (version != 1U) return std::nullopt; + const auto device = (flags & WEBSCENE_INPUT_POINTER_DEVICE_MASK) + >> WEBSCENE_INPUT_POINTER_DEVICE_SHIFT; + const auto pointer_id = (flags & WEBSCENE_INPUT_POINTER_ID_MASK) + >> WEBSCENE_INPUT_POINTER_ID_SHIFT; + if (device > WEBSCENE_POINTER_DEVICE_PEN || pointer_id == 0U) + return std::nullopt; + return pointer_input_metadata{ + static_cast(device), + pointer_id, + (flags & WEBSCENE_INPUT_POINTER_PRIMARY) != 0U, + false}; +} + +inline const char* pointer_device_name( + webscene_pointer_device_kind device) noexcept +{ + switch (device) { + case WEBSCENE_POINTER_DEVICE_TOUCH: return "touch"; + case WEBSCENE_POINTER_DEVICE_PEN: return "pen"; + default: return "mouse"; + } +} + +inline constexpr bool is_contact_pointer( + webscene_pointer_device_kind device) noexcept +{ + return device == WEBSCENE_POINTER_DEVICE_TOUCH + || device == WEBSCENE_POINTER_DEVICE_PEN; +} + +static_assert((WEBSCENE_INPUT_POINTER_METADATA_VERSION_MASK + | WEBSCENE_INPUT_POINTER_DEVICE_MASK + | WEBSCENE_INPUT_POINTER_PRIMARY + | WEBSCENE_INPUT_POINTER_ID_MASK) == 0xff800000U); +static_assert(((WEBSCENE_INPUT_POINTER_METADATA_VERSION_MASK + | WEBSCENE_INPUT_POINTER_DEVICE_MASK + | WEBSCENE_INPUT_POINTER_PRIMARY + | WEBSCENE_INPUT_POINTER_ID_MASK) & 0x007fffffU) == 0U); + +} // namespace webscene_native diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime.cpp b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime.cpp index b9f1cbe95..978958963 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime.cpp +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime.cpp @@ -12,6 +12,7 @@ #include "graphics/v8_webgpu_canvas_context.h" #endif #include "webscene_v8_runtime.h" +#include "webscene_pointer_input.h" #include "webscene_precompiled_javascript.h" #include "webscene_frame_trace.h" #if defined(WEBSCENE_NATIVE_ENGINE_ENABLE_GRAPHICS) diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_browser_apis.inc b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_browser_apis.inc index e303c06ad..efc86d1bf 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_browser_apis.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_browser_apis.inc @@ -846,22 +846,42 @@ { auto* self = current(info.GetIsolate()); auto* node = unwrap_node(info.This()); - if (node != nullptr) self->pointer_capture_target = node; + const auto pointer_id = info.Length() > 0 + ? info[0]->Uint32Value(info.GetIsolate()->GetCurrentContext()) + .FromMaybe(0U) + : 0U; + if (node != nullptr && pointer_id > 0U && pointer_id <= 15U) { + self->pointer_capture_target = node; + self->pointer_capture_id = pointer_id; + } } static void release_pointer_capture(const v8::FunctionCallbackInfo& info) { auto* self = current(info.GetIsolate()); auto* node = unwrap_node(info.This()); - if (self->pointer_capture_target == node) self->pointer_capture_target = nullptr; + const auto pointer_id = info.Length() > 0 + ? info[0]->Uint32Value(info.GetIsolate()->GetCurrentContext()) + .FromMaybe(0U) + : 0U; + if (self->pointer_capture_target == node + && self->pointer_capture_id == pointer_id) { + self->pointer_capture_target = nullptr; + self->pointer_capture_id = 0U; + } } static void has_pointer_capture(const v8::FunctionCallbackInfo& info) { auto* self = current(info.GetIsolate()); auto* node = unwrap_node(info.This()); + const auto pointer_id = info.Length() > 0 + ? info[0]->Uint32Value(info.GetIsolate()->GetCurrentContext()) + .FromMaybe(0U) + : 0U; info.GetReturnValue().Set( self->pointer_capture_target == node + && self->pointer_capture_id == pointer_id ? v8::True(info.GetIsolate()) : v8::False(info.GetIsolate())); } diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_dom_core.inc b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_dom_core.inc index 058741fb7..e2188cca7 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_dom_core.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_dom_core.inc @@ -723,7 +723,8 @@ { if (canonical_property_name == "overscroll-behavior" || canonical_property_name == "overscroll-behavior-x" - || canonical_property_name == "overscroll-behavior-y") { + || canonical_property_name == "overscroll-behavior-y" + || canonical_property_name == "touch-action") { return; } // These properties affect painting or interaction, but never box @@ -2104,7 +2105,16 @@ }); } if (nodes.contains(active_element)) active_element = nullptr; - if (nodes.contains(pointer_capture_target)) pointer_capture_target = nullptr; + if (nodes.contains(pointer_capture_target)) { + pointer_capture_target = nullptr; + pointer_capture_id = 0U; + } + for (auto& gesture : touch_pan_gestures) { + if (node_ids.contains(gesture.horizontal_target_id)) + gesture.horizontal_target_id = 0U; + if (node_ids.contains(gesture.vertical_target_id)) + gesture.vertical_target_id = 0U; + } if (nodes.contains(pointer_down_target)) pointer_down_target = nullptr; if (middle_paste_target_id != 0U && node_ids.contains(middle_paste_target_id)) { diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_navigation.inc b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_navigation.inc index ffbb8c1e4..431d0707c 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_navigation.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_navigation.inc @@ -290,6 +290,8 @@ canvas_states.clear(); pointer_capture_target = nullptr; + pointer_capture_id = 0U; + touch_pan_gestures = {}; pointer_down_target = nullptr; middle_paste_target_id = 0U; active_pointer_target = nullptr; diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_state.inc b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_state.inc index 8910b296b..5eb9e298e 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_state.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_state.inc @@ -727,7 +727,18 @@ uint32_t persistent_writes_since_prune{0}; uint64_t input_event_dispatch_count{0}; uint64_t input_callback_invocation_count{0}; + struct touch_pan_gesture final { + uint32_t horizontal_target_id{0U}; + uint32_t vertical_target_id{0U}; + double last_x{0}; + double last_y{0}; + bool active{false}; + }; + // Pointer IDs are ABI-bounded to 1..15. Fixed per-contact gesture state + // keeps panning and teardown free of allocation and hash scans. + std::array touch_pan_gestures{}; dom_node* pointer_capture_target{nullptr}; + uint32_t pointer_capture_id{0U}; dom_node* pointer_down_target{nullptr}; uint32_t pointer_selection_root_id{0U}; bool pointer_text_selection_active{false}; diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_style.inc b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_style.inc index 6336a8665..dc5ccf78a 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_style.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_style.inc @@ -302,6 +302,10 @@ textual->overscroll_y = overscroll_behavior::automatic; } } + } else if (name == "touchAction" || name == "touch-action") { + if (auto* textual = node.style.mutable_textual_if_present()) { + textual->touch_action_value = touch_action::automatic; + } } else if (name == "isolation") { node.style.isolation_stacking_context = false; } else if (name == "willChange" || name == "will-change") { @@ -1032,6 +1036,12 @@ std::string semantic_slice; } decision; css::apply_overscroll_behavior_value(*node, name, value, decision); + } else if (name == "touch-action") { + struct interaction_decision final { + std::string classification{"supported"}; + std::string semantic_slice; + } decision; + css::apply_touch_action_value(*node, name, value, decision); } else if (name == "isolation") { struct interaction_decision final { std::string classification{"supported"}; @@ -1681,6 +1691,9 @@ } else if (name == "overscroll-behavior-y") { value = css::serialize_overscroll_behavior( node->style.textual().overscroll_y); + } else if (name == "touch-action") { + value = css::serialize_touch_action( + node->style.textual().touch_action_value); } else if (name == "isolation") { value = node->style.isolation_stacking_context ? "isolate" : "auto"; } else if (name == "will-change") { @@ -2171,6 +2184,9 @@ } else if (name == "overscrollBehaviorY") { value = css::serialize_overscroll_behavior( node->style.textual().overscroll_y); + } else if (name == "touchAction") { + value = css::serialize_touch_action( + node->style.textual().touch_action_value); } else if (name == "isolation") { value = node->style.isolation_stacking_context ? "isolate" : "auto"; } else if (name == "willChange") { @@ -2787,6 +2803,13 @@ ? std::string{"overscroll-behavior-x"} : std::string{"overscroll-behavior-y"}; css::apply_overscroll_behavior_value(*node, property, value, decision); + } else if (name == "touchAction") { + struct interaction_decision final { + std::string classification{"supported"}; + std::string semantic_slice; + } decision; + css::apply_touch_action_value( + *node, std::string{"touch-action"}, value, decision); } else if (name == "isolation") { struct interaction_decision final { std::string classification{"supported"}; diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_support.inc b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_support.inc index 1fb8f3156..57da31fc3 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_support.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_support.inc @@ -688,6 +688,9 @@ bool valid_cssom_declaration_value(std::string_view property_name, std::string_v if (canonical_name == "text-wrap") { return normalized_value == "wrap" || normalized_value == "nowrap"; } + if (canonical_name == "touch-action") { + return css::parse_touch_action(normalized_value).has_value(); + } if (canonical_name == "caret-color") { const auto compiled = css::compile_specified_value( css_property_id::caret_color, trimmed); diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_tasks.inc b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_tasks.inc index 2ae7b6c5e..5c221efbb 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_tasks.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_tasks.inc @@ -1938,8 +1938,16 @@ ? v8::Local(v8::Null(isolate)) : v8::Local(wrap_node(*submitter))).Check(); } - event->Set(local_context, js_string(isolate, "pointerType"), js_string(isolate, "mouse")).Check(); - event->Set(local_context, js_string(isolate, "isPrimary"), v8::True(isolate)).Check(); + const auto pointer_metadata = decode_pointer_input_metadata(input.flags) + .value_or(pointer_input_metadata{}); + event->Set( + local_context, + js_string(isolate, "pointerType"), + js_string(isolate, pointer_device_name(pointer_metadata.device))).Check(); + event->Set( + local_context, + js_string(isolate, "isPrimary"), + v8::Boolean::New(isolate, pointer_metadata.primary)).Check(); event->Set( local_context, js_string(isolate, "relatedTarget"), @@ -1954,6 +1962,7 @@ || input.kind == WEBSCENE_INPUT_POINTER_DOWN || input.kind == WEBSCENE_INPUT_POINTER_UP || input.kind == WEBSCENE_INPUT_POINTER_LEAVE + || input.kind == WEBSCENE_INPUT_POINTER_CANCEL || input.kind == WEBSCENE_INPUT_WHEEL; if (pointer_input) { // The native host reports coordinates in the top-level viewport. @@ -2026,7 +2035,7 @@ const auto buttons = static_cast(input.flags & 0x1fU); set_number("button", changed_button); set_number("buttons", buttons); - set_number("pointerId", 1); + set_number("pointerId", pointer_metadata.pointer_id); const auto mouse_click_sequence_event = std::string_view(type) == "mousedown" || std::string_view(type) == "mouseup" || std::string_view(type) == "click" @@ -3888,6 +3897,92 @@ selection_paint_changed(); } + void begin_touch_pan_gesture( + const pointer_input_metadata& metadata, + const webscene_input_event& input, + dom_node& hit) + { + if (!is_contact_pointer(metadata.device) + || metadata.pointer_id >= touch_pan_gestures.size()) return; + auto& gesture = touch_pan_gestures[metadata.pointer_id]; + gesture = {}; + gesture.active = true; + gesture.last_x = input.x; + gesture.last_y = input.y; + bool seek_horizontal = true; + bool seek_vertical = true; + for (auto* node = &hit; node != nullptr + && (seek_horizontal || seek_vertical); node = node->parent) { + const auto axes = css::touch_action_axes( + node->style.textual().touch_action_value); + if (seek_horizontal && (axes & 1U) == 0U) seek_horizontal = false; + if (seek_vertical && (axes & 2U) == 0U) seek_vertical = false; + const auto maximum_x = std::max( + 0.0F, + node->scroll_content_width - node->scroll_viewport_width); + const auto maximum_y = std::max( + 0.0F, + node->scroll_content_height - node->scroll_viewport_height); + const auto root = node == &document.body(); + if (seek_horizontal && maximum_x > 0.01F + && (node->style.scroll_x_enabled || root)) { + gesture.horizontal_target_id = node->id; + seek_horizontal = false; + } + if (seek_vertical && maximum_y > 0.01F + && (node->style.scroll_y_enabled || root)) { + gesture.vertical_target_id = node->id; + seek_vertical = false; + } + } + } + + bool advance_touch_pan_gesture( + const pointer_input_metadata& metadata, + const webscene_input_event& input) + { + if (!is_contact_pointer(metadata.device) + || metadata.pointer_id >= touch_pan_gestures.size()) return true; + auto& gesture = touch_pan_gestures[metadata.pointer_id]; + if (!gesture.active) return true; + const auto delta_x = static_cast(input.x - gesture.last_x); + const auto delta_y = static_cast(input.y - gesture.last_y); + gesture.last_x = input.x; + gesture.last_y = input.y; + const auto apply = [&](uint32_t target_id, float x, float y) { + auto* target = document.find_by_native_id(target_id); + if (target == nullptr) return true; + const auto maximum_x = std::max( + 0.0F, + target->scroll_content_width - target->scroll_viewport_width); + const auto maximum_y = std::max( + 0.0F, + target->scroll_content_height - target->scroll_viewport_height); + const auto next_x = std::clamp( + target->scroll_left - x, 0.0F, maximum_x); + const auto next_y = std::clamp( + target->scroll_top - y, 0.0F, maximum_y); + if (std::abs(next_x - target->scroll_left) <= 0.01F + && std::abs(next_y - target->scroll_top) <= 0.01F) return true; + document.set_scroll_offset(*target, next_x, next_y); + return dispatch_input_event_type(input, "scroll", *target); + }; + if (gesture.horizontal_target_id != 0U + && gesture.horizontal_target_id == gesture.vertical_target_id) { + return apply(gesture.horizontal_target_id, delta_x, delta_y); + } + if (gesture.horizontal_target_id != 0U + && !apply(gesture.horizontal_target_id, delta_x, 0.0F)) return false; + return gesture.vertical_target_id == 0U + || apply(gesture.vertical_target_id, 0.0F, delta_y); + } + + void end_touch_pan_gesture(const pointer_input_metadata& metadata) + { + if (metadata.pointer_id < touch_pan_gestures.size()) + touch_pan_gestures[metadata.pointer_id] = {}; + } + bool dispatch_input(const webscene_input_event& input, bool defer_cursor_update = false) { struct user_input_dispatch_scope { @@ -3898,6 +3993,20 @@ ~user_input_dispatch_scope() { active = previous; } } dispatch_scope{dispatching_user_input}; current_input_sequence = input.sequence; + const auto pointing_input = input.kind == WEBSCENE_INPUT_POINTER_MOVE + || input.kind == WEBSCENE_INPUT_POINTER_DOWN + || input.kind == WEBSCENE_INPUT_POINTER_UP + || input.kind == WEBSCENE_INPUT_POINTER_LEAVE + || input.kind == WEBSCENE_INPUT_POINTER_CANCEL + || input.kind == WEBSCENE_INPUT_WHEEL; + auto pointer_metadata = pointer_input_metadata{}; + if (pointing_input) { + const auto decoded = decode_pointer_input_metadata(input.flags); + if (!decoded.has_value()) return true; + pointer_metadata = *decoded; + } + const auto mouse_pointer = + pointer_metadata.device == WEBSCENE_POINTER_DEVICE_MOUSE; if (input.kind == WEBSCENE_INPUT_POINTER_DOWN || input.kind == WEBSCENE_INPUT_KEY_DOWN) { last_user_activation = std::chrono::steady_clock::now(); @@ -3917,8 +4026,10 @@ || input.kind == WEBSCENE_INPUT_KEY_UP || input.kind == WEBSCENE_INPUT_TEXT; auto* initial_hit_target = keyboard ? nullptr : document.hit_test( document.body(), static_cast(input.x), static_cast(input.y)); + auto* captured_target = pointer_capture_id == pointer_metadata.pointer_id + ? pointer_capture_target : nullptr; auto* input_owner = keyboard ? active_element - : pointer_capture_target != nullptr ? pointer_capture_target : initial_hit_target; + : captured_target != nullptr ? captured_target : initial_hit_target; if (input_owner != nullptr) { local_context = context_for_node(*input_owner); } else if (keyboard && focused_document_owner_id != 0U) { @@ -3961,11 +4072,15 @@ } } const auto leave_host = [&]() -> bool { + if (!mouse_pointer) { + end_touch_pan_gesture(pointer_metadata); + return true; + } // OS/control exits need not have a final mousemove. Clear :hover // before dispatch, with no destination element (relatedTarget=null). // Capture keeps its boundary until release; a host exit is not up // or cancel and must not interrupt a chart/scrollbar drag. - if (pointer_capture_target != nullptr || scrollbar_drag_target != nullptr) { + if (captured_target != nullptr || scrollbar_drag_target != nullptr) { return true; } update_scrollbar_thumb_interaction(nullptr, nullptr); @@ -4026,7 +4141,8 @@ return hit_test_scrollbar_thumb( *start, static_cast(input.x), static_cast(input.y)); }; - auto current_scrollbar_hit = scrollbar_hit_at_pointer(); + auto current_scrollbar_hit = mouse_pointer + ? scrollbar_hit_at_pointer() : std::optional{}; if (scrollbar_drag_target == nullptr && input.kind == WEBSCENE_INPUT_POINTER_MOVE) { update_scrollbar_thumb_interaction( @@ -4070,7 +4186,7 @@ } return true; } - if (pointer_outside && pointer_capture_target == nullptr + if (pointer_outside && captured_target == nullptr && (input.kind == WEBSCENE_INPUT_POINTER_MOVE || input.kind == WEBSCENE_INPUT_POINTER_DOWN || input.kind == WEBSCENE_INPUT_POINTER_UP)) { @@ -4083,8 +4199,8 @@ auto* hit_target = initial_hit_target; if (hit_target == nullptr) hit_target = &document.body(); if (pointer_outside) { - hit_target = pointer_capture_target != nullptr - ? pointer_capture_target : &active_root(); + hit_target = captured_target != nullptr + ? captured_target : &active_root(); } if (input.kind == WEBSCENE_INPUT_POINTER_DOWN && ((input.flags >> 8U) & 0xffU) <= 1U) { @@ -4112,9 +4228,9 @@ current_scrollbar_hit.has_value() ? current_scrollbar_hit->target : nullptr, nullptr); } - auto* target = pointer_capture_target != nullptr + auto* target = captured_target != nullptr && input.kind != WEBSCENE_INPUT_POINTER_DOWN - ? pointer_capture_target + ? captured_target : hit_target; current_movement_x = has_pointer_position ? input.x - last_pointer_x : 0; current_movement_y = has_pointer_position ? input.y - last_pointer_y : 0; @@ -4123,9 +4239,10 @@ last_pointer_y = input.y; has_pointer_position = true; } - if ((input.kind == WEBSCENE_INPUT_POINTER_MOVE + if (mouse_pointer + && (input.kind == WEBSCENE_INPUT_POINTER_MOVE || input.kind == WEBSCENE_INPUT_POINTER_DOWN) - && (!pointer_outside || pointer_capture_target != nullptr) + && (!pointer_outside || captured_target != nullptr) && hit_target != hover_target) { auto* previous_hover_target = hover_target; // Browsers update :hover before dispatching the boundary events. @@ -4231,7 +4348,7 @@ static_cast(input.x), static_cast(input.y)); if (hit_target == nullptr) hit_target = &active_root(); - if (pointer_capture_target == nullptr + if (captured_target == nullptr || input.kind == WEBSCENE_INPUT_POINTER_DOWN) { target = hit_target; } @@ -4244,6 +4361,10 @@ "hover-target-replaced"); } } + if (input.kind == WEBSCENE_INPUT_POINTER_DOWN + && is_contact_pointer(pointer_metadata.device)) { + begin_touch_pan_gesture(pointer_metadata, input, *hit_target); + } const char* types[3]{}; size_t type_count = 0; bool form_control_changed = false; @@ -4266,8 +4387,8 @@ << ", hit=" << hit_target->id << ", target=" << target->id << ", capture-target=" - << (pointer_capture_target == nullptr - ? 0U : pointer_capture_target->id) + << (captured_target == nullptr + ? 0U : captured_target->id) << ", flags=" << input.flags << ", x=" << input.x << ", y=" << input.y @@ -4279,7 +4400,7 @@ switch (input.kind) { case WEBSCENE_INPUT_POINTER_MOVE: types[type_count++] = "pointermove"; - types[type_count++] = "mousemove"; + if (mouse_pointer) types[type_count++] = "mousemove"; break; case WEBSCENE_INPUT_POINTER_DOWN: { @@ -4298,7 +4419,7 @@ pointer_down_x = input.x; pointer_down_y = input.y; types[type_count++] = "pointerdown"; - types[type_count++] = "mousedown"; + if (mouse_pointer) types[type_count++] = "mousedown"; if (changed_button == 2) context_menu_target = target; break; case WEBSCENE_INPUT_POINTER_UP: @@ -4312,7 +4433,7 @@ "active-pointer-up"); } types[type_count++] = "pointerup"; - types[type_count++] = "mouseup"; + if (mouse_pointer) types[type_count++] = "mouseup"; if (pointer_down_target != nullptr && std::hypot(input.x - pointer_down_x, input.y - pointer_down_y) <= 4.0) { // UI Events targets click at the nearest common inclusive @@ -4353,6 +4474,17 @@ } } break; + case WEBSCENE_INPUT_POINTER_CANCEL: + { + auto* previous_active_target = active_pointer_target; + active_pointer_target = nullptr; + recascade_active_transition( + previous_active_target, + nullptr, + "active-pointer-cancel"); + } + types[type_count++] = "pointercancel"; + break; case WEBSCENE_INPUT_WHEEL: types[type_count++] = "wheel"; break; @@ -4387,23 +4519,32 @@ } } } - if (!maybe_begin_outbound_drag(input)) { + if (mouse_pointer && !maybe_begin_outbound_drag(input)) { last_error = "Native outbound drag initiation failed: " + describe_reported_exception(try_catch, local_context); report_input_exception(); return false; } if (input.kind == WEBSCENE_INPUT_POINTER_DOWN - && changed_button == 0 && !pointer_focus_default_prevented + && mouse_pointer && changed_button == 0 + && !pointer_focus_default_prevented && !begin_pointer_text_selection(input, *target)) { last_error = "Native selectstart dispatch failed"; return false; } if ((input.kind == WEBSCENE_INPUT_POINTER_MOVE || input.kind == WEBSCENE_INPUT_POINTER_UP) - && pointer_text_selection_active) { + && mouse_pointer && pointer_text_selection_active) { update_pointer_text_selection(input, *hit_target); } + if (input.kind == WEBSCENE_INPUT_POINTER_MOVE + && is_contact_pointer(pointer_metadata.device) + && !advance_touch_pan_gesture(pointer_metadata, input)) { + last_error = "Native touch-action scroll dispatch failed: " + + describe_reported_exception(try_catch, local_context); + report_input_exception(); + return false; + } if (input.kind == WEBSCENE_INPUT_WHEEL && !wheel_default_prevented) { const auto discrete_wheel = (input.flags & WEBSCENE_INPUT_WHEEL_DISCRETE) != 0U; @@ -4678,18 +4819,24 @@ return false; } } - if (input.kind == WEBSCENE_INPUT_POINTER_UP) { + if (input.kind == WEBSCENE_INPUT_POINTER_UP + || input.kind == WEBSCENE_INPUT_POINTER_CANCEL) { + end_touch_pan_gesture(pointer_metadata); pointer_text_selection_active = false; pointer_selection_root_id = 0U; - pointer_capture_target = nullptr; + if (pointer_capture_id == pointer_metadata.pointer_id) { + pointer_capture_target = nullptr; + pointer_capture_id = 0U; + } pointer_down_target = nullptr; middle_paste_target_id = 0U; outbound_drag_threshold_crossed = false; if (pointer_outside) return leave_host(); } - if (input.kind == WEBSCENE_INPUT_POINTER_MOVE + if (mouse_pointer + && (input.kind == WEBSCENE_INPUT_POINTER_MOVE || input.kind == WEBSCENE_INPUT_POINTER_DOWN - || input.kind == WEBSCENE_INPUT_POINTER_UP) { + || input.kind == WEBSCENE_INPUT_POINTER_UP)) { if (defer_cursor_update && input.kind == WEBSCENE_INPUT_POINTER_MOVE) { pointer_cursor_update_pending = true; return true; diff --git a/experiments/WebScene.NativeEngine.Probe/tests/css_property_identity_tests.cpp b/experiments/WebScene.NativeEngine.Probe/tests/css_property_identity_tests.cpp index 7f1c467c6..f8325cbd8 100644 --- a/experiments/WebScene.NativeEngine.Probe/tests/css_property_identity_tests.cpp +++ b/experiments/WebScene.NativeEngine.Probe/tests/css_property_identity_tests.cpp @@ -106,10 +106,11 @@ int main() static_assert(static_cast(css_property_id::will_change) == 154U); static_assert(static_cast(css_property_id::text_wrap) == 155U); static_assert(static_cast(css_property_id::caret_color) == 156U); - static_assert(native_typed_property_identity_catalog.size() == 233U); + static_assert(static_cast(css_property_id::touch_action) == 159U); + static_assert(native_typed_property_identity_catalog.size() == 235U); static_assert(native_storage_only_property_catalog.size() == 62U); - static_assert(cssom_supported_property_catalog.size() == 256U); - static_assert(cssom_style_template_property_accessor_count == 468U); + static_assert(cssom_supported_property_catalog.size() == 257U); + static_assert(cssom_style_template_property_accessor_count == 470U); for (const auto& entry : native_typed_property_identity_catalog) { require(property_id(entry.name) == entry.id, "typed name maps to its generated id", entry.name); @@ -152,7 +153,7 @@ int main() && !variable_collection_contains(indexed_variables, std::string{"--missing"}), "variable membership supports ordered and indexed containers"); - std::array(css_property_id::caret_color) + 1U> + std::array(css_property_id::touch_action) + 1U> audited_mask_ids{}; for (const auto& entry : native_typed_property_identity_catalog) { const auto index = static_cast(entry.id); @@ -171,6 +172,7 @@ int main() && generated_property_inherits_by_default("caret-color") && generated_property_inherits_by_default("-webkit-font-smoothing") && !generated_property_inherits_by_default("display") + && !generated_property_inherits_by_default("touch-action") && !generated_property_inherits_by_default("width"), "generated inheritance lookup preserves inherited and non-inherited sentinels"); for (const auto& entry : effective_property_metadata_catalog) { @@ -208,8 +210,8 @@ int main() "generated simple grammar dispatch agrees with specified-value implementation"); } #if !defined(WEBSCENE_TEST_LEGACY_SPECIFIED_VALUE) - static_assert(specified_property_samples.size() == 148U); - std::array sampled{}; + static_assert(specified_property_samples.size() == 161U); + std::array(css_property_id::touch_action) + 1U> sampled{}; for (const auto& sample : specified_property_samples) { const auto property = property_id(sample.name); const auto index = static_cast(property); diff --git a/experiments/WebScene.NativeEngine.Probe/tests/native_v8_runtime_css_layout_tests.inc b/experiments/WebScene.NativeEngine.Probe/tests/native_v8_runtime_css_layout_tests.inc index fe66c0f90..69526aed3 100644 --- a/experiments/WebScene.NativeEngine.Probe/tests/native_v8_runtime_css_layout_tests.inc +++ b/experiments/WebScene.NativeEngine.Probe/tests/native_v8_runtime_css_layout_tests.inc @@ -406,6 +406,64 @@ void test_css_caret_color_performance_gate(webscene_engine* engine) + std::to_string(elapsed) + " ms"); } +void test_css_touch_action_performance_gate(webscene_engine* engine) +{ + resize(engine, 1024, 768, 1U); + execute(engine, R"JS( + document.body.innerHTML = `
+
+
+
`; + globalThis.touchActionGrid = document.getElementById('touch-action-grid'); + const fragment = document.createDocumentFragment(); + for (let index = 0; index < 4096; ++index) { + fragment.appendChild(document.createElement('div')); + } + touchActionGrid.appendChild(fragment); + )JS", "native-css-touch-action-performance-setup.js"); + webscene_engine_metrics before{}; + webscene_engine_get_metrics(engine, &before); + const auto started = std::chrono::steady_clock::now(); + const auto result = evaluate(engine, R"JS( + (() => { + for (let cycle = 0; cycle < 10; ++cycle) { + const value = cycle % 2 === 0 ? 'pan-x' : 'none'; + for (const node of touchActionGrid.children) node.style.touchAction = value; + } + return [touchActionGrid.children.length, + touchActionGrid.lastElementChild.style.touchAction]; + })() + )JS", "native-css-touch-action-performance-mutation.js"); + webscene_engine_metrics after_mutation{}; + webscene_engine_get_metrics(engine, &after_mutation); + require(result == R"JSON([4096,"none"])JSON", + "CSS touch-action performance fixture regressed: " + result); + require(after_mutation.layout_passes == before.layout_passes, + "CSS touch-action transitions performed layout"); + require(after_mutation.published_scenes <= before.published_scenes + 1U, + "CSS touch-action transitions published unbounded scenes"); + + const auto consumed_before = consumed_input_count(engine); + for (uint32_t gesture = 0; gesture < 1000U; ++gesture) { + const auto sequence = 10000U + static_cast(gesture) * 3U; + contact_pointer(engine, WEBSCENE_INPUT_POINTER_DOWN, + WEBSCENE_POINTER_DEVICE_TOUCH, 1U, 80, 80, sequence, true); + contact_pointer(engine, WEBSCENE_INPUT_POINTER_MOVE, + WEBSCENE_POINTER_DEVICE_TOUCH, 1U, 70, 70, sequence + 1U, true); + contact_pointer(engine, WEBSCENE_INPUT_POINTER_UP, + WEBSCENE_POINTER_DEVICE_TOUCH, 1U, 70, 70, sequence + 2U, false); + } + wait_for_consumed_inputs(engine, consumed_before + 3000U, + "1,000 touch-action gestures did not retire"); + const auto elapsed = std::chrono::duration( + std::chrono::steady_clock::now() - started).count(); + require(webscene_engine_requires_animation_frame(engine) == 0, + "touch-action gestures leaked animation-frame demand"); + require(elapsed < 5000.0, + "4,096-node/1,000-gesture touch-action gate exceeded 5 seconds: " + + std::to_string(elapsed) + " ms"); +} + void test_modal_backdrop_scene(webscene_engine* engine) { resize(engine,400,300,1); diff --git a/experiments/WebScene.NativeEngine.Probe/tests/native_v8_runtime_input_tests.inc b/experiments/WebScene.NativeEngine.Probe/tests/native_v8_runtime_input_tests.inc index 8f2683e9a..392ed468d 100644 --- a/experiments/WebScene.NativeEngine.Probe/tests/native_v8_runtime_input_tests.inc +++ b/experiments/WebScene.NativeEngine.Probe/tests/native_v8_runtime_input_tests.inc @@ -84,6 +84,38 @@ void pointer_button( require(webscene_engine_enqueue(engine, &input) != 0, "pointer button was rejected"); } +uint32_t pointer_metadata_flags( + webscene_pointer_device_kind device, + uint32_t pointer_id, + bool primary = true) +{ + return WEBSCENE_INPUT_POINTER_METADATA_VERSION_1 + | (static_cast(device) + << WEBSCENE_INPUT_POINTER_DEVICE_SHIFT) + | (primary ? WEBSCENE_INPUT_POINTER_PRIMARY : 0U) + | (pointer_id << WEBSCENE_INPUT_POINTER_ID_SHIFT); +} + +void contact_pointer( + webscene_engine* engine, + uint32_t kind, + webscene_pointer_device_kind device, + uint32_t pointer_id, + double x, + double y, + uint64_t sequence, + bool pressed, + bool primary = true) +{ + auto flags = pointer_metadata_flags(device, pointer_id, primary); + if (pressed) flags |= 1U; + if (kind == WEBSCENE_INPUT_POINTER_DOWN + || kind == WEBSCENE_INPUT_POINTER_UP) flags |= 1U << 8U; + const webscene_input_event input{kind, flags, sequence, x, y, 0, 0}; + require(webscene_engine_enqueue(engine, &input) != 0, + "contact pointer input was rejected"); +} + void animation_frame(webscene_engine* engine, double timestamp, uint64_t sequence) { const webscene_input_event input{ @@ -154,6 +186,64 @@ void test_user_select_pointer_default_action(webscene_engine* engine) "canceled selectstart still created a pointer selection"); } +void test_touch_action_pointer_device_and_pan(webscene_engine* engine) +{ + resize(engine, 320, 220, 7920U); + execute(engine, R"JS( + document.body.innerHTML = `
`; + globalThis.pointerRecords = []; + globalThis.mouseCompatibility = 0; + for (const type of ['pointerdown','pointermove','pointerup','pointercancel']) { + content.addEventListener(type, event => { + pointerRecords.push([ + type, event.pointerType, event.pointerId, + event.isPrimary, event.buttons + ]); + if (type === 'pointerdown') content.setPointerCapture(event.pointerId); + }); + } + content.addEventListener('mousedown', () => ++mouseCompatibility); + content.addEventListener('mouseup', () => ++mouseCompatibility); + )JS", "native-touch-action-setup.js"); + + contact_pointer(engine, WEBSCENE_INPUT_POINTER_DOWN, + WEBSCENE_POINTER_DEVICE_TOUCH, 7U, 80, 80, 7921U, true); + // The used policy is frozen at gesture start. A mutation after pointerdown + // affects only the next contact. + execute(engine, "scroller.style.touchAction='none'", + "native-touch-action-mutate-after-start.js"); + contact_pointer(engine, WEBSCENE_INPUT_POINTER_MOVE, + WEBSCENE_POINTER_DEVICE_TOUCH, 7U, 45, 45, 7922U, true); + contact_pointer(engine, WEBSCENE_INPUT_POINTER_UP, + WEBSCENE_POINTER_DEVICE_TOUCH, 7U, 45, 45, 7923U, false); + require(evaluate(engine, R"JS(JSON.stringify([ + scroller.scrollLeft, scroller.scrollTop, pointerRecords, + mouseCompatibility, content.hasPointerCapture(7) + ]))JS", "native-touch-action-first-result.js") + == R"JSON([0,35,[["pointerdown","touch",7,true,1],["pointermove","touch",7,true,1],["pointerup","touch",7,true,0]],0,false])JSON", + "pan-y did not freeze its axis policy or preserve touch identity"); + + execute(engine, "scroller.scrollTop=0; pointerRecords=[];", + "native-touch-action-none-reset.js"); + contact_pointer(engine, WEBSCENE_INPUT_POINTER_DOWN, + WEBSCENE_POINTER_DEVICE_PEN, 2U, 80, 80, 7924U, true); + contact_pointer(engine, WEBSCENE_INPUT_POINTER_MOVE, + WEBSCENE_POINTER_DEVICE_PEN, 2U, 40, 40, 7925U, true); + contact_pointer(engine, WEBSCENE_INPUT_POINTER_CANCEL, + WEBSCENE_POINTER_DEVICE_PEN, 2U, 40, 40, 7926U, false); + require(evaluate(engine, R"JS(JSON.stringify([ + scroller.scrollLeft, scroller.scrollTop, pointerRecords, + content.hasPointerCapture(2) + ]))JS", "native-touch-action-none-result.js") + == R"JSON([0,0,[["pointerdown","pen",2,true,1],["pointermove","pen",2,true,1],["pointercancel","pen",2,true,0]],false])JSON", + "touch-action:none admitted retained panning or leaked cancellation state"); +} + void animation_frame_and_wait( webscene_engine* engine, double timestamp, diff --git a/experiments/WebScene.NativeEngine.Probe/tests/native_v8_runtime_tests.cpp b/experiments/WebScene.NativeEngine.Probe/tests/native_v8_runtime_tests.cpp index 1639fb828..43207a456 100644 --- a/experiments/WebScene.NativeEngine.Probe/tests/native_v8_runtime_tests.cpp +++ b/experiments/WebScene.NativeEngine.Probe/tests/native_v8_runtime_tests.cpp @@ -445,6 +445,14 @@ int main() webscene_engine_destroy(focused_engine); return 0; } + if (selected == "css-touch-action-performance") { + auto* focused_engine = webscene_engine_create(0); + require(focused_engine != nullptr, + "CSS touch-action performance engine creation failed"); + test_css_touch_action_performance_gate(focused_engine); + webscene_engine_destroy(focused_engine); + return 0; + } if(selected=="placeholder-pseudo") { auto* focused_engine=webscene_engine_create(0); require(focused_engine!=nullptr,"placeholder pseudo engine creation failed"); @@ -1534,6 +1542,7 @@ int main() test_inline_block_preserves_vertical_padding(engine); test_pointer_hit_targets_and_related_targets_are_elements(engine); test_user_select_pointer_default_action(engine); + test_touch_action_pointer_device_and_pan(engine); test_overscroll_behavior_wheel_chaining(engine); test_pointer_cursor_and_external_anchor_host_handoff(engine); test_nested_drag_drop_routing_and_retirement(); diff --git a/experiments/WebScene.NativeEngine.Probe/tests/pointer_input_metadata_tests.cpp b/experiments/WebScene.NativeEngine.Probe/tests/pointer_input_metadata_tests.cpp new file mode 100644 index 000000000..5286c1f57 --- /dev/null +++ b/experiments/WebScene.NativeEngine.Probe/tests/pointer_input_metadata_tests.cpp @@ -0,0 +1,36 @@ +#include "webscene_pointer_input.h" + +#include +#include + +using namespace webscene_native; + +int main() +{ + static_assert(sizeof(webscene_input_event) == 48U); + const auto legacy = decode_pointer_input_metadata(0U); + assert(legacy.has_value()); + assert(legacy->legacy); + assert(legacy->device == WEBSCENE_POINTER_DEVICE_MOUSE); + assert(legacy->pointer_id == 1U && legacy->primary); + + const auto flags = WEBSCENE_INPUT_POINTER_METADATA_VERSION_1 + | (WEBSCENE_POINTER_DEVICE_TOUCH + << WEBSCENE_INPUT_POINTER_DEVICE_SHIFT) + | WEBSCENE_INPUT_POINTER_PRIMARY + | (7U << WEBSCENE_INPUT_POINTER_ID_SHIFT); + const auto touch = decode_pointer_input_metadata(flags); + assert(touch.has_value() && !touch->legacy); + assert(touch->device == WEBSCENE_POINTER_DEVICE_TOUCH); + assert(touch->pointer_id == 7U && touch->primary); + assert(std::string_view{pointer_device_name(touch->device)} == "touch"); + + assert(!decode_pointer_input_metadata( + 2U << WEBSCENE_INPUT_POINTER_METADATA_VERSION_SHIFT)); + assert(!decode_pointer_input_metadata( + WEBSCENE_INPUT_POINTER_METADATA_VERSION_1)); + assert(!decode_pointer_input_metadata( + WEBSCENE_INPUT_POINTER_METADATA_VERSION_1 + | (3U << WEBSCENE_INPUT_POINTER_DEVICE_SHIFT) + | (1U << WEBSCENE_INPUT_POINTER_ID_SHIFT))); +} diff --git a/src/WebScene.Css/CssPropertyCatalog.cs b/src/WebScene.Css/CssPropertyCatalog.cs index d78bd600f..2629be08d 100644 --- a/src/WebScene.Css/CssPropertyCatalog.cs +++ b/src/WebScene.Css/CssPropertyCatalog.cs @@ -93,6 +93,9 @@ public static bool IsValidCssomValue(string propertyName, string value) "isolation" => normalizedValue is "auto" or "isolate", "will-change" => IsWillChange(normalizedValue), "text-wrap" => normalizedValue is "wrap" or "nowrap", + "touch-action" => normalizedValue is "auto" or "none" + or "manipulation" or "pan-x" or "pan-y" + or "pan-x pan-y" or "pan-y pan-x", "caret-color" => normalizedValue is "auto" or "currentcolor" || CssColorParser.TryParseColor(trimmed, out _), "font-size" => IsFontSize(normalizedValue), diff --git a/src/WebScene.Css/CssPropertyMetadata.Generated.cs b/src/WebScene.Css/CssPropertyMetadata.Generated.cs index acba5b7bf..4bb633f7b 100644 --- a/src/WebScene.Css/CssPropertyMetadata.Generated.cs +++ b/src/WebScene.Css/CssPropertyMetadata.Generated.cs @@ -123,6 +123,7 @@ internal static class CssGeneratedPropertyMetadata "will-change", "text-wrap", "caret-color", + "touch-action", ]; internal static readonly string[] SupportedNames = @@ -361,6 +362,7 @@ internal static class CssGeneratedPropertyMetadata "text-transform", "text-wrap", "top", + "touch-action", "transform", "transform-origin", "transition", @@ -507,6 +509,7 @@ internal static bool TryGetKnownId(string name, out int id) "will-change" => 115, "text-wrap" => 116, "caret-color" => 117, + "touch-action" => 118, _ => -1, }; return id >= 0; diff --git a/tests/WebPlatformSubset/capabilities.json b/tests/WebPlatformSubset/capabilities.json index 6776d0357..8c2ec7cc5 100644 --- a/tests/WebPlatformSubset/capabilities.json +++ b/tests/WebPlatformSubset/capabilities.json @@ -464,6 +464,14 @@ "productEvidence": ["Code OSS EditContext text-area cursor", "Code OSS editor cursor theme color"], "coverage": ["contracts/css-caret-color.html", "test_native_text_input_focus_events_and_caret", "css-caret-color-performance"], "gap": "Issue #804 implements the caret-color slice used by unchanged Code OSS. The inherited token is stored only in sparse cold style state and drives the existing kind-14 retained caret command independently from text foreground, with no extra scene node. Direct browser/native execution, exact editor pixels, package regions, memory, lifecycle, and performance evidence remain qualification debt." + }, + { + "family": "css-touch-action", + "required": ["touch-action CSSOM", "mouse/touch/pen identity", "stable pointer IDs", "frozen ancestor policy", "axis-qualified retained panning", "capture/cancel teardown"], + "candidate": ["unchanged Code OSS touch surfaces", "cross-RID native contact qualification"], + "productEvidence": ["52 declarations across 27 unchanged Code OSS files", "tabs/editors/split views/sliders/terminal/Chat interaction surfaces"], + "coverage": ["contracts/css-touch-action.html", "test_touch_action_pointer_device_and_pan", "css-touch-action-performance", "webscene_pointer_input_metadata_tests"], + "gap": "Issue #799 implements the unchanged Code OSS auto/none/manipulation/pan-x/pan-y slice with versioned AppScene #298 pointer metadata and fixed per-contact gesture state. Direct macOS/Windows/Linux contact hardware, exact product pixels, package regions, accessibility, memory, and full performance evidence remain qualification debt." } ] } diff --git a/tests/WebPlatformSubset/contracts/css-touch-action.html b/tests/WebPlatformSubset/contracts/css-touch-action.html new file mode 100644 index 000000000..3d7c55b84 --- /dev/null +++ b/tests/WebPlatformSubset/contracts/css-touch-action.html @@ -0,0 +1,34 @@ + + +Touch action CSSOM and non-inheritance + + + +
+ diff --git a/tests/WebPlatformSubset/webscene-component-profile.json b/tests/WebPlatformSubset/webscene-component-profile.json index c21449dcf..5dec5a06a 100644 --- a/tests/WebPlatformSubset/webscene-component-profile.json +++ b/tests/WebPlatformSubset/webscene-component-profile.json @@ -2871,6 +2871,13 @@ "capabilities": ["caret-color-cssom", "inherited-caret-color", "currentcolor-caret", "retained-caret-paint"], "evidence": ["focused-native-scene-contract-authored", "direct-chromium-contract-authored", "unchanged-code-oss-645f29c-inventory"], "reason": "Implementation-first WebScene #804 contract for the unchanged Code OSS EditContext caret color. It covers inheritance, auto/currentColor, computed CSSOM, mutation/removal, and a distinct retained caret scene color without adding a second caret node. Direct browser/native execution, exact editor pixels, package regions, accessibility, performance, memory, and lifecycle evidence remain qualification debt." + }, + { + "path": "contracts/css-touch-action.html", + "type": "testharness", + "capabilities": ["touch-action-cssom", "non-inherited-touch-policy", "axis-policy", "mutation-removal"], + "evidence": ["focused-native-input-contract-authored", "direct-chromium-contract-authored", "unchanged-code-oss-645f29c-inventory"], + "reason": "Implementation-first WebScene #799 contract for 52 touch-action declarations across 27 unchanged Code OSS files. Native contracts cover versioned mouse/touch/pen identity, stable pointer IDs, frozen gesture policy, axis panning, capture, cancellation, and bounded fixed storage. Direct hardware, exact product pixels, package regions, accessibility, memory, and full performance evidence remain qualification debt." } ], "harnessBlocked": [], diff --git a/tests/WebScene.Css.Tests/CssMutationInvalidationPlannerTests.cs b/tests/WebScene.Css.Tests/CssMutationInvalidationPlannerTests.cs index 190820109..38465eee7 100644 --- a/tests/WebScene.Css.Tests/CssMutationInvalidationPlannerTests.cs +++ b/tests/WebScene.Css.Tests/CssMutationInvalidationPlannerTests.cs @@ -13,6 +13,7 @@ public sealed class CssMutationInvalidationPlannerTests [InlineData("flex-wrap:wrap", CssMutationInvalidationScope.Target | CssMutationInvalidationScope.Layout)] [InlineData("text-wrap:nowrap", CssMutationInvalidationScope.Target | CssMutationInvalidationScope.Descendants | CssMutationInvalidationScope.Layout)] [InlineData("caret-color:red", CssMutationInvalidationScope.Target | CssMutationInvalidationScope.Descendants)] + [InlineData("touch-action:none", CssMutationInvalidationScope.Target)] [InlineData("background:red", CssMutationInvalidationScope.Target)] [InlineData("broken", CssMutationInvalidationScope.Target)] public void InlineStylePlanClassifiesPortableScope(string style, CssMutationInvalidationScope expected) diff --git a/tests/WebScene.Css.Tests/CssPropertyCatalogTests.cs b/tests/WebScene.Css.Tests/CssPropertyCatalogTests.cs index a4e349206..9150d0df3 100644 --- a/tests/WebScene.Css.Tests/CssPropertyCatalogTests.cs +++ b/tests/WebScene.Css.Tests/CssPropertyCatalogTests.cs @@ -62,6 +62,7 @@ public void DoesNotMisclassifyNonColorValueProperties(string propertyName) [InlineData("willChange")] [InlineData("textWrap")] [InlineData("caretColor")] + [InlineData("touchAction")] public void ExposesSupportedCssomProperties(string name) => Assert.True(CssPropertyCatalog.IsSupported(name)); @@ -135,6 +136,9 @@ public void DoesNotExposeUnknownOrCustomPropertiesAsIdlAttributes(string name) [InlineData("caret-color", "#123456", true)] [InlineData("caret-color", "currentColor", true)] [InlineData("caret-color", "not-a-color", false)] + [InlineData("touch-action", "manipulation", true)] + [InlineData("touch-action", "pan-x pan-y", true)] + [InlineData("touch-action", "pinch-zoom", false)] public void ValidatesCssomValuesWithoutFrameworkKnowledge(string name, string value, bool expected) => Assert.Equal(expected, CssPropertyCatalog.IsValidCssomValue(name, value)); } From d1ffa0a81a99e8bf7484a82440e1030542561c03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Sun, 20 Sep 2026 08:02:10 +0200 Subject: [PATCH 2/4] test(css): refresh generated catalog sentinels --- scripts/test_css_property_metadata.py | 45 +++++++++++++-------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/scripts/test_css_property_metadata.py b/scripts/test_css_property_metadata.py index b2ef86f7d..914501ce5 100644 --- a/scripts/test_css_property_metadata.py +++ b/scripts/test_css_property_metadata.py @@ -66,40 +66,39 @@ def test_checked_in_outputs_match_catalog(self): def test_catalog_preserves_known_ids_and_unifies_supported_names(self): catalog = GENERATOR.load_catalog(GENERATOR.DEFAULT_INPUT) - self.assertEqual(106, len(catalog.managed_known_properties)) + self.assertEqual(119, len(catalog.managed_known_properties)) self.assertEqual("align-content", catalog.managed_known_properties[0]) - self.assertEqual("grid-template-areas", catalog.managed_known_properties[104]) + self.assertEqual("touch-action", catalog.managed_known_properties[-1]) self.assertEqual( - "fc4d77c515998b6df1f119ff7e09b0b82cc812744c34f1c44c6fd142067b1fb2", + "e7e0aed213f767dbc84f70ba35f9348c1ba705dd9363eeed634928364f8f36ee", hashlib.sha256("\n".join( - catalog.managed_known_properties[:105]).encode()).hexdigest(), + catalog.managed_known_properties).encode()).hexdigest(), ) - self.assertEqual(["aspect-ratio"], catalog.managed_known_properties[105:]) - self.assertEqual(239, len(GENERATOR.supported_names(catalog))) + self.assertEqual(257, len(GENERATOR.supported_names(catalog))) self.assertEqual( - "d259d18039fda1e0666700e232ca20a821a1118babebc37c4f4c5999eb15feff", + "fe13b91ddf2c8ab38f18445e6efe6084a576b9d4ac5d69b49076fa68cb2046d5", hashlib.sha256("\n".join( GENERATOR.supported_names(catalog)).encode()).hexdigest(), ) - self.assertEqual(147, len(catalog.native_property_ids)) + self.assertEqual(160, len(catalog.native_property_ids)) self.assertEqual(62, len(catalog.native_storage_only_properties)) native_ids = [entry["id"] for entry in catalog.native_property_ids] self.assertEqual( - "b17b1cd0520ae5e36fd875ed8226b1818508bbe41be9a32540ef58a1b31da036", - hashlib.sha256("\n".join(native_ids[:146]).encode()).hexdigest(), + "c72e585041c378389948483a534bec02fd150ebadab9360773083d0d698dd824", + hashlib.sha256("\n".join(native_ids).encode()).hexdigest(), ) - self.assertEqual(["aspect_ratio"], native_ids[146:]) + self.assertEqual("touch_action", native_ids[-1]) native_pairs = [ f'{name}:{entry["id"]}' for entry in catalog.native_property_ids for name in GENERATOR.native_property_names(entry) ] - self.assertEqual(202, len(native_pairs)) + self.assertEqual(235, len(native_pairs)) self.assertEqual( - "10bc777d9724b11b8872131e36b8b6eb4b5ae53becdea23f3f1f3e71302bd7f3", - hashlib.sha256("\n".join(native_pairs[:201]).encode()).hexdigest(), + "80f3c32a9c29b64cfc893b0cef327512d07ffe968aad846ba801a4c64cd4090e", + hashlib.sha256("\n".join(native_pairs).encode()).hexdigest(), ) - self.assertEqual(["aspect-ratio:aspect_ratio"], native_pairs[201:]) + self.assertEqual("touchaction:touch_action", native_pairs[-1]) self.assertEqual( "3f5f3616586fd121b580531538674b9002752a94d7c6d01610dc4c43125933c2", hashlib.sha256("\n".join( @@ -116,24 +115,24 @@ def test_catalog_preserves_known_ids_and_unifies_supported_names(self): ] self.assertEqual( { - "keyword": 29, - "componentList": 15, + "keyword": 40, + "componentList": 16, "length": 34, "lengthList4": 5, "lengthList2": 10, "color": 9, - "complex": 43, + "complex": 44, }, {family: len(ids) for family, ids in catalog.native_grammar_families.items()}, ) self.assertEqual( - "9087fb17d192ba0eb6848adcfbc46c9b88eed9903e1619d9684de53ea90ce724", + "f2fde1b185ddac76bb95618bcede6220beb93614aa62268a866500e92991a666", hashlib.sha256("\n".join(grammar_rows).encode()).hexdigest(), ) - self.assertEqual(20, len(catalog.native_inherited_properties)) - self.assertEqual(25, len(catalog.native_maskless_property_ids)) + self.assertEqual(24, len(catalog.native_inherited_properties)) + self.assertEqual(38, len(catalog.native_maskless_property_ids)) self.assertEqual( - "23bd3c8d7cd6527e45f5ed7f972c4f65504ae4b16e3054e2fb637526822551ed", + "36e7c460403184a993e4011932282a264d6a32b8144f91ac1c77f844e0210a35", hashlib.sha256("\n".join( catalog.native_maskless_property_ids).encode()).hexdigest(), ) @@ -141,7 +140,7 @@ def test_catalog_preserves_known_ids_and_unifies_supported_names(self): self.assertEqual("MozTransform", GENERATOR.css_idl_name("-moz-transform")) self.assertEqual("cssFloat", GENERATOR.css_idl_name("float")) self.assertIn( - "cssom_style_template_property_accessor_count = 435U", + "cssom_style_template_property_accessor_count = 470U", GENERATOR.generate_native_supported(catalog), ) From 5c29f2edd6f442fde780ebbb28081b738a487b37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Sun, 20 Sep 2026 08:04:04 +0200 Subject: [PATCH 3/4] test(ledger): refresh reachable API denominator --- scripts/test_code_oss_web_api_ledger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/test_code_oss_web_api_ledger.py b/scripts/test_code_oss_web_api_ledger.py index 584053b55..5a7d3e10f 100644 --- a/scripts/test_code_oss_web_api_ledger.py +++ b/scripts/test_code_oss_web_api_ledger.py @@ -95,7 +95,7 @@ def test_report_is_deterministic_and_bounded(self) -> None: second = ledger_module.render_report(self.catalog, self.snapshot, self.ledger) self.assertEqual(first, second) self.assertLessEqual(len(first.encode("utf-8")), 131_072) - self.assertIn("25/25 catalog entries reachable", first) + self.assertIn("26/26 catalog entries reachable", first) self.assertIn("Browser assertions: 25 pass, 7 fail", first) From c21f893028f86ae156f73c49ee1e0303f8930165 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Sun, 20 Sep 2026 08:05:51 +0200 Subject: [PATCH 4/4] docs(ledger): refresh generated capability report --- docs/validation/code-oss-web-api-ledger.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/validation/code-oss-web-api-ledger.md b/docs/validation/code-oss-web-api-ledger.md index d06e66422..c15902fda 100644 --- a/docs/validation/code-oss-web-api-ledger.md +++ b/docs/validation/code-oss-web-api-ledger.md @@ -29,7 +29,7 @@ Static matches establish source reachability only. Runtime claims require the ev | `form-data` | yes | blocked | linux-x64, macos-arm64, windows-x64 | WebScene#266 | 0 | FormData and multipart transport support exist in the runtime; bounded cross-platform webview/product evidence has not been accepted. | | `iframe` | yes | blocked | linux-x64, macos-arm64, windows-x64 | WebScene#253, WebScene#264, WebScene#265, WebScene#266, WebScene#267, WebScene#268 | 2 (12 pass, 0 fail, 0 skipped, 0 unavailable, 12 total) | Recorded evidence covers the earlier sandbox Wave 0 revision only. Current service-worker/resource/nested-frame stacks and packaged Markdown must still be promoted with revision-pinned results. | | `indexeddb` | yes | blocked | linux-x64, macos-arm64, windows-x64 | WebScene#56, WebScene#227 | 0 | Engine contracts exist, but Code OSS origin durability, quota, failure recovery, and package evidence are incomplete. | -| `media-capture` | yes | blocked | linux-x64, macos-arm64, windows-x64 | WebScene#770, WebScene#773, WebScene#775, WebScene#776, WebScene#777, AppScene#289, WebScene#261 | 0 | MediaDevices, microphone tracks, analyser graph delivery, and the narrow Code OSS PCM AudioWorklet are implemented with authored gates. Windows/Linux providers, physical-device and exact-package evidence remain incomplete. | +| `media-capture` | yes | blocked | linux-x64, macos-arm64, windows-x64 | WebScene#770, WebScene#773, WebScene#775, WebScene#776, WebScene#777, AppScene#289, WebScene#261 | 0 | WebScene exposes bounded MediaDevices enumeration, audio getUserMedia host handoff, microphone track lifecycle, graph delivery, and the narrow Code OSS PCM AudioWorklet/MessagePort path with authored but unexecuted gates. AppScene provides the bounded transport and macOS provider; Windows/Linux providers and physical-device evidence remain open. | | `message-port` | yes | blocked | linux-x64, macos-arm64, windows-x64 | WebScene#81 | 0 | Current implementation evidence has not yet been recorded as an accepted cross-platform run. | | `observers` | yes | blocked | linux-x64, macos-arm64, windows-x64 | WebScene#227, WebScene#258 | 0 | Observer scheduling, batching, disconnection, memory, and Code OSS panel-churn evidence are incomplete. | | `performance-timing` | yes | blocked | linux-x64, macos-arm64, windows-x64 | WebScene#227 | 0 | Monotonicity exists but accepted-scenario marks, observers, resource timing, bounds, and package evidence are not reconciled. |