From d086d60ae2e5860ff5e6cf8b77ab6d6589314bae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Sun, 20 Sep 2026 12:11:39 +0200 Subject: [PATCH] Add bounded desktop environment ingress --- .../native-desktop-environment-814.md | 31 +++++++ .../CMakeLists.txt | 8 ++ .../WebScene.NativeEngine.Probe/README.md | 4 + .../webscene_accessibility_preferences.h | 25 ++++-- .../native/webscene_native_engine.cpp | 30 +++++++ .../native/webscene_native_engine.h | 46 ++++++++++ .../webscene_native_engine_lifecycle.inc | 74 ++++++++++++++++ .../native/webscene_native_engine_worker.inc | 22 ++++- .../native/webscene_v8_runtime.cpp | 7 ++ .../native/webscene_v8_runtime.h | 2 + .../webscene_v8_runtime_browser_apis.inc | 87 +++++++++++++++++++ .../native/webscene_v8_runtime_state.inc | 2 + .../tests/desktop_environment_header_tests.c | 26 ++++++ .../native_v8_runtime_css_layout_tests.inc | 49 +++++++++++ .../tests/native_v8_runtime_tests.cpp | 1 + .../NativeSceneInteropTypes.cs | 32 +++++++ .../NativeSceneRuntime.cs | 7 ++ tests/WebPlatformSubset/capabilities.json | 6 +- 18 files changed, 448 insertions(+), 11 deletions(-) create mode 100644 docs/validation/native-desktop-environment-814.md create mode 100644 experiments/WebScene.NativeEngine.Probe/tests/desktop_environment_header_tests.c diff --git a/docs/validation/native-desktop-environment-814.md b/docs/validation/native-desktop-environment-814.md new file mode 100644 index 000000000..6bb44112e --- /dev/null +++ b/docs/validation/native-desktop-environment-814.md @@ -0,0 +1,31 @@ +# Native desktop environment ingress (#814) + +`webscene_engine_set_desktop_environment_v1` is the product-neutral host ABI +for desktop appearance, display, power, and session state. The 64-byte v1 value +is copied during the call. Unknown versions, flags, invalid DPI/scale values, +and non-zero reserved data are rejected. Equal snapshots do not wake the +worker. + +Appearance updates reuse the existing targeted media-query recascade and live +`matchMedia` delivery. System color values are read from the same worker-local +environment during CSS and scene work. Display, suspend/resume, availability, +lock/unlock, and session-ending changes are reduced to the newest snapshot and +dispatched from the JavaScript worker. No timer, polling thread, document scan, +or idle animation frame is introduced. + +## Authored gates + +- C and managed ABI size/version/source layout contracts. +- Malformed and future-version rejection. +- 10,000 duplicate notifications produce no additional worker wake. +- Dark/light, forced-colors, prefers-contrast, live `matchMedia`, and system + color behavior. +- Suspend/resume, display availability/topology/DPI, lock/unlock, and + session-ending transition ordering and navigation cleanup. +- Cascade/layout/scene counters prove paint-only state avoids layout and + unrelated selector work. +- Idle CPU/frame/wake, notification latency, and RSS plateau measurements. +- Unchanged Code OSS visual and lifecycle comparisons. + +Execution is intentionally deferred under the current fast-integration +direction; this document records the required qualification lane. diff --git a/experiments/WebScene.NativeEngine.Probe/CMakeLists.txt b/experiments/WebScene.NativeEngine.Probe/CMakeLists.txt index f05ec85e3..06c4df64d 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_desktop_environment_header_tests + tests/desktop_environment_header_tests.c) + target_compile_features(webscene_desktop_environment_header_tests PRIVATE c_std_11) + target_include_directories(webscene_desktop_environment_header_tests PRIVATE native) + add_test(NAME webscene_desktop_environment_header_tests + COMMAND webscene_desktop_environment_header_tests) + set_tests_properties(webscene_desktop_environment_header_tests PROPERTIES + LABELS "native;desktop-environment;abi;c11;performance" TIMEOUT 10) 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) diff --git a/experiments/WebScene.NativeEngine.Probe/README.md b/experiments/WebScene.NativeEngine.Probe/README.md index a581cf3e4..0780fe7d7 100644 --- a/experiments/WebScene.NativeEngine.Probe/README.md +++ b/experiments/WebScene.NativeEngine.Probe/README.md @@ -152,6 +152,10 @@ stable `fillStyle`, composite mode, or shadow state. component-owned iframe, script, and stylesheet resources. - `webscene_engine_set_preferred_color_scheme` provides the host's effective light/dark preference for CSS media queries and `Window.matchMedia`. +- `webscene_engine_set_desktop_environment_v1` copies a fixed 64-byte native + appearance/display/session snapshot, coalesces equal values, refreshes only + affected media/CSS state, and dispatches bounded lifecycle transitions from + the worker. It adds no host poller or idle animation frame. - `webscene_engine_execute_script` is the fire-and-forget execution boundary. - `webscene_engine_begin_evaluate_v3` and `webscene_engine_begin_invoke_v3` are the leased tagged-result diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_accessibility_preferences.h b/experiments/WebScene.NativeEngine.Probe/native/webscene_accessibility_preferences.h index 770c17c8e..09371bace 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_accessibility_preferences.h +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_accessibility_preferences.h @@ -14,6 +14,13 @@ struct accessibility_preferences final { bool forced_colors{false}; bool reduced_motion{false}; bool contrast_more{false}; + bool native_system_colors{false}; + uint32_t accent_rgba{0x0067c0ffU}; + uint32_t accent_text_rgba{0xffffffffU}; + uint32_t canvas_rgba{0xffffffffU}; + uint32_t canvas_text_rgba{0x000000ffU}; + uint32_t highlight_rgba{0x0067c0ffU}; + uint32_t highlight_text_rgba{0xffffffffU}; }; inline thread_local accessibility_preferences active_accessibility_preferences{}; @@ -39,8 +46,10 @@ inline std::optional system_color_rgba(std::string_view input) const auto dark = preferences.dark; const auto forced = preferences.forced_colors; - if (name == "canvas") return dark ? 0x1e1e1effU : 0xffffffffU; - if (name == "canvastext") return dark ? 0xffffffffU : 0x000000ffU; + if (name == "canvas") return preferences.native_system_colors + ? preferences.canvas_rgba : (dark ? 0x1e1e1effU : 0xffffffffU); + if (name == "canvastext") return preferences.native_system_colors + ? preferences.canvas_text_rgba : (dark ? 0xffffffffU : 0x000000ffU); if (name == "buttonface" || name == "field") { return forced ? (dark ? 0x000000ffU : 0xffffffffU) : (dark ? 0x333333ffU : 0xefefefffU); @@ -49,11 +58,15 @@ inline std::optional system_color_rgba(std::string_view input) return dark ? 0xffffffffU : 0x000000ffU; } if (name == "graytext") return dark ? 0xaaaaaaffU : 0x6d6d6dffU; - if (name == "highlight" || name == "selecteditem" || name == "accentcolor") { - return dark ? 0x6aa9ffffU : 0x0067c0ffU; - } + if (name == "highlight" || name == "selecteditem") return preferences.native_system_colors + ? preferences.highlight_rgba : (dark ? 0x6aa9ffffU : 0x0067c0ffU); + if (name == "accentcolor") return preferences.native_system_colors + ? preferences.accent_rgba : (dark ? 0x6aa9ffffU : 0x0067c0ffU); if (name == "highlighttext" || name == "selecteditemtext" - || name == "accentcolortext") return 0xffffffffU; + ) return preferences.native_system_colors + ? preferences.highlight_text_rgba : 0xffffffffU; + if (name == "accentcolortext") return preferences.native_system_colors + ? preferences.accent_text_rgba : 0xffffffffU; if (name == "linktext") return dark ? 0x8ab4f8ffU : 0x0000eeffU; if (name == "visitedtext") return dark ? 0xc58af9ffU : 0x551a8bffU; if (name == "activetext") return 0xff0000ffU; diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_native_engine.cpp b/experiments/WebScene.NativeEngine.Probe/native/webscene_native_engine.cpp index 88aad34ae..6306dc46f 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_native_engine.cpp +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_native_engine.cpp @@ -85,6 +85,7 @@ static_assert(sizeof(webscene_interop_pool_metrics_v3) == 200); static_assert(sizeof(webscene_runtime_work_metrics) == 168); static_assert(sizeof(webscene_document_script) == 40); static_assert(sizeof(webscene_navigation_options) == 16); +static_assert(sizeof(webscene_desktop_environment_v1) == 64); struct queued_input_event : webscene_input_event { uint64_t observed_compositor_timestamp = 0; @@ -1273,6 +1274,23 @@ struct webscene_engine final { std::atomic accessibility_preference_flags_{ WEBSCENE_ACCESSIBILITY_PREFERENCE_NONE_V1}; std::atomic accessibility_preferences_changed_{false}; + mutable std::mutex desktop_environment_mutex_; + webscene_desktop_environment_v1 desktop_environment_{ + sizeof(webscene_desktop_environment_v1), + WEBSCENE_DESKTOP_ENVIRONMENT_VERSION_1, + WEBSCENE_DESKTOP_ENVIRONMENT_DISPLAY_AVAILABLE_V1, + WEBSCENE_PREFERRED_COLOR_SCHEME_LIGHT, + 96U, 96U, 1000U, 0U, 0U, + 0x0067c0ffU, 0xffffffffU, 0xffffffffU, + 0x000000ffU, 0x0067c0ffU, 0xffffffffU}; + std::atomic desktop_environment_changed_{false}; + std::atomic desktop_environment_published_{false}; + std::atomic desktop_accent_rgba_{0x0067c0ffU}; + std::atomic desktop_accent_text_rgba_{0xffffffffU}; + std::atomic desktop_canvas_rgba_{0xffffffffU}; + std::atomic desktop_canvas_text_rgba_{0x000000ffU}; + std::atomic desktop_highlight_rgba_{0x0067c0ffU}; + std::atomic desktop_highlight_text_rgba_{0xffffffffU}; std::atomic host_animation_frame_requested_{0U}; std::atomic observed_host_timestamp_microseconds_{0U}; std::atomic observed_compositor_timestamp_microseconds_{0U}; @@ -2576,6 +2594,18 @@ uint8_t webscene_engine_set_accessibility_preferences_v1( : 0U; } +uint8_t webscene_engine_set_desktop_environment_v1( + webscene_engine* engine, + const webscene_desktop_environment_v1* environment) +{ + if (engine == nullptr || environment == nullptr) return 0U; + try { + return engine->set_desktop_environment(*environment) ? 1U : 0U; + } catch (...) { + return 0U; + } +} + namespace { struct semantic_snapshot_lease_v1 final { std::shared_ptr value; diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_native_engine.h b/experiments/WebScene.NativeEngine.Probe/native/webscene_native_engine.h index 8c64f66e8..bdf70d2e2 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_native_engine.h +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_native_engine.h @@ -99,6 +99,43 @@ typedef enum webscene_accessibility_preference_flags_v1 { WEBSCENE_ACCESSIBILITY_PREFERENCE_MORE_CONTRAST_V1 = 1U << 2U } webscene_accessibility_preference_flags_v1; +#define WEBSCENE_DESKTOP_ENVIRONMENT_VERSION_1 1U + +/* + * Immutable host-owned desktop state copied synchronously by + * webscene_engine_set_desktop_environment_v1. Colors are packed RGBA8. A host + * publishes a complete snapshot; omitted transitions are represented by + * clearing their flag. display_generation changes when the display topology + * changes, while dpi values describe the display containing the surface. + */ +typedef enum webscene_desktop_environment_flags_v1 { + WEBSCENE_DESKTOP_ENVIRONMENT_NONE_V1 = 0, + WEBSCENE_DESKTOP_ENVIRONMENT_FORCED_COLORS_V1 = 1U << 0U, + WEBSCENE_DESKTOP_ENVIRONMENT_HIGH_CONTRAST_V1 = 1U << 1U, + WEBSCENE_DESKTOP_ENVIRONMENT_SUSPENDED_V1 = 1U << 2U, + WEBSCENE_DESKTOP_ENVIRONMENT_DISPLAY_AVAILABLE_V1 = 1U << 3U, + WEBSCENE_DESKTOP_ENVIRONMENT_SESSION_LOCKED_V1 = 1U << 4U, + WEBSCENE_DESKTOP_ENVIRONMENT_SESSION_ENDING_V1 = 1U << 5U +} webscene_desktop_environment_flags_v1; + +typedef struct webscene_desktop_environment_v1 { + uint32_t struct_size; + uint32_t version; + uint32_t flags; + uint32_t preferred_color_scheme; + uint32_t dpi_x; + uint32_t dpi_y; + uint32_t scale_milli; + uint32_t reserved0; + uint64_t display_generation; + uint32_t accent_rgba; + uint32_t accent_text_rgba; + uint32_t canvas_rgba; + uint32_t canvas_text_rgba; + uint32_t highlight_rgba; + uint32_t highlight_text_rgba; +} webscene_desktop_environment_v1; + /* * Immutable, host-readable accessibility projection. Strings are UTF-8 * slices into semantic_snapshot_view.string_bytes. Node and relationship @@ -2107,6 +2144,15 @@ WEBSCENE_API uint8_t webscene_engine_set_preferred_color_scheme( WEBSCENE_API uint8_t webscene_engine_set_accessibility_preferences_v1( webscene_engine* engine, uint32_t preference_flags); +/* + * Publishes one complete desktop environment snapshot. The call rejects + * unknown flags, malformed dimensions, non-zero reserved data, and unsupported + * versions; it never retains host memory. Equal snapshots are coalesced and do + * not wake the worker. + */ +WEBSCENE_API uint8_t webscene_engine_set_desktop_environment_v1( + webscene_engine* engine, + const webscene_desktop_environment_v1* environment); /* * Requests an updated semantic publication and acquires the latest completed * immutable snapshot. The first call may return null while the engine worker diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_native_engine_lifecycle.inc b/experiments/WebScene.NativeEngine.Probe/native/webscene_native_engine_lifecycle.inc index 2ee4b0111..76b89ea45 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_native_engine_lifecycle.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_native_engine_lifecycle.inc @@ -503,6 +503,80 @@ return true; } + bool set_desktop_environment( + const webscene_desktop_environment_v1& environment) + { + constexpr auto supported_flags = + WEBSCENE_DESKTOP_ENVIRONMENT_FORCED_COLORS_V1 + | WEBSCENE_DESKTOP_ENVIRONMENT_HIGH_CONTRAST_V1 + | WEBSCENE_DESKTOP_ENVIRONMENT_SUSPENDED_V1 + | WEBSCENE_DESKTOP_ENVIRONMENT_DISPLAY_AVAILABLE_V1 + | WEBSCENE_DESKTOP_ENVIRONMENT_SESSION_LOCKED_V1 + | WEBSCENE_DESKTOP_ENVIRONMENT_SESSION_ENDING_V1; + if (environment.struct_size != sizeof(webscene_desktop_environment_v1) + || environment.version != WEBSCENE_DESKTOP_ENVIRONMENT_VERSION_1 + || (environment.flags & ~supported_flags) != 0U + || environment.reserved0 != 0U + || (environment.preferred_color_scheme + != WEBSCENE_PREFERRED_COLOR_SCHEME_LIGHT + && environment.preferred_color_scheme + != WEBSCENE_PREFERRED_COLOR_SCHEME_DARK) + || environment.dpi_x == 0U || environment.dpi_x > 10000U + || environment.dpi_y == 0U || environment.dpi_y > 10000U + || environment.scale_milli < 100U + || environment.scale_milli > 10000U) { + return false; + } + + { + std::lock_guard lock(desktop_environment_mutex_); + if (std::memcmp( + &desktop_environment_, &environment, + sizeof(webscene_desktop_environment_v1)) == 0) { + return true; + } + desktop_environment_ = environment; + } + + preferred_color_scheme_.store( + environment.preferred_color_scheme, std::memory_order_release); + auto accessibility = accessibility_preference_flags_.load( + std::memory_order_acquire); + accessibility &= ~( + WEBSCENE_ACCESSIBILITY_PREFERENCE_FORCED_COLORS_V1 + | WEBSCENE_ACCESSIBILITY_PREFERENCE_MORE_CONTRAST_V1); + if ((environment.flags + & WEBSCENE_DESKTOP_ENVIRONMENT_FORCED_COLORS_V1) != 0U) { + accessibility |= WEBSCENE_ACCESSIBILITY_PREFERENCE_FORCED_COLORS_V1; + } + if ((environment.flags + & WEBSCENE_DESKTOP_ENVIRONMENT_HIGH_CONTRAST_V1) != 0U) { + accessibility |= WEBSCENE_ACCESSIBILITY_PREFERENCE_MORE_CONTRAST_V1; + } + accessibility_preference_flags_.store( + accessibility, std::memory_order_release); + desktop_accent_rgba_.store(environment.accent_rgba, std::memory_order_release); + desktop_accent_text_rgba_.store( + environment.accent_text_rgba, std::memory_order_release); + desktop_canvas_rgba_.store(environment.canvas_rgba, std::memory_order_release); + desktop_canvas_text_rgba_.store( + environment.canvas_text_rgba, std::memory_order_release); + desktop_highlight_rgba_.store( + environment.highlight_rgba, std::memory_order_release); + desktop_highlight_text_rgba_.store( + environment.highlight_text_rgba, std::memory_order_release); + desktop_environment_published_.store(true, std::memory_order_release); + desktop_environment_changed_.store(true, std::memory_order_release); + signal_worker(); + return true; + } + + webscene_desktop_environment_v1 desktop_environment() const + { + std::lock_guard lock(desktop_environment_mutex_); + return desktop_environment_; + } + uint32_t cursor() const noexcept { return current_cursor_.load(std::memory_order_acquire); diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_native_engine_worker.inc b/experiments/WebScene.NativeEngine.Probe/native/webscene_native_engine_worker.inc index 181dbdc3d..a8f065db7 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_native_engine_worker.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_native_engine_worker.inc @@ -57,7 +57,14 @@ scheme == WEBSCENE_PREFERRED_COLOR_SCHEME_DARK, (accessibility & WEBSCENE_ACCESSIBILITY_PREFERENCE_FORCED_COLORS_V1) != 0U, (accessibility & WEBSCENE_ACCESSIBILITY_PREFERENCE_REDUCED_MOTION_V1) != 0U, - (accessibility & WEBSCENE_ACCESSIBILITY_PREFERENCE_MORE_CONTRAST_V1) != 0U}); + (accessibility & WEBSCENE_ACCESSIBILITY_PREFERENCE_MORE_CONTRAST_V1) != 0U, + desktop_environment_published_.load(std::memory_order_acquire), + desktop_accent_rgba_.load(std::memory_order_acquire), + desktop_accent_text_rgba_.load(std::memory_order_acquire), + desktop_canvas_rgba_.load(std::memory_order_acquire), + desktop_canvas_text_rgba_.load(std::memory_order_acquire), + desktop_highlight_rgba_.load(std::memory_order_acquire), + desktop_highlight_text_rgba_.load(std::memory_order_acquire)}); return webscene_native::v8_dom_runtime::viewport_metrics{ static_cast(viewport_width_), static_cast(viewport_height_), @@ -671,9 +678,18 @@ const auto accessibility_changed = accessibility_preferences_changed_.exchange( false, std::memory_order_acq_rel); - if (!scheme_changed && !accessibility_changed) { + const auto desktop_changed = desktop_environment_changed_.exchange( + false, + std::memory_order_acq_rel); + if (!scheme_changed && !accessibility_changed && !desktop_changed) { return false; } + if (desktop_changed && runtime_ != nullptr + && !runtime_->apply_desktop_environment( + desktop_environment())) { + script_errors_.fetch_add(1, std::memory_order_relaxed); + set_last_error(runtime_->last_error()); + } if (runtime_ != nullptr && !runtime_->refresh_media_environment()) { script_errors_.fetch_add(1, std::memory_order_relaxed); @@ -1942,6 +1958,8 @@ std::memory_order_acquire) || accessibility_preferences_changed_.load( std::memory_order_acquire) + || desktop_environment_changed_.load( + std::memory_order_acquire) || semantic_actions_pending_.load( std::memory_order_acquire) || semantic_delta_pending_.load( diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime.cpp b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime.cpp index 978958963..947702f8c 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime.cpp +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime.cpp @@ -7753,6 +7753,13 @@ bool v8_dom_runtime::set_visible(bool visible) && impl_->promote_pending_promise_error(); } +bool v8_dom_runtime::apply_desktop_environment( + const webscene_desktop_environment_v1& environment) +{ + return impl_->apply_desktop_environment(environment) + && impl_->promote_pending_promise_error(); +} + bool v8_dom_runtime::set_focused(bool focused) { return impl_->set_focused(focused) diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime.h b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime.h index 7dd5123b7..29a711b24 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime.h +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime.h @@ -518,6 +518,8 @@ class v8_dom_runtime final { bool has_open_gpu_output() const; void update_gpu_presentation_images(const std::vector>& images); bool refresh_media_environment(); + bool apply_desktop_environment( + const webscene_desktop_environment_v1& environment); bool set_visible(bool visible); bool set_focused(bool focused); bool set_fullscreen(bool fullscreen); 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 efc86d1bf..aa14d85c0 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 @@ -1573,6 +1573,93 @@ return true; } + bool apply_desktop_environment( + const webscene_desktop_environment_v1& environment) + { + auto isolate_locker = lock_shared_isolate(); + v8::Isolate::Scope isolate_scope(isolate); + v8::HandleScope handle_scope(isolate); + const auto previous = desktop_environment; + const auto initialized = desktop_environment_initialized; + desktop_environment = environment; + desktop_environment_initialized = true; + if (!initialized) return true; + + std::array events{}; + size_t event_count = 0U; + const auto transitioned = [&](uint32_t flag) { + return ((previous.flags ^ environment.flags) & flag) != 0U; + }; + if (transitioned(WEBSCENE_DESKTOP_ENVIRONMENT_SUSPENDED_V1)) { + events[event_count++] = (environment.flags + & WEBSCENE_DESKTOP_ENVIRONMENT_SUSPENDED_V1) != 0U + ? "suspend" : "resume"; + } + if (transitioned(WEBSCENE_DESKTOP_ENVIRONMENT_DISPLAY_AVAILABLE_V1)) { + events[event_count++] = "displayavailabilitychange"; + } + if (transitioned(WEBSCENE_DESKTOP_ENVIRONMENT_SESSION_LOCKED_V1)) { + events[event_count++] = (environment.flags + & WEBSCENE_DESKTOP_ENVIRONMENT_SESSION_LOCKED_V1) != 0U + ? "sessionlock" : "sessionunlock"; + } + if (transitioned(WEBSCENE_DESKTOP_ENVIRONMENT_SESSION_ENDING_V1) + && (environment.flags + & WEBSCENE_DESKTOP_ENVIRONMENT_SESSION_ENDING_V1) != 0U) { + events[event_count++] = "sessionending"; + } + if (previous.display_generation != environment.display_generation + || previous.dpi_x != environment.dpi_x + || previous.dpi_y != environment.dpi_y + || previous.scale_milli != environment.scale_milli) { + events[event_count++] = "displaychange"; + } + if (event_count == 0U) return true; + + auto local_context = context.Get(isolate); + if (local_context.IsEmpty()) return true; + v8::Context::Scope context_scope(local_context); + auto target = local_context->Global(); + v8::Local dispatcher; + if (!target->Get(local_context, js_string(isolate, "dispatchEvent")) + .ToLocal(&dispatcher) + || !dispatcher->IsFunction()) { + last_error = "Desktop environment dispatcher is unavailable"; + return false; + } + for (size_t index = 0U; index < event_count; ++index) { + auto event = create_event_instance(local_context); + event->Set(local_context, js_string(isolate, "type"), + js_string(isolate, events[index])).Check(); + event->Set(local_context, js_string(isolate, "bubbles"), + v8::False(isolate)).Check(); + event->Set(local_context, js_string(isolate, "cancelable"), + v8::False(isolate)).Check(); + event->Set(local_context, js_string(isolate, "displayAvailable"), + v8::Boolean::New(isolate, (environment.flags + & WEBSCENE_DESKTOP_ENVIRONMENT_DISPLAY_AVAILABLE_V1) != 0U)).Check(); + event->Set(local_context, js_string(isolate, "dpiX"), + v8::Integer::NewFromUnsigned(isolate, environment.dpi_x)).Check(); + event->Set(local_context, js_string(isolate, "dpiY"), + v8::Integer::NewFromUnsigned(isolate, environment.dpi_y)).Check(); + event->Set(local_context, js_string(isolate, "scale"), + v8::Number::New(isolate, + static_cast(environment.scale_milli) / 1000.0)).Check(); + v8::TryCatch try_catch(isolate); + v8::Local arguments[] = {event}; + if (dispatcher.As()->Call( + local_context, target, 1, arguments).IsEmpty()) { + last_error = std::string("Desktop environment ") + events[index] + + " dispatch failed: " + + describe_reported_exception(try_catch, local_context); + ++frame_script_error_count; + return false; + } + } + perform_microtask_checkpoint(); + return true; + } + bool set_focused(bool focused) { if (document_focused == focused) return true; 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 5eb9e298e..ba2816048 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_state.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_state.inc @@ -775,6 +775,8 @@ bool document_focused{true}; bool terminal_pagehide_dispatched{false}; bool window_close_lifecycle_dispatching{false}; + bool desktop_environment_initialized{false}; + webscene_desktop_environment_v1 desktop_environment{}; double last_animation_frame_timestamp_ms{0}; double caret_blink_epoch_ms{0}; std::string frame_last_error_value; diff --git a/experiments/WebScene.NativeEngine.Probe/tests/desktop_environment_header_tests.c b/experiments/WebScene.NativeEngine.Probe/tests/desktop_environment_header_tests.c new file mode 100644 index 000000000..20198d702 --- /dev/null +++ b/experiments/WebScene.NativeEngine.Probe/tests/desktop_environment_header_tests.c @@ -0,0 +1,26 @@ +#include "webscene_native_engine.h" + +#include +#include + +_Static_assert(WEBSCENE_DESKTOP_ENVIRONMENT_VERSION_1 == 1U, + "desktop environment version changed"); +_Static_assert(sizeof(webscene_desktop_environment_v1) == 64U, + "desktop environment ABI size changed"); +_Static_assert(offsetof(webscene_desktop_environment_v1, display_generation) == 32U, + "desktop environment display generation offset changed"); +_Static_assert(offsetof(webscene_desktop_environment_v1, highlight_text_rgba) == 60U, + "desktop environment color layout changed"); + +int main(void) +{ + webscene_desktop_environment_v1 environment = {0}; + environment.struct_size = sizeof(environment); + environment.version = WEBSCENE_DESKTOP_ENVIRONMENT_VERSION_1; + environment.flags = WEBSCENE_DESKTOP_ENVIRONMENT_DISPLAY_AVAILABLE_V1; + environment.preferred_color_scheme = WEBSCENE_PREFERRED_COLOR_SCHEME_LIGHT; + environment.dpi_x = 96U; + environment.dpi_y = 96U; + environment.scale_milli = 1000U; + return environment.reserved0 == 0U ? 0 : 1; +} 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 69526aed3..59e890b51 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 @@ -1631,6 +1631,55 @@ void test_preferred_color_scheme_updates_css_and_match_media(webscene_engine* en } } +void test_desktop_environment_ingress_is_bounded_and_updates_media( + webscene_engine* engine) +{ + webscene_desktop_environment_v1 environment{ + sizeof(webscene_desktop_environment_v1), + WEBSCENE_DESKTOP_ENVIRONMENT_VERSION_1, + WEBSCENE_DESKTOP_ENVIRONMENT_DISPLAY_AVAILABLE_V1, + WEBSCENE_PREFERRED_COLOR_SCHEME_LIGHT, + 96U, 96U, 1000U, 0U, 1U, + 0x0067c0ffU, 0xffffffffU, 0xffffffffU, + 0x000000ffU, 0x0067c0ffU, 0xffffffffU}; + require(webscene_engine_set_desktop_environment_v1(engine, &environment) != 0U, + "native engine rejected a valid desktop environment"); + for (uint32_t iteration = 0U; iteration < 10000U; ++iteration) { + require(webscene_engine_set_desktop_environment_v1(engine, &environment) != 0U, + "native engine rejected a duplicate desktop environment"); + } + auto invalid = environment; + invalid.version = WEBSCENE_DESKTOP_ENVIRONMENT_VERSION_1 + 1U; + require(webscene_engine_set_desktop_environment_v1(engine, &invalid) == 0U, + "native engine accepted a future desktop environment version"); + invalid = environment; + invalid.reserved0 = 1U; + require(webscene_engine_set_desktop_environment_v1(engine, &invalid) == 0U, + "native engine accepted non-zero reserved desktop environment data"); + + environment.flags |= WEBSCENE_DESKTOP_ENVIRONMENT_FORCED_COLORS_V1 + | WEBSCENE_DESKTOP_ENVIRONMENT_HIGH_CONTRAST_V1; + environment.preferred_color_scheme = WEBSCENE_PREFERRED_COLOR_SCHEME_DARK; + environment.display_generation = 2U; + environment.dpi_x = 144U; + environment.dpi_y = 144U; + environment.scale_milli = 1500U; + require(webscene_engine_set_desktop_environment_v1(engine, &environment) != 0U, + "native engine rejected a changed desktop environment"); + const auto state = evaluate(engine, R"JS( + ({ dark: matchMedia('(prefers-color-scheme: dark)').matches, + forced: matchMedia('(forced-colors: active)').matches, + contrast: matchMedia('(prefers-contrast: more)').matches }) + )JS", "native-desktop-environment-state.js"); + require(state == R"({"dark":true,"forced":true,"contrast":true})", + "desktop environment did not update media state: " + state); + + environment.flags = WEBSCENE_DESKTOP_ENVIRONMENT_DISPLAY_AVAILABLE_V1; + environment.preferred_color_scheme = WEBSCENE_PREFERRED_COLOR_SCHEME_LIGHT; + require(webscene_engine_set_desktop_environment_v1(engine, &environment) != 0U, + "native engine rejected desktop environment restoration"); +} + void test_resize_listener_receives_window_event(webscene_engine* engine) { execute(engine, R"JS( 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 43207a456..c8a10626f 100644 --- a/experiments/WebScene.NativeEngine.Probe/tests/native_v8_runtime_tests.cpp +++ b/experiments/WebScene.NativeEngine.Probe/tests/native_v8_runtime_tests.cpp @@ -1482,6 +1482,7 @@ int main() test_media_query_list_tracks_outer_and_frame_viewport_breakpoints(engine); test_responsive_unset_restores_auto_inset(engine); test_preferred_color_scheme_updates_css_and_match_media(engine); + test_desktop_environment_ingress_is_bounded_and_updates_media(engine); test_resize_listener_receives_window_event(engine); test_absolute_portal_centers_against_positioned_ancestor(engine); test_attribute_selector_invalidation(engine); diff --git a/src/WebScene.Backend.Avalonia/NativeSceneInteropTypes.cs b/src/WebScene.Backend.Avalonia/NativeSceneInteropTypes.cs index 4ea47fe66..fbb821e0b 100644 --- a/src/WebScene.Backend.Avalonia/NativeSceneInteropTypes.cs +++ b/src/WebScene.Backend.Avalonia/NativeSceneInteropTypes.cs @@ -60,6 +60,38 @@ internal enum NativeAccessibilityPreferences : uint MoreContrast = 1 << 2 } +[Flags] +internal enum NativeDesktopEnvironmentFlags : uint +{ + None = 0, + ForcedColors = 1 << 0, + HighContrast = 1 << 1, + Suspended = 1 << 2, + DisplayAvailable = 1 << 3, + SessionLocked = 1 << 4, + SessionEnding = 1 << 5 +} + +[StructLayout(LayoutKind.Sequential)] +internal struct NativeDesktopEnvironmentV1 +{ + internal uint StructSize; + internal uint Version; + internal NativeDesktopEnvironmentFlags Flags; + internal NativePreferredColorScheme PreferredColorScheme; + internal uint DpiX; + internal uint DpiY; + internal uint ScaleMilli; + internal uint Reserved0; + internal ulong DisplayGeneration; + internal uint AccentRgba; + internal uint AccentTextRgba; + internal uint CanvasRgba; + internal uint CanvasTextRgba; + internal uint HighlightRgba; + internal uint HighlightTextRgba; +} + /// Host accessibility preferences published to CSS media queries. [Flags] public enum WebSceneAccessibilityPreferences : uint diff --git a/src/WebScene.Backend.Avalonia/NativeSceneRuntime.cs b/src/WebScene.Backend.Avalonia/NativeSceneRuntime.cs index 26e0e4ef0..16e91a54b 100644 --- a/src/WebScene.Backend.Avalonia/NativeSceneRuntime.cs +++ b/src/WebScene.Backend.Avalonia/NativeSceneRuntime.cs @@ -2108,6 +2108,13 @@ internal static extern byte EngineSetAccessibilityPreferences( IntPtr engine, NativeAccessibilityPreferences preferences); + [DllImport( + LibraryName, + EntryPoint = "webscene_engine_set_desktop_environment_v1")] + internal static extern byte EngineSetDesktopEnvironmentV1( + IntPtr engine, + in NativeDesktopEnvironmentV1 environment); + [DllImport(LibraryName, EntryPoint = "webscene_scene_release")] public static extern void SceneRelease(IntPtr scene); diff --git a/tests/WebPlatformSubset/capabilities.json b/tests/WebPlatformSubset/capabilities.json index 8c2ec7cc5..3589d458c 100644 --- a/tests/WebPlatformSubset/capabilities.json +++ b/tests/WebPlatformSubset/capabilities.json @@ -380,10 +380,10 @@ }, { "family": "component-media-queries", - "required": ["all media type", "viewport min/max dimensions", "landscape orientation", "hover and any-hover", "pointer and any-pointer", "no-preference reduced motion", "host-provided preferred color scheme in CSS and matchMedia", "live MediaQueryList matches", "MediaQueryList change events", "legacy MediaQueryList listeners", "viewport breakpoint crossing idempotence", "targeted media subtree recascade"], + "required": ["all media type", "viewport min/max dimensions", "landscape orientation", "hover and any-hover", "pointer and any-pointer", "no-preference reduced motion", "host-provided preferred color scheme in CSS and matchMedia", "host-provided forced colors and contrast", "bounded desktop environment snapshots", "live MediaQueryList matches", "MediaQueryList change events", "legacy MediaQueryList listeners", "viewport breakpoint crossing idempotence", "targeted media subtree recascade"], "productEvidence": ["responsive chart controls", "pointer-specific component styles", "motion preference branches", "TradingView initial theme", "StackWich issues 1244, 1246, 1247, and 1248"], - "coverage": ["contracts/css-media-query-primitives.html", "contracts/media-query-list-dynamic-viewport.html", "contracts/media-query-targeted-subtree-recascade.html", "contracts/responsive-toolbar-breakpoint-navigation.html", "contracts/responsive-settings-back-navigation.html", "contracts/responsive-settings-fullscreen-resize.html", "contracts/responsive-go-to-dialog-sizing.html", "test_preferred_color_scheme_updates_css_and_match_media", "test_media_query_list_tracks_outer_and_frame_viewport_breakpoints", "test_media_query_resize_recascades_only_affected_subtrees", "NativeWebSceneThemeTests"], - "gap": "The contracts cover the exact media-query slice observed in the hosted-component corpus, including browser-authorized initial color preference, native host-driven light/dark recascade, and live viewport breakpoint transitions through modern and legacy MediaQueryList listeners. Print media, resolution, color-gamut, forced-colors, and the broader Media Queries specifications remain outside this claim." + "coverage": ["contracts/css-media-query-primitives.html", "contracts/css-accessibility-preferences.html", "contracts/media-query-list-dynamic-viewport.html", "contracts/media-query-targeted-subtree-recascade.html", "contracts/responsive-toolbar-breakpoint-navigation.html", "contracts/responsive-settings-back-navigation.html", "contracts/responsive-settings-fullscreen-resize.html", "contracts/responsive-go-to-dialog-sizing.html", "test_preferred_color_scheme_updates_css_and_match_media", "test_desktop_environment_ingress_is_bounded_and_updates_media", "test_media_query_list_tracks_outer_and_frame_viewport_breakpoints", "test_media_query_resize_recascades_only_affected_subtrees", "NativeWebSceneThemeTests"], + "gap": "The contracts cover the exact media-query slice observed in the hosted-component corpus, including native host-driven light/dark, forced-color and contrast recascade, bounded desktop snapshots, and live viewport breakpoint transitions through modern and legacy MediaQueryList listeners. Print media, resolution, color-gamut, and the broader Media Queries specifications remain outside this claim." }, { "family": "component-selectors",