Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions docs/validation/nativeweb-pointer-device-807-298.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# NativeWeb pointer device adapter contract (#807 / AppScene #298)

## Boundary

AppScene's installed NativeWeb adapter previously reduced every
`ws_input_event` to the legacy `document::pointer` overload. The unchanged Code
OSS V8 runtime does not use this adapter, but the installed SDK must preserve
the same pointer metadata for other AppScene applications.

## Implemented slice

- Adds an additive `pointer_input` descriptor with mouse/touch/pen, stable ID
1..15, and primary state.
- Keeps the existing `document::pointer` symbol and behavior as primary mouse
ID 1.
- Appends device, ID, and primary fields to the public event prefix without
shifting existing fields.
- Tracks pressed targets in a fixed 16-entry array so cancellation and release
retire only the matching contact. Only primary input drives the legacy
`:active` slot; touch never creates sticky mouse hover.
- Rejects invalid device values and IDs before DOM dispatch.

## Authored gates

The NativeWeb contract covers legacy identity, touch and pen identity, primary
state, two simultaneous contact IDs, matching cancellation, cancel-without-
activation, malformed IDs, and a 10,000-move bounded-state sequence.

## Performance and evidence

Metadata adds no DOM scan, layout, scene publication, timer, browser component,
hash table, or heap-backed contact registry. Implementation and gates are
authored from source review. Per the active fast direction, no local build,
test, package, performance, memory, or lifecycle command was executed.
17 changes: 17 additions & 0 deletions src/WebScene.NativeWeb/include/webscene/native_web.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,12 @@ struct rule {
std::optional<bool> reduced_motion;
};
struct input_modifiers {bool shift{},control{},alt{},meta{};};
enum class pointer_device : uint8_t { mouse, touch, pen };
struct pointer_input {
pointer_device device{pointer_device::mouse};
uint32_t pointer_id{1};
bool primary{true};
};
struct event {
std::string type;
node_id target{}, current_target{};
Expand All @@ -349,6 +355,10 @@ struct event {
std::string data;
std::string input_type;
std::string key;
// Appended to preserve the prefix layout used by existing native clients.
pointer_device device{pointer_device::mouse};
uint32_t pointer_id{1};
bool is_primary{true};
void stop_propagation() { propagation_stopped = true; }
void prevent_default() { default_prevented = true; }
};
Expand Down Expand Up @@ -434,6 +444,10 @@ class document {
std::string property_name = {}, float elapsed_time_seconds = 0,
input_modifiers modifiers = {}, std::string data = {}, std::string input_type = {}, std::string key = {});
void pointer(std::string type, float x, float y, uint32_t buttons = 1, input_modifiers = {});
// Additive contact-aware path. The legacy overload remains primary mouse
// pointer 1 and keeps its existing source and binary symbol.
void pointer(std::string type, float x, float y, uint32_t buttons,
input_modifiers, pointer_input);
void wheel(float x, float y, float delta_y, input_modifiers = {});
void focus(node_id);
// Structural modal scope for a connected HTML dialog. The application chooses
Expand Down Expand Up @@ -475,6 +489,9 @@ class document {
bool disposed() const noexcept;

private:
bool dispatch_impl(node_id, std::string, float, float, float, uint32_t,
std::string, float, input_modifiers, std::string,
std::string, std::string, pointer_input);
void activate(node_id,float,float,uint32_t,input_modifiers);
std::shared_ptr<document_state> state_;
};
Expand Down
43 changes: 37 additions & 6 deletions src/WebScene.NativeWeb/src/native_web.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ struct document_state {
uint64_t rendered_scene_generation{};
bool keyboard_modality{true};
node_id focus{}, hover{}, pressed{}, body_id{};
std::array<node_id, 16> pointer_pressed{};
std::map<node_id,std::string> uncommitted_text;
uint64_t next_listener{1};
std::mutex listener_mutex;
Expand Down Expand Up @@ -220,6 +221,8 @@ void document::remove(node_id id) {
state_->hover = 0;
if (std::ranges::find(removed, state_->pressed) != removed.end())
state_->pressed = 0;
for (auto &pressed : state_->pointer_pressed)
if (std::ranges::find(removed, pressed) != removed.end()) pressed = 0;
std::erase_if(state_->rules, [&](const auto &r) {
return r.inline_target &&
std::ranges::find(removed, r.inline_target) != removed.end();
Expand Down Expand Up @@ -258,6 +261,15 @@ subscription document::on(node_id node, std::string type,
return subscription(state_, id);
}
bool document::dispatch(node_id target, std::string type, float client_x, float client_y, float delta_y, uint32_t buttons, std::string property_name, float elapsed_time_seconds, input_modifiers modifiers, std::string data, std::string input_type, std::string key) {
return dispatch_impl(target, std::move(type), client_x, client_y, delta_y,
buttons, std::move(property_name), elapsed_time_seconds,
modifiers, std::move(data), std::move(input_type),
std::move(key), {});
}
bool document::dispatch_impl(node_id target, std::string type, float client_x,
float client_y, float delta_y, uint32_t buttons, std::string property_name,
float elapsed_time_seconds, input_modifiers modifiers, std::string data,
std::string input_type, std::string key, pointer_input pointer) {
auto &n = state_->node(target);
std::vector<node_id> path;
for (auto *p = &n; p; p = p->parent)
Expand All @@ -271,6 +283,7 @@ bool document::dispatch(node_id target, std::string type, float client_x, float
e.elapsed_time_seconds = elapsed_time_seconds;
e.modifiers = modifiers;
e.data=std::move(data);e.input_type=std::move(input_type);e.key=std::move(key);
e.device=pointer.device;e.pointer_id=pointer.pointer_id;e.is_primary=pointer.primary;
for (auto id : path) {
if (!state_->alive)
return false;
Expand Down Expand Up @@ -559,35 +572,51 @@ void document::wheel(float x, float y, float delta_y, input_modifiers modifiers)
}
}
void document::pointer(std::string type, float x, float y, uint32_t buttons, input_modifiers modifiers) {
pointer(std::move(type), x, y, buttons, modifiers, {});
}
void document::pointer(std::string type, float x, float y, uint32_t buttons,
input_modifiers modifiers, pointer_input pointer) {
state_->check();
if (pointer.device != pointer_device::mouse
&& pointer.device != pointer_device::touch
&& pointer.device != pointer_device::pen)
throw std::invalid_argument("invalid pointer device");
if (pointer.pointer_id == 0 || pointer.pointer_id >= state_->pointer_pressed.size())
throw std::invalid_argument("pointer id must be in 1..15");
auto *n = state_->dom.hit_test(state_->dom.body(), x, y);
auto id = n ? n->id : 0;
if (state_->hover != id) {
if (pointer.device == pointer_device::mouse && state_->hover != id) {
state_->hover = id;
state_->styles_dirty = true;
state_->dom.mark_dirty();
}
if (type == "pointerdown" || type == "pointerup") {
for (auto *p = n; p; p = p->parent)
if (p->tag == "button" && p->attributes.contains("disabled")) {
state_->pressed = 0;
const auto prior = std::exchange(
state_->pointer_pressed[pointer.pointer_id], 0);
if (pointer.primary && state_->pressed == prior) state_->pressed = 0;
return;
}
}
if (type == "pointerdown") {
state_->keyboard_modality = false;
// Only the primary button participates in click activation.
// Auxiliary presses still reach native handlers (for example CAD panning).
state_->pressed = (buttons & 1u) ? id : 0;
state_->pointer_pressed[pointer.pointer_id] = (buttons & 1u) ? id : 0;
if (pointer.primary) state_->pressed = state_->pointer_pressed[pointer.pointer_id];
state_->styles_dirty = true;
state_->dom.mark_dirty();
}
if (type == "pointercancel") {
state_->pressed = 0;
const auto prior = std::exchange(
state_->pointer_pressed[pointer.pointer_id], 0);
if (pointer.primary && state_->pressed == prior) state_->pressed = 0;
state_->styles_dirty = true;
state_->dom.mark_dirty();
}
const bool default_allowed = !id || dispatch(id, type, x, y, 0, buttons, {}, 0, modifiers);
const bool default_allowed = !id || dispatch_impl(
id, type, x, y, 0, buttons, {}, 0, modifiers, {}, {}, {}, pointer);
if (!state_->alive)
return;
if (type == "pointerdown" && default_allowed) {
Expand All @@ -599,7 +628,9 @@ void document::pointer(std::string type, float x, float y, uint32_t buttons, inp
}
}
if (type == "pointerup") {
const auto pressed = std::exchange(state_->pressed, 0);
const auto pressed = std::exchange(
state_->pointer_pressed[pointer.pointer_id], 0);
if (pointer.primary && state_->pressed == pressed) state_->pressed = 0;
state_->styles_dirty = true;
state_->dom.mark_dirty();
if (id && id == pressed && state_->dom.find_by_native_id(id))
Expand Down
45 changes: 45 additions & 0 deletions tests/NativeWeb/contracts.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -977,6 +977,51 @@ int main() {
d.pointer("pointerdown", bounds.x + 1, bounds.y + 1, 1);
d.pointer("pointerup", bounds.x + 1, bounds.y + 1, 0);
check(clicks == 1, "primary button still activates after cancellation");

unsigned identity_events = 0;
bool legacy_identity = false, touch_identity = false;
bool pen_identity = false, pen_canceled = false;
auto pointer_identity = d.on(target, "pointerdown", [&](event &event) {
++identity_events;
legacy_identity = legacy_identity
|| (event.device == pointer_device::mouse
&& event.pointer_id == 1 && event.is_primary);
touch_identity = touch_identity
|| (event.device == pointer_device::touch
&& event.pointer_id == 7 && event.is_primary);
pen_identity = pen_identity
|| (event.device == pointer_device::pen
&& event.pointer_id == 2 && !event.is_primary);
});
auto pointer_cancel = d.on(target, "pointercancel", [&](event &event) {
pen_canceled = event.device == pointer_device::pen
&& event.pointer_id == 2 && !event.is_primary;
});
d.pointer("pointerdown", bounds.x + 1, bounds.y + 1, 1);
d.pointer("pointercancel", bounds.x + 1, bounds.y + 1, 0);
d.pointer("pointerdown", bounds.x + 1, bounds.y + 1, 1, {},
{pointer_device::touch, 7, true});
d.pointer("pointerdown", bounds.x + 1, bounds.y + 1, 1, {},
{pointer_device::pen, 2, false});
d.pointer("pointercancel", bounds.x + 1, bounds.y + 1, 0, {},
{pointer_device::pen, 2, false});
d.pointer("pointerup", bounds.x + 1, bounds.y + 1, 0, {},
{pointer_device::touch, 7, true});
check(identity_events == 3 && legacy_identity && touch_identity
&& pen_identity && pen_canceled,
"native pointer metadata preserves legacy/touch/pen identity");
bool invalid_pointer_rejected = false;
try {
d.pointer("pointermove", bounds.x + 1, bounds.y + 1, 0, {},
{pointer_device::touch, 0, true});
} catch (const std::invalid_argument &) {
invalid_pointer_rejected = true;
}
check(invalid_pointer_rejected,
"native pointer metadata rejects an out-of-range ID");
for (unsigned index = 0; index < 10000; ++index)
d.pointer("pointermove", bounds.x + 1, bounds.y + 1, 0, {},
{pointer_device::touch, 7, true});
}
check(d.bounds(d.find("numeric-length")).width == 5.f &&
d.bounds(d.find("numeric-length")).height == .75f,
Expand Down
2 changes: 1 addition & 1 deletion tests/WebScene.Css.Tests/CssPropertyStorageTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ public sealed class CssPropertyStorageTests
[Fact]
public void KnownPropertyCatalogHasStableCaseInsensitiveIds()
{
Assert.Equal(106, CssKnownProperties.Count);
Assert.Equal(119, CssKnownProperties.Count);
Assert.Equal(CssGeneratedPropertyMetadata.KnownNames, CssKnownProperties.Names);

for (var index = 0; index < CssKnownProperties.Names.Length; index++)
Expand Down
3 changes: 2 additions & 1 deletion tests/WebScene.Css.Tests/CssStylesheetCompilerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ public void PreservesScrollbarThumbInteractionStateAfterParserProtection()
static pseudo => !pseudo.IsElement && pseudo.Name == "active");
Assert.Contains(result.Rules[1].Declarations,
static declaration => declaration.Name == "background-color"
&& declaration.Value == "blue" && declaration.Important);
&& declaration.Value == "rgba(0, 0, 255, 1)"
&& declaration.Important);
}

[Fact]
Expand Down
Loading