Rewild Your Web
web browser dweb
at main 754 lines 32 kB view raw
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)