Rewild Your Web
web
browser
dweb
1--- original
2+++ modified
3@@ -10,12 +10,15 @@
4 use std::time::{Duration, Instant};
5
6 use base::generic_channel::GenericCallback;
7-use constellation_traits::{KeyboardScroll, ScriptToConstellationMessage};
8+use base::id::WebViewId;
9+use constellation_traits::{
10+ EmbeddedWebViewEventType, KeyboardScroll, ScriptToConstellationMessage,
11+};
12 use embedder_traits::{
13 Cursor, EditingActionEvent, EmbedderMsg, ImeEvent, InputEvent, InputEventAndId,
14 InputEventResult, KeyboardEvent as EmbedderKeyboardEvent, MouseButton, MouseButtonAction,
15 MouseButtonEvent, MouseLeftViewportEvent, ScrollEvent, TouchEvent as EmbedderTouchEvent,
16- TouchEventType, TouchId, UntrustedNodeAddress, WheelEvent as EmbedderWheelEvent,
17+ TouchEventType, TouchId, UntrustedNodeAddress, WebViewPoint, WheelEvent as EmbedderWheelEvent,
18 };
19 #[cfg(feature = "gamepad")]
20 use embedder_traits::{
21@@ -27,8 +30,10 @@
22 use layout_api::{ScrollContainerQueryFlags, node_id_from_scroll_id};
23 use script_bindings::codegen::GenericBindings::DocumentBinding::DocumentMethods;
24 use script_bindings::codegen::GenericBindings::EventBinding::EventMethods;
25+#[cfg(feature = "gamepad")]
26 use script_bindings::codegen::GenericBindings::NavigatorBinding::NavigatorMethods;
27 use script_bindings::codegen::GenericBindings::NodeBinding::NodeMethods;
28+#[cfg(feature = "gamepad")]
29 use script_bindings::codegen::GenericBindings::PerformanceBinding::PerformanceMethods;
30 use script_bindings::codegen::GenericBindings::TouchBinding::TouchMethods;
31 use script_bindings::codegen::GenericBindings::WindowBinding::{ScrollBehavior, WindowMethods};
32@@ -47,12 +52,13 @@
33 use crate::dom::bindings::refcounted::Trusted;
34 use crate::dom::bindings::root::MutNullableDom;
35 use crate::dom::clipboardevent::ClipboardEventType;
36-use crate::dom::document::{FireMouseEventType, FocusInitiator};
37+use crate::dom::document::{Document, FireMouseEventType, FocusInitiator};
38 use crate::dom::event::{EventBubbles, EventCancelable, EventComposed, EventFlags};
39 #[cfg(feature = "gamepad")]
40 use crate::dom::gamepad::gamepad::{Gamepad, contains_user_gesture};
41 #[cfg(feature = "gamepad")]
42 use crate::dom::gamepad::gamepadevent::GamepadEventType;
43+use crate::dom::html::htmliframeelement::HTMLIFrameElement;
44 use crate::dom::inputevent::HitTestResult;
45 use crate::dom::node::{self, Node, NodeTraits, ShadowIncluding};
46 use crate::dom::pointerevent::PointerId;
47@@ -64,6 +70,7 @@
48 };
49 use crate::drag_data_store::{DragDataStore, Kind, Mode};
50 use crate::realms::enter_realm;
51+use crate::timers::OneshotTimerHandle;
52
53 /// A data structure used for tracking the current click count. This can be
54 /// reset to 0 if a mouse button event happens at a sufficient distance or time
55@@ -127,6 +134,56 @@
56 }
57 }
58
59+/// Long-press duration threshold for context menu
60+const LONG_PRESS_DURATION_MS: u64 = 400;
61+/// Maximum movement allowed during long-press detection (square of the value)
62+const LONG_PRESS_MOVE_THRESHOLD: f32 = 50.0;
63+
64+/// State for tracking an active long-press gesture for context menu.
65+#[derive(JSTraceable, MallocSizeOf)]
66+struct LongPressState {
67+ /// Timer handle for the long-press callback.
68+ timer: OneshotTimerHandle,
69+ /// Touch ID being tracked.
70+ #[no_trace]
71+ #[ignore_malloc_size_of = "TouchId is from embedder_traits"]
72+ touch_id: TouchId,
73+ /// Start point of the touch.
74+ #[no_trace]
75+ #[ignore_malloc_size_of = "Point2D is from euclid"]
76+ start_point: Point2D<f32, CSSPixel>,
77+}
78+
79+/// Callback structure for the long-press context menu timer.
80+#[derive(JSTraceable, MallocSizeOf)]
81+pub(crate) struct LongPressContextMenuCallback {
82+ #[ignore_malloc_size_of = "Document pointers are handled elsewhere"]
83+ pub(crate) document: Trusted<Document>,
84+ #[no_trace]
85+ #[ignore_malloc_size_of = "TouchId is from embedder_traits"]
86+ pub(crate) touch_id: TouchId,
87+ #[no_trace]
88+ #[ignore_malloc_size_of = "Point2D is from euclid"]
89+ pub(crate) point: Point2D<f32, CSSPixel>,
90+}
91+
92+impl LongPressContextMenuCallback {
93+ pub(crate) fn invoke(self, can_gc: CanGc) {
94+ let document = self.document.root();
95+ document
96+ .event_handler()
97+ .handle_long_press_context_menu(self.touch_id, self.point, can_gc);
98+ }
99+}
100+
101+/// Source of a context menu trigger, used to set appropriate PointerEvent values.
102+enum ContextMenuSource<'a> {
103+ /// Context menu triggered by mouse (right-click).
104+ Mouse(&'a ConstellationInputEvent),
105+ /// Context menu triggered by touch (long-press).
106+ Touch(TouchId),
107+}
108+
109 /// The [`DocumentEventHandler`] is a structure responsible for handling input events for
110 /// the [`crate::Document`] and storing data related to event handling. It exists to
111 /// decrease the size of the [`crate::Document`] structure.
112@@ -161,6 +218,20 @@
113 /// The active keyboard modifiers for the WebView. This is updated when receiving any input event.
114 #[no_trace]
115 active_keyboard_modifiers: Cell<Modifiers>,
116+ /// Long-press state for context menu detection.
117+ long_press_state: DomRefCell<Option<LongPressState>>,
118+ /// Touch ID that triggered a context menu via long-press.
119+ /// When this touch ends, we should return DefaultPrevented to prevent click synthesis.
120+ #[no_trace]
121+ #[ignore_malloc_size_of = "TouchId is from embedder_traits"]
122+ context_menu_touch_id: Cell<Option<TouchId>>,
123+ /// Touches that have been forwarded to embedded webviews.
124+ /// Maps TouchId to the embedded WebViewId so subsequent events for the same
125+ /// touch can be forwarded to the same webview even if hit testing returns
126+ /// something different.
127+ #[no_trace]
128+ #[ignore_malloc_size_of = "TouchId and WebViewId are from embedder_traits"]
129+ forwarded_touches: DomRefCell<Vec<(TouchId, WebViewId)>>,
130 }
131
132 impl DocumentEventHandler {
133@@ -177,6 +248,9 @@
134 current_cursor: Default::default(),
135 active_touch_points: Default::default(),
136 active_keyboard_modifiers: Default::default(),
137+ long_press_state: Default::default(),
138+ context_menu_touch_id: Default::default(),
139+ forwarded_touches: Default::default(),
140 }
141 }
142
143@@ -421,6 +495,198 @@
144 }
145 }
146
147+ /// Check if the hit test result landed on an embedded iframe, and if so, forward
148+ /// the input event to the embedded webview. Returns `true` if the event was forwarded
149+ /// (and should not be processed locally), `false` otherwise.
150+ fn forward_event_to_embedded_iframe_if_needed(
151+ &self,
152+ hit_test_result: &HitTestResult,
153+ input_event: &ConstellationInputEvent,
154+ ) -> bool {
155+ // Walk up from the hit node to find if any ancestor is an embedded iframe
156+ // We use ShadowIncluding::Yes because embedded iframes may be inside shadow DOM
157+ // (e.g., inside a custom element like <web-view>)
158+ let Some(embedded_iframe) = hit_test_result
159+ .node
160+ .inclusive_ancestors(ShadowIncluding::Yes)
161+ .find_map(|ancestor| {
162+ let iframe = DomRoot::downcast::<HTMLIFrameElement>(ancestor)?;
163+ if iframe.is_embedded_webview() {
164+ Some(iframe)
165+ } else {
166+ None
167+ }
168+ })
169+ else {
170+ return false;
171+ };
172+
173+ // Get the embedded webview ID
174+ let Some(embedded_webview_id) = embedded_iframe.embedded_webview_id() else {
175+ return false;
176+ };
177+
178+ // Get the iframe's border box to transform coordinates from parent to embedded viewport.
179+ // The border box origin is in document coordinates (relative to initial containing block).
180+ let Some(iframe_border_box) = embedded_iframe.upcast::<Node>().border_box() else {
181+ return false;
182+ };
183+
184+ // Convert iframe position from document coords to viewport coords by subtracting scroll offset.
185+ let scroll_offset = self.window.scroll_offset();
186+ let iframe_viewport_x = iframe_border_box.origin.x.to_f32_px() - scroll_offset.x as f32;
187+ let iframe_viewport_y = iframe_border_box.origin.y.to_f32_px() - scroll_offset.y as f32;
188+
189+ // Get device pixel ratio for converting between CSS and device pixels
190+ let device_pixel_ratio = self.window.device_pixel_ratio().get();
191+
192+ // Helper to transform a WebViewPoint by subtracting iframe's viewport position.
193+ // Device points need the offset scaled by device_pixel_ratio.
194+ // Page (CSS) points use the offset directly.
195+ let transform_point = |point: WebViewPoint| -> WebViewPoint {
196+ match point {
197+ WebViewPoint::Device(p) => {
198+ // Device pixels: scale the CSS offset by device pixel ratio
199+ let offset_x = iframe_viewport_x * device_pixel_ratio;
200+ let offset_y = iframe_viewport_y * device_pixel_ratio;
201+ WebViewPoint::Device(Point2D::new(p.x - offset_x, p.y - offset_y))
202+ },
203+ WebViewPoint::Page(p) => {
204+ // CSS pixels: use offset directly
205+ WebViewPoint::Page(Point2D::new(
206+ p.x - iframe_viewport_x,
207+ p.y - iframe_viewport_y,
208+ ))
209+ },
210+ }
211+ };
212+
213+ // Transform the input event to have coordinates relative to the embedded webview
214+ let transformed_event = match input_event.event.event.clone() {
215+ InputEvent::MouseMove(mut mouse_move) => {
216+ mouse_move.point = transform_point(mouse_move.point);
217+ InputEvent::MouseMove(mouse_move)
218+ },
219+ InputEvent::MouseButton(mut mouse_button) => {
220+ mouse_button.point = transform_point(mouse_button.point);
221+ InputEvent::MouseButton(mouse_button)
222+ },
223+ InputEvent::Touch(mut touch) => {
224+ touch.point = transform_point(touch.point);
225+ InputEvent::Touch(touch)
226+ },
227+ InputEvent::Wheel(mut wheel) => {
228+ wheel.point = transform_point(wheel.point);
229+ InputEvent::Wheel(wheel)
230+ },
231+ // For events without coordinates, just pass them through
232+ other => other,
233+ };
234+
235+ // Create the event with ID to forward to the embedded webview
236+ let event_with_id = InputEventAndId::from(transformed_event);
237+
238+ // Forward the event to the embedded webview via the Constellation
239+ self.window.send_to_constellation(
240+ ScriptToConstellationMessage::ForwardEventToEmbeddedWebView(
241+ embedded_webview_id,
242+ event_with_id,
243+ ),
244+ );
245+
246+ // Track forwarded touches so subsequent events for the same touch go to the same webview.
247+ // This is important because the hit test might return a different result for touchmove/touchend.
248+ if let InputEvent::Touch(touch) = &input_event.event.event {
249+ if touch.event_type == TouchEventType::Down {
250+ self.forwarded_touches
251+ .borrow_mut()
252+ .push((touch.id, embedded_webview_id));
253+ }
254+ }
255+
256+ // Notify the parent iframe element that input was received by the embedded webview,
257+ // but only for "activation" events (mousedown/touchstart), not for moves or other events.
258+ // This allows the parent document to track which embedded webview is "active".
259+ let is_activation_event = match &input_event.event.event {
260+ InputEvent::MouseButton(mouse_button) => mouse_button.action == MouseButtonAction::Down,
261+ InputEvent::Touch(touch) => touch.event_type == TouchEventType::Down,
262+ _ => false,
263+ };
264+ if is_activation_event {
265+ embedded_iframe.dispatch_embedded_webview_event(
266+ EmbeddedWebViewEventType::InputReceived,
267+ CanGc::note(),
268+ );
269+ }
270+
271+ true
272+ }
273+
274+ /// Forward a touch event to a specific embedded webview. This is used for subsequent
275+ /// touch events (move, end, cancel) after the initial touchstart was forwarded.
276+ fn forward_touch_event_to_webview(
277+ &self,
278+ webview_id: WebViewId,
279+ event: &EmbedderTouchEvent,
280+ _input_event: &ConstellationInputEvent,
281+ ) {
282+ // We need to find the iframe for this webview to get coordinate transformation info.
283+ // Search for the iframe with the matching embedded webview ID.
284+ let document = self.window.Document();
285+ let Some(embedded_iframe) = document
286+ .iframes()
287+ .iter()
288+ .find(|iframe| iframe.embedded_webview_id() == Some(webview_id))
289+ else {
290+ warn!(
291+ "Could not find iframe for embedded webview {:?}",
292+ webview_id
293+ );
294+ return;
295+ };
296+
297+ // Get the iframe's border box for coordinate transformation
298+ let Some(iframe_border_box) = embedded_iframe.upcast::<Node>().border_box() else {
299+ return;
300+ };
301+
302+ // Convert iframe position from document coords to viewport coords
303+ let scroll_offset = self.window.scroll_offset();
304+ let iframe_viewport_x = iframe_border_box.origin.x.to_f32_px() - scroll_offset.x as f32;
305+ let iframe_viewport_y = iframe_border_box.origin.y.to_f32_px() - scroll_offset.y as f32;
306+
307+ // Get device pixel ratio for coordinate conversion
308+ let device_pixel_ratio = self.window.device_pixel_ratio().get();
309+
310+ // Transform the touch point
311+ let transformed_point = match event.point {
312+ WebViewPoint::Device(p) => {
313+ let offset_x = iframe_viewport_x * device_pixel_ratio;
314+ let offset_y = iframe_viewport_y * device_pixel_ratio;
315+ WebViewPoint::Device(Point2D::new(p.x - offset_x, p.y - offset_y))
316+ },
317+ WebViewPoint::Page(p) => WebViewPoint::Page(Point2D::new(
318+ p.x - iframe_viewport_x,
319+ p.y - iframe_viewport_y,
320+ )),
321+ };
322+
323+ // Create transformed touch event
324+ let mut transformed_touch =
325+ EmbedderTouchEvent::new(event.event_type, event.id, transformed_point);
326+
327+ // Preserve the cancelable state from the original event
328+ if !event.is_cancelable() {
329+ transformed_touch.disable_cancelable();
330+ }
331+
332+ // Forward to the embedded webview
333+ let event_with_id = InputEventAndId::from(InputEvent::Touch(transformed_touch));
334+ self.window.send_to_constellation(
335+ ScriptToConstellationMessage::ForwardEventToEmbeddedWebView(webview_id, event_with_id),
336+ );
337+ }
338+
339 /// <https://w3c.github.io/uievents/#handle-native-mouse-move>
340 fn handle_native_mouse_move_event(&self, input_event: &ConstellationInputEvent, can_gc: CanGc) {
341 // Ignore all incoming events without a hit test.
342@@ -435,6 +701,57 @@
343 return;
344 }
345
346+ // Check if the hit target is an embedded iframe. If so, forward the event
347+ // to the embedded webview and don't process it locally.
348+ if self.forward_event_to_embedded_iframe_if_needed(&hit_test_result, input_event) {
349+ // Before returning, we need to update the hover state in the parent document.
350+ // The mouse is now over the embedded iframe, so we should clear hover from
351+ // any previous target and fire mouseout/mouseleave events.
352+ if let Some(old_target) = self.current_hover_target.get() {
353+ // Clear hover state on the old target and its ancestors
354+ for element in old_target
355+ .upcast::<Node>()
356+ .inclusive_ancestors(ShadowIncluding::No)
357+ .filter_map(DomRoot::downcast::<Element>)
358+ {
359+ element.set_hover_state(false);
360+ element.set_active_state(false);
361+ }
362+
363+ // Fire mouseout event on the old target
364+ MouseEvent::new_for_platform_motion_event(
365+ &self.window,
366+ FireMouseEventType::Out,
367+ &hit_test_result,
368+ input_event,
369+ can_gc,
370+ )
371+ .upcast::<Event>()
372+ .fire(old_target.upcast(), can_gc);
373+
374+ // Fire mouseleave events up the ancestor chain
375+ self.handle_mouse_enter_leave_event(
376+ DomRoot::from_ref(old_target.upcast::<Node>()),
377+ None, // No new target in the parent document
378+ FireMouseEventType::Leave,
379+ &hit_test_result,
380+ input_event,
381+ can_gc,
382+ );
383+
384+ // Clear the hover target since mouse is now in embedded iframe
385+ self.current_hover_target.set(None);
386+ }
387+
388+ // Release the parent's cursor claim by sending Default to the embedder.
389+ // This ensures the embedded iframe's cursor takes effect even if
390+ // the embedded's cursor hasn't changed (which would cause set_cursor
391+ // to short-circuit and not send a message).
392+ self.set_cursor(None);
393+
394+ return;
395+ }
396+
397 // Update the cursor when the mouse moves, if it has changed.
398 self.set_cursor(Some(hit_test_result.cursor));
399
400@@ -615,6 +932,12 @@
401 return;
402 };
403
404+ // Check if the hit target is an embedded iframe. If so, forward the event
405+ // to the embedded webview and don't process it locally.
406+ if self.forward_event_to_embedded_iframe_if_needed(&hit_test_result, input_event) {
407+ return;
408+ }
409+
410 debug!(
411 "{:?}: at {:?}",
412 event.action, hit_test_result.point_in_frame
413@@ -685,11 +1008,18 @@
414 let target_el = element.find_focusable_shadow_host_if_necessary();
415
416 let document = self.window.Document();
417- document.begin_focus_transaction();
418
419- // Try to focus `el`. If it's not focusable, focus the document instead.
420- document.request_focus(None, FocusInitiator::Local, can_gc);
421- document.request_focus(target_el.as_deref(), FocusInitiator::Local, can_gc);
422+ // Skip focus handling for hidefocus webviews - no blur/focus events
423+ // should be fired and focus should not be transferred.
424+ let hide_focus = self.window.as_global_scope().hide_focus();
425+
426+ if !hide_focus {
427+ document.begin_focus_transaction();
428+
429+ // Try to focus `el`. If it's not focusable, focus the document instead.
430+ document.request_focus(None, FocusInitiator::Local, can_gc);
431+ document.request_focus(target_el.as_deref(), FocusInitiator::Local, can_gc);
432+ }
433
434 // Step 7. Let result = dispatch event at target
435 let result = dom_event.dispatch(node.upcast(), false, can_gc);
436@@ -696,7 +1026,7 @@
437
438 // Step 8. If result is true and target is a focusable area
439 // that is click focusable, then Run the focusing steps at target.
440- if result && document.has_focus_transaction() {
441+ if !hide_focus && result && document.has_focus_transaction() {
442 document.commit_focus_transaction(FocusInitiator::Local, can_gc);
443 }
444
445@@ -706,7 +1036,7 @@
446 self.maybe_show_context_menu(
447 node.upcast(),
448 &hit_test_result,
449- input_event,
450+ ContextMenuSource::Mouse(input_event),
451 can_gc,
452 );
453 }
454@@ -817,9 +1147,30 @@
455 &self,
456 target: &EventTarget,
457 hit_test_result: &HitTestResult,
458- input_event: &ConstellationInputEvent,
459+ source: ContextMenuSource,
460 can_gc: CanGc,
461 ) {
462+ // Get pointer-specific values based on the source
463+ let (button, pressed_buttons, pointer_id, pointer_type, modifiers) = match source {
464+ ContextMenuSource::Mouse(input_event) => (
465+ 2i16, // right mouse button
466+ input_event.pressed_mouse_buttons,
467+ PointerId::Mouse as i32,
468+ DOMString::from("mouse"),
469+ input_event.active_keyboard_modifiers,
470+ ),
471+ ContextMenuSource::Touch(touch_id) => {
472+ let TouchId(id) = touch_id;
473+ (
474+ 0i16, // no mouse button for touch
475+ 0, // no pressed mouse buttons
476+ id, // use touch identifier as pointer_id
477+ DOMString::from("touch"),
478+ Modifiers::empty(),
479+ )
480+ },
481+ };
482+
483 // <https://w3c.github.io/uievents/#contextmenu>
484 let menu_event = PointerEvent::new(
485 &self.window, // window
486@@ -833,25 +1184,25 @@
487 hit_test_result
488 .point_relative_to_initial_containing_block
489 .to_i32(),
490- input_event.active_keyboard_modifiers,
491- 2i16, // button, right mouse button
492- input_event.pressed_mouse_buttons,
493- None, // related_target
494- None, // point_in_target
495- PointerId::Mouse as i32, // pointer_id
496- 1, // width
497- 1, // height
498- 0.5, // pressure
499- 0.0, // tangential_pressure
500- 0, // tilt_x
501- 0, // tilt_y
502- 0, // twist
503- PI / 2.0, // altitude_angle
504- 0.0, // azimuth_angle
505- DOMString::from("mouse"), // pointer_type
506- true, // is_primary
507- vec![], // coalesced_events
508- vec![], // predicted_events
509+ modifiers,
510+ button,
511+ pressed_buttons,
512+ None, // related_target
513+ None, // point_in_target
514+ pointer_id,
515+ 1, // width
516+ 1, // height
517+ 0.5, // pressure
518+ 0.0, // tangential_pressure
519+ 0, // tilt_x
520+ 0, // tilt_y
521+ 0, // twist
522+ PI / 2.0, // altitude_angle
523+ 0.0, // azimuth_angle
524+ pointer_type,
525+ true, // is_primary
526+ vec![], // coalesced_events
527+ vec![], // predicted_events
528 can_gc,
529 );
530
531@@ -867,6 +1218,89 @@
532 };
533 }
534
535+ /// Start the long-press timer for context menu detection.
536+ fn start_long_press_timer(&self, touch_id: TouchId, point: Point2D<f32, CSSPixel>) {
537+ // Cancel any existing timer first
538+ self.cancel_long_press_timer();
539+
540+ // Schedule the callback
541+ let callback = crate::timers::OneshotTimerCallback::LongPressContextMenu(
542+ LongPressContextMenuCallback {
543+ document: Trusted::new(&*self.window.Document()),
544+ touch_id,
545+ point,
546+ },
547+ );
548+
549+ let handle = self
550+ .window
551+ .as_global_scope()
552+ .schedule_callback(callback, Duration::from_millis(LONG_PRESS_DURATION_MS));
553+
554+ // Store the long-press state
555+ *self.long_press_state.borrow_mut() = Some(LongPressState {
556+ timer: handle,
557+ touch_id,
558+ start_point: point,
559+ });
560+ }
561+
562+ /// Cancel the long-press timer if one is active.
563+ fn cancel_long_press_timer(&self) {
564+ if let Some(state) = self.long_press_state.borrow_mut().take() {
565+ self.window
566+ .as_global_scope()
567+ .unschedule_callback(state.timer);
568+ }
569+ }
570+
571+ /// Handle the long-press context menu timer callback.
572+ pub(crate) fn handle_long_press_context_menu(
573+ &self,
574+ touch_id: TouchId,
575+ point: Point2D<f32, CSSPixel>,
576+ can_gc: CanGc,
577+ ) {
578+ // Only trigger if this touch is still the one we're tracking
579+ let is_tracked = self
580+ .long_press_state
581+ .borrow()
582+ .as_ref()
583+ .is_some_and(|state| state.touch_id == touch_id);
584+
585+ if !is_tracked {
586+ return;
587+ }
588+
589+ // Clear the long-press state
590+ *self.long_press_state.borrow_mut() = None;
591+
592+ // Track this touch so we can prevent click on touchend
593+ self.context_menu_touch_id.set(Some(touch_id));
594+
595+ // Hit test at the touch point
596+ let Some(hit_test_result) = self.window.hit_test_from_point_in_viewport(point) else {
597+ return;
598+ };
599+
600+ // Find the target element
601+ let Some(el) = hit_test_result
602+ .node
603+ .inclusive_ancestors(ShadowIncluding::Yes)
604+ .find_map(DomRoot::downcast::<Element>)
605+ else {
606+ return;
607+ };
608+
609+ // Fire the contextmenu PointerEvent with touch-specific values.
610+ self.maybe_show_context_menu(
611+ el.upcast(),
612+ &hit_test_result,
613+ ContextMenuSource::Touch(touch_id),
614+ can_gc,
615+ );
616+ }
617+
618 fn handle_touch_event(
619 &self,
620 event: EmbedderTouchEvent,
621@@ -873,6 +1307,29 @@
622 input_event: &ConstellationInputEvent,
623 can_gc: CanGc,
624 ) -> InputEventResult {
625+ // Check if this touch was previously forwarded to an embedded webview.
626+ // If so, continue forwarding to the same webview regardless of current hit test.
627+ // This ensures touch sequences stay with their original target.
628+ {
629+ let mut forwarded = self.forwarded_touches.borrow_mut();
630+ if let Some(pos) = forwarded.iter().position(|(id, _)| *id == event.id) {
631+ let (_, webview_id) = forwarded[pos];
632+
633+ // Forward this event to the same webview
634+ self.forward_touch_event_to_webview(webview_id, &event, input_event);
635+
636+ // Remove tracking on touchend/touchcancel
637+ if matches!(
638+ event.event_type,
639+ TouchEventType::Up | TouchEventType::Cancel
640+ ) {
641+ forwarded.swap_remove(pos);
642+ }
643+
644+ return InputEventResult::DefaultPrevented;
645+ }
646+ }
647+
648 // Ignore all incoming events without a hit test.
649 let Some(hit_test_result) = self.window.hit_test_from_input_event(input_event) else {
650 self.update_active_touch_points_when_early_return(event);
651@@ -879,6 +1336,16 @@
652 return Default::default();
653 };
654
655+ // Check if the hit target is an embedded iframe. If so, forward the event
656+ // to the embedded webview and don't process it locally.
657+ if self.forward_event_to_embedded_iframe_if_needed(&hit_test_result, input_event) {
658+ self.update_active_touch_points_when_early_return(event);
659+ // Return DefaultPrevented so the parent's compositor doesn't synthesize
660+ // a click for this touch sequence. The embedded webview's compositor will
661+ // handle click synthesis for the forwarded touch events.
662+ return InputEventResult::DefaultPrevented;
663+ }
664+
665 let TouchId(identifier) = event.id;
666 let event_name = match event.event_type {
667 TouchEventType::Down => "touchstart",
668@@ -918,8 +1385,31 @@
669 self.active_touch_points
670 .borrow_mut()
671 .push(Dom::from_ref(&*touch));
672+
673+ // Start the long-press timer for context menu detection
674+ self.start_long_press_timer(event.id, hit_test_result.point_in_frame);
675 },
676 TouchEventType::Move => {
677+ // Check if this is the tracked touch and if moved too far
678+ let should_cancel = self
679+ .long_press_state
680+ .borrow()
681+ .as_ref()
682+ .is_some_and(|state| {
683+ if state.touch_id == event.id {
684+ let dx = hit_test_result.point_in_frame.x - state.start_point.x;
685+ let dy = hit_test_result.point_in_frame.y - state.start_point.y;
686+ let distance = dx * dx + dy * dy;
687+ distance > LONG_PRESS_MOVE_THRESHOLD
688+ } else {
689+ false
690+ }
691+ });
692+
693+ if should_cancel {
694+ self.cancel_long_press_timer();
695+ }
696+
697 // Replace an existing touch point
698 let mut active_touch_points = self.active_touch_points.borrow_mut();
699 match active_touch_points
700@@ -931,6 +1421,17 @@
701 }
702 },
703 TouchEventType::Up | TouchEventType::Cancel => {
704+ // Cancel the long-press timer if this is the tracked touch
705+ let should_cancel = self
706+ .long_press_state
707+ .borrow()
708+ .as_ref()
709+ .is_some_and(|state| state.touch_id == event.id);
710+
711+ if should_cancel {
712+ self.cancel_long_press_timer();
713+ }
714+
715 // Remove an existing touch point
716 let mut active_touch_points = self.active_touch_points.borrow_mut();
717 match active_touch_points
718@@ -973,6 +1474,19 @@
719
720 let event = touch_event.upcast::<Event>();
721 event.fire(&target, can_gc);
722+
723+ // If this touch triggered a context menu via long-press, prevent click synthesis
724+ if let InputEvent::Touch(ref touch_ev) = input_event.event.event {
725+ if matches!(
726+ touch_ev.event_type,
727+ TouchEventType::Up | TouchEventType::Cancel
728+ ) && self.context_menu_touch_id.get() == Some(touch_ev.id)
729+ {
730+ self.context_menu_touch_id.set(None);
731+ return InputEventResult::DefaultPrevented;
732+ }
733+ }
734+
735 event.flags().into()
736 }
737
738@@ -1158,6 +1672,16 @@
739 return Default::default();
740 };
741
742+ // Check if the hit target is an embedded iframe. If so, forward the event
743+ // to the embedded webview and don't process it locally.
744+ if self.forward_event_to_embedded_iframe_if_needed(&hit_test_result, input_event) {
745+ // Return DefaultPrevented to stop the parent from scrolling.
746+ // The embedded webview will handle the scroll independently.
747+ // TODO: Implement proper scroll chaining where scroll bubbles back to parent
748+ // when embedded iframe reaches its scroll limit.
749+ return InputEventResult::DefaultPrevented;
750+ }
751+
752 let Some(el) = hit_test_result
753 .node
754 .inclusive_ancestors(ShadowIncluding::Yes)