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