Rewild Your Web
at main 764 lines 33 kB view raw
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)