Skip to main content

paint/
webview_renderer.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::cell::Cell;
6use std::collections::hash_map::Entry;
7use std::rc::Rc;
8
9use crossbeam_channel::Sender;
10use embedder_traits::{
11    AnimationState, InputEvent, InputEventAndId, InputEventId, InputEventResult, MouseButton,
12    MouseButtonAction, MouseButtonEvent, MouseMoveEvent, PaintHitTestResult, Scroll, TouchEvent,
13    TouchEventType, ViewportDetails, WebViewPoint, WheelEvent,
14};
15use euclid::{Scale, Size2D, Vector2D};
16use log::{debug, warn};
17use malloc_size_of::MallocSizeOf;
18use paint_api::display_list::ScrollType;
19use paint_api::viewport_description::{
20    DEFAULT_PAGE_ZOOM, MAX_PAGE_ZOOM, MIN_PAGE_ZOOM, ViewportDescription,
21};
22use paint_api::{PipelineExitSource, SendableFrameTree, WebViewTrait};
23use rustc_hash::FxHashMap;
24use servo_base::id::{PipelineId, WebViewId};
25use servo_constellation_traits::{
26    EmbedderToConstellationMessage, ScrollStateUpdate, WindowSizeType,
27};
28use servo_geometry::DeviceIndependentPixel;
29use style_traits::CSSPixel;
30use webrender::RenderApi;
31use webrender_api::units::{DevicePixel, DevicePoint, DeviceRect, DeviceVector2D, LayoutVector2D};
32use webrender_api::{DocumentId, ExternalScrollId, ScrollLocation};
33
34use crate::paint::RepaintReason;
35use crate::painter::Painter;
36use crate::pinch_zoom::PinchZoom;
37use crate::pipeline_details::PipelineDetails;
38use crate::refresh_driver::BaseRefreshDriver;
39use crate::touch::{
40    PanPolicyInput, PendingTouchInputEvent, TouchHandler, TouchIdMoveTracking, TouchMoveAllowed,
41    TouchSequenceState,
42};
43
44#[derive(Clone, Copy)]
45pub(crate) struct ScrollEvent {
46    /// Scroll by this offset, or to Start or End
47    pub scroll: Scroll,
48    /// Scroll the scroll node that is found at this point.
49    pub point: DevicePoint,
50    /// The kind of input that originated this scroll. `Touch` respects `touch-action`
51    /// restrictions; `InputEvents` (mouse wheel, keyboard) does not.
52    pub scroll_type: ScrollType,
53}
54
55#[derive(Clone, Copy)]
56pub(crate) enum ScrollZoomEvent {
57    /// A pinch zoom event that magnifies the view by the given factor from the given
58    /// center point.
59    PinchZoom(f32, DevicePoint),
60    /// A scroll event that scrolls the scroll node at the given location by the
61    /// given amount.
62    Scroll(ScrollEvent),
63}
64
65#[derive(Clone, Debug)]
66pub(crate) struct ScrollResult {
67    pub hit_test_result: PaintHitTestResult,
68    /// The [`ExternalScrollId`] of the node that was actually scrolled.
69    ///
70    /// Note that this is an inclusive ancestor of `external_scroll_id` in
71    /// [`Self::hit_test_result`].
72    pub external_scroll_id: ExternalScrollId,
73    pub offset: LayoutVector2D,
74}
75
76#[derive(Debug, PartialEq)]
77pub(crate) enum PinchZoomResult {
78    DidPinchZoom,
79    DidNotPinchZoom,
80}
81
82/// A renderer for a libservo `WebView`. This is essentially the [`ServoRenderer`]'s interface to a
83/// libservo `WebView`, but the code here cannot depend on libservo in order to prevent circular
84/// dependencies, which is why we store a `dyn WebViewTrait` here instead of the `WebView` itself.
85pub(crate) struct WebViewRenderer {
86    /// The [`WebViewId`] of the `WebView` associated with this [`WebViewDetails`].
87    pub id: WebViewId,
88    /// The renderer's view of the embedding layer `WebView` as a trait implementation,
89    /// so that the renderer doesn't need to depend on the embedding layer. This avoids
90    /// a dependency cycle.
91    pub webview: Box<dyn WebViewTrait>,
92    /// The root [`PipelineId`] of the currently displayed page in this WebView.
93    pub root_pipeline_id: Option<PipelineId>,
94    /// The rectangle of the [`WebView`] in device pixels, which is the viewport.
95    pub rect: DeviceRect,
96    /// Tracks details about each active pipeline that `Paint` knows about.
97    pub pipelines: FxHashMap<PipelineId, PipelineDetails>,
98    /// Pending scroll/zoom events.
99    pending_scroll_zoom_events: Vec<ScrollZoomEvent>,
100    /// A map of pending wheel events. These are events that have been sent to script,
101    /// but are waiting for processing. When they are handled by script, they may trigger
102    /// scroll events depending on whether `preventDefault()` was called on the event.
103    pending_wheel_events: FxHashMap<InputEventId, WheelEvent>,
104    /// Touch input state machine
105    touch_handler: TouchHandler,
106    /// "Desktop-style" zoom that resizes the viewport to fit the window.
107    pub page_zoom: Scale<f32, CSSPixel, DeviceIndependentPixel>,
108    /// "Mobile-style" zoom that does not reflow the page. When there is no [`PinchZoom`] a
109    /// zoom factor of 1.0 is implied and the [`PinchZoom::transform`] will be the identity.
110    pinch_zoom: PinchZoom,
111    /// The HiDPI scale factor for the `WebView` associated with this renderer. This is controlled
112    /// by the embedding layer.
113    hidpi_scale_factor: Scale<f32, DeviceIndependentPixel, DevicePixel>,
114    /// Whether or not this [`WebViewRenderer`] is hidden.
115    hidden: bool,
116    /// Whether or not this [`WebViewRenderer`] isn't throttled and has a pipeline with
117    /// active animations or animation frame callbacks.
118    animating: bool,
119    /// A [`ViewportDescription`] for this [`WebViewRenderer`], which contains the limitations
120    /// and initial values for zoom derived from the `viewport` meta tag in web content.
121    viewport_description: ViewportDescription,
122
123    /// The dimensions of the screen on which this WebView is rendering.
124    screen_size: Size2D<f32, DevicePixel>,
125
126    //
127    // Data that is shared with the parent renderer.
128    //
129    /// The channel on which messages can be sent to the constellation.
130    embedder_to_constellation_sender: Sender<EmbedderToConstellationMessage>,
131    /// The [`BaseRefreshDriver`] which manages the painting of `WebView`s during animations.
132    refresh_driver: Rc<BaseRefreshDriver>,
133    /// The active webrender document.
134    webrender_document: DocumentId,
135}
136
137impl WebViewRenderer {
138    pub(crate) fn new(
139        renderer_webview: Box<dyn WebViewTrait>,
140        viewport_details: ViewportDetails,
141        embedder_to_constellation_sender: Sender<EmbedderToConstellationMessage>,
142        refresh_driver: Rc<BaseRefreshDriver>,
143        webrender_document: DocumentId,
144    ) -> Self {
145        let hidpi_scale_factor = viewport_details.hidpi_scale_factor;
146        let size = viewport_details.size * viewport_details.hidpi_scale_factor;
147        let rect = DeviceRect::from_origin_and_size(DevicePoint::origin(), size);
148        let webview_id = renderer_webview.id();
149        Self {
150            id: webview_id,
151            webview: renderer_webview,
152            root_pipeline_id: None,
153            rect,
154            pipelines: Default::default(),
155            touch_handler: TouchHandler::new(webview_id),
156            pending_scroll_zoom_events: Default::default(),
157            pending_wheel_events: Default::default(),
158            page_zoom: DEFAULT_PAGE_ZOOM,
159            pinch_zoom: PinchZoom::new(rect),
160            hidpi_scale_factor: Scale::new(hidpi_scale_factor.0),
161            hidden: false,
162            animating: false,
163            viewport_description: Default::default(),
164            screen_size: viewport_details.device_size,
165            embedder_to_constellation_sender,
166            refresh_driver,
167            webrender_document,
168        }
169    }
170
171    fn hit_test(&self, webrender_api: &RenderApi, point: DevicePoint) -> Vec<PaintHitTestResult> {
172        Painter::hit_test_at_point_with_api_and_document(
173            webrender_api,
174            self.webrender_document,
175            point,
176        )
177    }
178
179    pub(crate) fn animation_callbacks_running(&self) -> bool {
180        self.pipelines
181            .values()
182            .any(PipelineDetails::animation_callbacks_running)
183    }
184
185    /// Whether this [`WebViewRenderer`] is animating. Note that hidden `WebView`s
186    /// are never considered to be animating.
187    pub(crate) fn animating(&self) -> bool {
188        self.animating && !self.hidden()
189    }
190
191    pub(crate) fn hidden(&self) -> bool {
192        self.hidden
193    }
194
195    /// Set whether this [`WebViewRenderer`] is in the hidden state or not. Return `true` if the
196    /// value changed or `false` otherwise.
197    pub(crate) fn set_hidden(&mut self, new_value: bool) -> bool {
198        let old_value = std::mem::replace(&mut self.hidden, new_value);
199        if new_value == old_value {
200            return false;
201        }
202
203        let _ = self.embedder_to_constellation_sender.send(
204            EmbedderToConstellationMessage::SetWebViewHidden(self.id, new_value),
205        );
206        self.update_animation_state();
207        true
208    }
209
210    /// Returns the [`PipelineDetails`] for the given [`PipelineId`], creating it if needed.
211    pub(crate) fn ensure_pipeline_details(
212        &mut self,
213        pipeline_id: PipelineId,
214    ) -> &mut PipelineDetails {
215        self.pipelines
216            .entry(pipeline_id)
217            .or_insert_with(PipelineDetails::new)
218    }
219
220    /// Record that `source` has reported `pipeline_id` as exited. Returns `true`
221    /// once every source has done so and the pipeline details have been discarded,
222    /// so that the caller can also drop the pipeline's scene state in WebRender.
223    pub(crate) fn pipeline_exited(
224        &mut self,
225        pipeline_id: PipelineId,
226        source: PipelineExitSource,
227    ) -> bool {
228        let pipeline = self.pipelines.entry(pipeline_id);
229        let Entry::Occupied(mut pipeline) = pipeline else {
230            return false;
231        };
232
233        pipeline.get_mut().exited.insert(source);
234
235        // Do not remove pipeline details until both the Constellation and Script have
236        // finished processing the pipeline shutdown. This prevents any followup messges
237        // from re-adding the pipeline details and creating a zombie.
238        if !pipeline.get().exited.is_all() {
239            return false;
240        }
241
242        // Flush the LCP candidates when exiting pipeline.
243        pipeline.get_mut().lcp_candidates.clear();
244
245        pipeline.remove_entry();
246        true
247    }
248
249    pub(crate) fn set_frame_tree(&mut self, frame_tree: &SendableFrameTree) {
250        let pipeline_id = frame_tree.pipeline.id;
251        let old_pipeline_id = self.root_pipeline_id.replace(pipeline_id);
252
253        if old_pipeline_id != self.root_pipeline_id {
254            debug!(
255                "Updating webview ({:?}) from pipeline {:?} to {:?}",
256                3, old_pipeline_id, self.root_pipeline_id
257            );
258        }
259
260        self.set_frame_tree_on_pipeline_details(frame_tree, None);
261    }
262
263    pub(crate) fn send_scroll_positions_to_layout_for_pipeline(
264        &self,
265        pipeline_id: PipelineId,
266        scrolled_node: ExternalScrollId,
267    ) {
268        let Some(details) = self.pipelines.get(&pipeline_id) else {
269            return;
270        };
271
272        let offsets = details.scroll_tree.scroll_offsets();
273
274        // This might be true if we have not received a display list from the layout
275        // associated with this pipeline yet. In that case, the layout is not ready to
276        // receive scroll offsets anyway, so just save time and prevent other issues by
277        // not sending them.
278        if offsets.is_empty() {
279            return;
280        }
281
282        let _ = self.embedder_to_constellation_sender.send(
283            EmbedderToConstellationMessage::SetScrollStates(
284                pipeline_id,
285                ScrollStateUpdate {
286                    scrolled_node,
287                    offsets,
288                },
289            ),
290        );
291    }
292
293    pub(crate) fn set_frame_tree_on_pipeline_details(
294        &mut self,
295        frame_tree: &SendableFrameTree,
296        parent_pipeline_id: Option<PipelineId>,
297    ) {
298        let pipeline_id = frame_tree.pipeline.id;
299        let pipeline_details = self.ensure_pipeline_details(pipeline_id);
300        pipeline_details.pipeline = Some(frame_tree.pipeline.clone());
301        pipeline_details.parent_pipeline_id = parent_pipeline_id;
302        pipeline_details.children = frame_tree
303            .children
304            .iter()
305            .map(|frame_tree| frame_tree.pipeline.id)
306            .collect();
307
308        for kid in &frame_tree.children {
309            self.set_frame_tree_on_pipeline_details(kid, Some(pipeline_id));
310        }
311    }
312
313    /// Sets or unsets the animations-running flag for the given pipeline. Returns
314    /// true if the pipeline has started animating.
315    pub(crate) fn change_pipeline_running_animations_state(
316        &mut self,
317        pipeline_id: PipelineId,
318        animation_state: AnimationState,
319    ) -> bool {
320        let pipeline_details = self.ensure_pipeline_details(pipeline_id);
321        let was_animating = pipeline_details.animating();
322        match animation_state {
323            AnimationState::AnimationsPresent => {
324                pipeline_details.animations_running = true;
325            },
326            AnimationState::AnimationCallbacksPresent => {
327                pipeline_details.animation_callbacks_running = true;
328            },
329            AnimationState::NoAnimationsPresent => {
330                pipeline_details.animations_running = false;
331            },
332            AnimationState::AnimationCallbacksAbsent => {
333                pipeline_details.animation_callbacks_running = false;
334            },
335        }
336        let started_animating = !was_animating && pipeline_details.animating();
337
338        self.update_animation_state();
339
340        // It's important that an animation tick is triggered even if the
341        // WebViewRenderer's overall animation state hasn't changed. It's possible that
342        // the WebView was animating, but not producing new display lists. In that case,
343        // no repaint will happen and thus no repaint will trigger the next animation tick.
344        started_animating
345    }
346
347    /// Sets or unsets the throttled flag for the given pipeline. Returns
348    /// true if the pipeline has started animating.
349    pub(crate) fn set_throttled(&mut self, pipeline_id: PipelineId, throttled: bool) -> bool {
350        let pipeline_details = self.ensure_pipeline_details(pipeline_id);
351        let was_animating = pipeline_details.animating();
352        pipeline_details.throttled = throttled;
353        let started_animating = !was_animating && pipeline_details.animating();
354
355        // Throttling a pipeline can cause it to be taken into the "not-animating" state.
356        self.update_animation_state();
357
358        // It's important that an animation tick is triggered even if the
359        // WebViewRenderer's overall animation state hasn't changed. It's possible that
360        // the WebView was animating, but not producing new display lists. In that case,
361        // no repaint will happen and thus no repaint will trigger the next animation tick.
362        started_animating
363    }
364
365    fn update_animation_state(&mut self) {
366        self.animating = self.pipelines.values().any(PipelineDetails::animating);
367        self.webview.set_animating(self.animating());
368    }
369
370    pub(crate) fn for_each_connected_pipeline(&self, callback: &mut impl FnMut(&PipelineDetails)) {
371        if let Some(root_pipeline_id) = self.root_pipeline_id {
372            self.for_each_connected_pipeline_internal(root_pipeline_id, callback);
373        }
374    }
375
376    fn for_each_connected_pipeline_internal(
377        &self,
378        pipeline_id: PipelineId,
379        callback: &mut impl FnMut(&PipelineDetails),
380    ) {
381        let Some(pipeline) = self.pipelines.get(&pipeline_id) else {
382            return;
383        };
384        callback(pipeline);
385        for child_pipeline_id in &pipeline.children {
386            self.for_each_connected_pipeline_internal(*child_pipeline_id, callback);
387        }
388    }
389
390    /// Update touch-based animations (currently just fling) during a `RefreshDriver`-based
391    /// frame tick. Returns `true` if we should continue observing frames (the fling is ongoing)
392    /// or `false` if we should stop observing frames (the fling has finished).
393    pub(crate) fn update_touch_handling_at_new_frame_start(&mut self) -> bool {
394        let Some(fling_action) = self.touch_handler.notify_new_frame_start() else {
395            return false;
396        };
397
398        self.on_scroll_window_event(
399            Scroll::Delta((-fling_action.delta).into()),
400            fling_action.cursor,
401            // Fling is a continuation of a touch pan, so it must respect
402            // `touch-action` like the originating touch gesture.
403            ScrollType::Touch,
404        );
405        true
406    }
407
408    fn dispatch_input_event_with_hit_testing(
409        &mut self,
410        render_api: &RenderApi,
411        event: InputEventAndId,
412    ) -> bool {
413        let event_point = event
414            .event
415            .point()
416            .map(|point| point.as_device_point(self.device_pixels_per_page_pixel()));
417        let is_touch_down = matches!(
418            &event.event,
419            InputEvent::Touch(TouchEvent {
420                event_type: TouchEventType::Down,
421                ..
422            })
423        );
424        let hit_test_result = match event_point {
425            Some(point) => {
426                let hit_test_result = match event.event {
427                    InputEvent::Touch(_) => self.touch_handler.get_hit_test_result_cache_value(),
428                    _ => None,
429                }
430                .or_else(|| self.hit_test(render_api, point).into_iter().nth(0));
431                if hit_test_result.is_none() {
432                    warn!("Empty hit test result for input event, ignoring.");
433                    return false;
434                }
435                hit_test_result
436            },
437            None => None,
438        };
439
440        // For touch-down, capture the hit node's `touch-action` and scrollable
441        // axes from the scroll tree so the pan axis-lock policy can be decided
442        // at pan-start.
443        if is_touch_down && let Some(hit) = &hit_test_result {
444            let policy_input = self
445                .pipelines
446                .get(&hit.pipeline_id)
447                .and_then(|pipeline_details| {
448                    pipeline_details
449                        .scroll_tree
450                        .touch_action_and_scrollable_axes_for(hit.external_scroll_id)
451                })
452                .map(
453                    |(touch_action, scrollable_x, scrollable_y)| PanPolicyInput {
454                        touch_action,
455                        scrollable_x,
456                        scrollable_y,
457                    },
458                );
459            if let Some(input) = policy_input {
460                self.touch_handler.set_pan_policy_input(input);
461            }
462        }
463
464        if let Err(error) = self.embedder_to_constellation_sender.send(
465            EmbedderToConstellationMessage::ForwardInputEvent(self.id, event, hit_test_result),
466        ) {
467            warn!("Sending event to constellation failed ({error:?}).");
468            false
469        } else {
470            true
471        }
472    }
473
474    pub(crate) fn notify_input_event(
475        &mut self,
476        render_api: &RenderApi,
477        repaint_reason: &Cell<RepaintReason>,
478        event_and_id: InputEventAndId,
479    ) -> bool {
480        if let InputEvent::Touch(touch_event) = event_and_id.event {
481            return self.on_touch_event(render_api, repaint_reason, touch_event, event_and_id.id);
482        }
483
484        if let InputEvent::Wheel(wheel_event) = event_and_id.event {
485            self.pending_wheel_events
486                .insert(event_and_id.id, wheel_event);
487        }
488
489        self.dispatch_input_event_with_hit_testing(render_api, event_and_id)
490    }
491
492    fn send_touch_event(
493        &mut self,
494        render_api: &RenderApi,
495        event: TouchEvent,
496        id: InputEventId,
497    ) -> bool {
498        let cancelable = event.is_cancelable();
499        let event_type = event.event_type;
500
501        let input_event_and_id = InputEventAndId {
502            event: InputEvent::Touch(event),
503            id,
504        };
505
506        let result = self.dispatch_input_event_with_hit_testing(render_api, input_event_and_id);
507
508        // We only post-process events that are actually cancelable. Uncancelable ones
509        // are processed immediately and can be ignored once they have been sent to the
510        // Constellation.
511        if cancelable && result {
512            self.touch_handler
513                .add_pending_touch_input_event(id, event.touch_id, event_type);
514        }
515
516        result
517    }
518
519    pub(crate) fn on_touch_event(
520        &mut self,
521        render_api: &RenderApi,
522        repaint_reason: &Cell<RepaintReason>,
523        event: TouchEvent,
524        id: InputEventId,
525    ) -> bool {
526        let result = match event.event_type {
527            TouchEventType::Down => self.on_touch_down(render_api, event, id),
528            TouchEventType::Move => self.on_touch_move(render_api, event, id),
529            TouchEventType::Up => self.on_touch_up(render_api, event, id),
530            TouchEventType::Cancel => self.on_touch_cancel(render_api, event, id),
531        };
532
533        self.touch_handler
534            .add_touch_move_refresh_observer_if_necessary(
535                self.refresh_driver.clone(),
536                repaint_reason,
537            );
538        result
539    }
540
541    fn on_touch_down(
542        &mut self,
543        render_api: &RenderApi,
544        event: TouchEvent,
545        id: InputEventId,
546    ) -> bool {
547        let point = event
548            .point
549            .as_device_point(self.device_pixels_per_page_pixel());
550        self.touch_handler.on_touch_down(event.touch_id, point);
551        self.send_touch_event(render_api, event, id)
552    }
553
554    fn on_touch_move(
555        &mut self,
556        render_api: &RenderApi,
557        mut event: TouchEvent,
558        id: InputEventId,
559    ) -> bool {
560        let point = event
561            .point
562            .as_device_point(self.device_pixels_per_page_pixel());
563        let action = self.touch_handler.on_touch_move(
564            event.touch_id,
565            point,
566            self.device_pixels_per_page_pixel_not_including_pinch_zoom()
567                .get(),
568        );
569        if let Some(action) = action {
570            // if first move processed and allowed, we directly process the move event,
571            // without waiting for the script handler.
572            if self
573                .touch_handler
574                .move_allowed(self.touch_handler.current_sequence_id)
575            {
576                // https://w3c.github.io/touch-events/#cancelability
577                event.disable_cancelable();
578                self.pending_scroll_zoom_events.push(action);
579            }
580        }
581        let mut reached_constellation = false;
582        // When the event is touchmove, if the script thread is processing the touch
583        // move event, we skip sending the event to the script thread.
584        // This prevents the script thread from stacking up for a large amount of time.
585        if !self.touch_handler.is_handling_touch_move_for_touch_id(
586            self.touch_handler.current_sequence_id,
587            event.touch_id,
588        ) {
589            reached_constellation = self.send_touch_event(render_api, event, id);
590            if reached_constellation && event.is_cancelable() {
591                self.touch_handler.set_handling_touch_move_for_touch_id(
592                    self.touch_handler.current_sequence_id,
593                    event.touch_id,
594                    TouchIdMoveTracking::Track,
595                );
596            }
597        }
598        reached_constellation
599    }
600
601    fn on_touch_up(&mut self, render_api: &RenderApi, event: TouchEvent, id: InputEventId) -> bool {
602        let point = event
603            .point
604            .as_device_point(self.device_pixels_per_page_pixel());
605        self.touch_handler.on_touch_up(event.touch_id, point);
606        self.send_touch_event(render_api, event, id)
607    }
608
609    fn on_touch_cancel(
610        &mut self,
611        render_api: &RenderApi,
612        event: TouchEvent,
613        id: InputEventId,
614    ) -> bool {
615        let point = event
616            .point
617            .as_device_point(self.device_pixels_per_page_pixel());
618        self.touch_handler.on_touch_cancel(event.touch_id, point);
619        self.send_touch_event(render_api, event, id)
620    }
621
622    fn on_touch_event_processed(
623        &mut self,
624        render_api: &RenderApi,
625        pending_touch_input_event: PendingTouchInputEvent,
626        result: InputEventResult,
627    ) {
628        let PendingTouchInputEvent {
629            sequence_id,
630            event_type,
631            touch_id,
632        } = pending_touch_input_event;
633
634        if result.contains(InputEventResult::DefaultPrevented) {
635            debug!(
636                "Touch event {:?} in sequence {:?} prevented!",
637                event_type, sequence_id
638            );
639            match event_type {
640                TouchEventType::Down => {
641                    // prevents both click and move
642                    self.touch_handler.prevent_click(sequence_id);
643                    self.touch_handler.prevent_move(sequence_id);
644                    self.touch_handler
645                        .remove_pending_touch_move_actions(sequence_id);
646                },
647                TouchEventType::Move => {
648                    // script thread processed the touch move event, mark this false.
649                    if let Some(info) = self.touch_handler.get_touch_sequence_mut(sequence_id) {
650                        info.prevent_move = TouchMoveAllowed::Prevented;
651                        if let TouchSequenceState::PendingFling { .. } = info.state {
652                            info.state = TouchSequenceState::Finished;
653                        }
654                        self.touch_handler.set_handling_touch_move_for_touch_id(
655                            self.touch_handler.current_sequence_id,
656                            touch_id,
657                            TouchIdMoveTracking::Remove,
658                        );
659                        self.touch_handler
660                            .remove_pending_touch_move_actions(sequence_id);
661                    }
662                },
663                TouchEventType::Up => {
664                    // Note: We don't have to consider PendingFling here, since we handle that
665                    // in the DefaultAllowed case of the touch_move event.
666                    // Note: Removing can and should fail, if we still have an active Fling,
667                    let Some(info) = &mut self.touch_handler.get_touch_sequence_mut(sequence_id)
668                    else {
669                        // The sequence ID could already be removed, e.g. if Fling finished,
670                        // before the touch_up event was handled (since fling can start
671                        // immediately if move was previously allowed, and clicks are anyway not
672                        // happening from fling).
673                        return;
674                    };
675                    match info.state {
676                        TouchSequenceState::PendingClick(_) => {
677                            info.state = TouchSequenceState::Finished;
678                            self.touch_handler.remove_touch_sequence(sequence_id);
679                        },
680                        TouchSequenceState::Flinging { .. } => {
681                            // We can't remove the touch sequence yet
682                        },
683                        TouchSequenceState::Finished => {
684                            self.touch_handler.remove_touch_sequence(sequence_id);
685                        },
686                        TouchSequenceState::Touching |
687                        TouchSequenceState::Panning { .. } |
688                        TouchSequenceState::Pinching |
689                        TouchSequenceState::MultiTouch |
690                        TouchSequenceState::PendingFling { .. } => {
691                            // It's possible to transition from Pinch to pan, Which means that
692                            // a touch_up event for a pinch might have arrived here, but we
693                            // already transitioned to pan or even PendingFling.
694                            // We don't need to do anything in these cases though.
695                        },
696                    }
697                },
698                TouchEventType::Cancel => {
699                    // We could still have pending event handlers, so we remove the pending
700                    // actions, and try to remove the touch sequence.
701                    self.touch_handler
702                        .remove_pending_touch_move_actions(sequence_id);
703                    self.touch_handler.try_remove_touch_sequence(sequence_id);
704                },
705            }
706        } else {
707            debug!(
708                "Touch event {:?} in sequence {:?} allowed",
709                event_type, sequence_id
710            );
711            match event_type {
712                TouchEventType::Down => {},
713                TouchEventType::Move => {
714                    self.pending_scroll_zoom_events.extend(
715                        self.touch_handler
716                            .take_pending_touch_move_actions(sequence_id),
717                    );
718                    self.touch_handler.set_handling_touch_move_for_touch_id(
719                        self.touch_handler.current_sequence_id,
720                        touch_id,
721                        TouchIdMoveTracking::Remove,
722                    );
723                    if let Some(info) = self.touch_handler.get_touch_sequence_mut(sequence_id) &&
724                        info.prevent_move == TouchMoveAllowed::Pending
725                    {
726                        info.prevent_move = TouchMoveAllowed::Allowed;
727                        if let TouchSequenceState::PendingFling { velocity, point } = info.state {
728                            info.state = TouchSequenceState::Flinging { velocity, point }
729                        }
730                    }
731                },
732                TouchEventType::Up => {
733                    let Some(info) = self.touch_handler.get_touch_sequence_mut(sequence_id) else {
734                        // The sequence was already removed because there is no default action.
735                        return;
736                    };
737                    match info.state {
738                        TouchSequenceState::PendingClick(point) => {
739                            info.state = TouchSequenceState::Finished;
740                            // PreventDefault from touch_down may have been processed after
741                            // touch_up already occurred.
742                            if !info.prevent_click {
743                                self.simulate_mouse_click(render_api, point);
744                            }
745                            self.touch_handler.remove_touch_sequence(sequence_id);
746                        },
747                        TouchSequenceState::Flinging { .. } => {
748                            // We can't remove the touch sequence yet
749                        },
750                        TouchSequenceState::Finished => {
751                            self.touch_handler.remove_touch_sequence(sequence_id);
752                        },
753                        TouchSequenceState::Panning { .. } |
754                        TouchSequenceState::Pinching |
755                        TouchSequenceState::PendingFling { .. } => {
756                            // It's possible to transition from Pinch to pan, Which means that
757                            // a touch_up event for a pinch might have arrived here, but we
758                            // already transitioned to pan or even PendingFling.
759                            // We don't need to do anything in these cases though.
760                        },
761                        TouchSequenceState::MultiTouch | TouchSequenceState::Touching => {
762                            // We transitioned to touching from multi-touch or pinching.
763                        },
764                    }
765                },
766                TouchEventType::Cancel => {
767                    self.touch_handler
768                        .remove_pending_touch_move_actions(sequence_id);
769                    self.touch_handler.try_remove_touch_sequence(sequence_id);
770                },
771            }
772        }
773    }
774
775    /// <http://w3c.github.io/touch-events/#mouse-events>
776    fn simulate_mouse_click(&mut self, render_api: &RenderApi, point: DevicePoint) {
777        let button = MouseButton::Primary;
778        self.dispatch_input_event_with_hit_testing(
779            render_api,
780            InputEvent::MouseMove(MouseMoveEvent::new_compatibility_for_touch(point.into())).into(),
781        );
782        self.dispatch_input_event_with_hit_testing(
783            render_api,
784            InputEvent::MouseButton(MouseButtonEvent::new(
785                MouseButtonAction::Down,
786                button,
787                point.into(),
788            ))
789            .into(),
790        );
791        self.dispatch_input_event_with_hit_testing(
792            render_api,
793            InputEvent::MouseButton(MouseButtonEvent::new(
794                MouseButtonAction::Up,
795                button,
796                point.into(),
797            ))
798            .into(),
799        );
800    }
801
802    pub(crate) fn notify_scroll_event(&mut self, scroll: Scroll, point: WebViewPoint) {
803        let point = point.as_device_point(self.device_pixels_per_page_pixel());
804        self.on_scroll_window_event(scroll, point, ScrollType::InputEvents);
805    }
806
807    fn on_scroll_window_event(
808        &mut self,
809        scroll: Scroll,
810        cursor: DevicePoint,
811        scroll_type: ScrollType,
812    ) {
813        self.pending_scroll_zoom_events
814            .push(ScrollZoomEvent::Scroll(ScrollEvent {
815                scroll,
816                point: cursor,
817                scroll_type,
818            }));
819    }
820
821    /// Process pending scroll events for this [`WebViewRenderer`]. Returns a tuple containing:
822    ///
823    ///  - A boolean that is true if a zoom occurred.
824    ///  - An optional [`ScrollResult`] if a scroll occurred.
825    ///
826    /// It is up to the caller to ensure that these events update the rendering appropriately.
827    pub(crate) fn process_pending_scroll_and_pinch_zoom_events(
828        &mut self,
829        render_api: &RenderApi,
830    ) -> (PinchZoomResult, Option<ScrollResult>) {
831        if self.pending_scroll_zoom_events.is_empty() {
832            return (PinchZoomResult::DidNotPinchZoom, None);
833        }
834
835        // Batch up all scroll events and changes to pinch zoom into a single change, or
836        // else we'll do way too much painting.
837        let mut combined_scroll_event: Option<ScrollEvent> = None;
838        let mut new_pinch_zoom = self.pinch_zoom;
839        let device_pixels_per_page_pixel = self.device_pixels_per_page_pixel();
840
841        for scroll_event in self.pending_scroll_zoom_events.drain(..) {
842            match scroll_event {
843                ScrollZoomEvent::PinchZoom(magnification, center) => {
844                    let new_factor = self
845                        .viewport_description
846                        .clamp_zoom(self.pinch_zoom.zoom_factor().0 * magnification);
847                    new_pinch_zoom.set_zoom(new_factor, center);
848                },
849                ScrollZoomEvent::Scroll(scroll_event_info) => {
850                    let combined_event = match combined_scroll_event.as_mut() {
851                        None => {
852                            combined_scroll_event = Some(scroll_event_info);
853                            continue;
854                        },
855                        Some(combined_event) => combined_event,
856                    };
857
858                    match (combined_event.scroll, scroll_event_info.scroll) {
859                        (Scroll::Delta(old_delta), Scroll::Delta(new_delta)) => {
860                            let old_delta =
861                                old_delta.as_device_vector(device_pixels_per_page_pixel);
862                            let new_delta =
863                                new_delta.as_device_vector(device_pixels_per_page_pixel);
864                            combined_event.scroll = Scroll::Delta((old_delta + new_delta).into());
865                        },
866                        (Scroll::Start, _) | (Scroll::End, _) => {
867                            // Once we see Start or End, we shouldn't process any more events.
868                            break;
869                        },
870                        (_, Scroll::Start) | (_, Scroll::End) => {
871                            // If this is an event which is scrolling to the start or end of the page,
872                            // disregard other pending events and exit the loop.
873                            *combined_event = scroll_event_info;
874                            break;
875                        },
876                    }
877                },
878            }
879        }
880
881        // When zoomed in via pinch zoom, first try to move the center of the zoom and use the rest
882        // of the delta for scrolling. This allows moving the zoomed into viewport around in the
883        // unzoomed viewport before actually scrolling the underlying layers.
884        if let Some(combined_scroll_event) = combined_scroll_event.as_mut() {
885            new_pinch_zoom.pan(
886                &mut combined_scroll_event.scroll,
887                self.device_pixels_per_page_pixel(),
888            )
889        }
890
891        let scroll_result = combined_scroll_event.and_then(|combined_event| {
892            self.scroll_node_at_device_point(
893                render_api,
894                combined_event.point.to_f32(),
895                combined_event.scroll,
896                combined_event.scroll_type,
897            )
898        });
899        if let Some(ref scroll_result) = scroll_result {
900            self.send_scroll_positions_to_layout_for_pipeline(
901                scroll_result.hit_test_result.pipeline_id,
902                scroll_result.external_scroll_id,
903            );
904        } else {
905            self.touch_handler.stop_fling_if_needed();
906        }
907
908        // Additionally notify pinch zoom update to the script.
909        let pinch_zoom_result = self.set_pinch_zoom(new_pinch_zoom);
910        if pinch_zoom_result == PinchZoomResult::DidPinchZoom {
911            self.send_pinch_zoom_infos_to_script();
912            // Pinch zoom changes the viewport transform without touching the pipeline, so no reflow
913            // will occur. Notify the embedding layer so it can refresh the accessibility root node,
914            // whose transform scales by the pinch zoom.
915            self.webview.notify_viewport_updated();
916        }
917
918        (pinch_zoom_result, scroll_result)
919    }
920
921    /// Perform a hit test at the given [`DevicePoint`] and apply the [`Scroll`]
922    /// scrolling to the applicable scroll node under that point. If a scroll was
923    /// performed, returns the hit test result contains [`PipelineId`] of the node
924    /// scrolled, the id, and the final scroll delta.
925    fn scroll_node_at_device_point(
926        &mut self,
927        render_api: &RenderApi,
928        cursor: DevicePoint,
929        scroll: Scroll,
930        scroll_type: ScrollType,
931    ) -> Option<ScrollResult> {
932        let scroll_location = match scroll {
933            Scroll::Delta(delta) => {
934                let device_pixels_per_page = self.device_pixels_per_page_pixel();
935                let calculate_delta =
936                    delta.as_device_vector(device_pixels_per_page) / device_pixels_per_page;
937                ScrollLocation::Delta(calculate_delta.cast_unit())
938            },
939            Scroll::Start => ScrollLocation::Start,
940            Scroll::End => ScrollLocation::End,
941        };
942
943        let hit_test_results: Vec<_> = self
944            .touch_handler
945            .get_hit_test_result_cache_value()
946            .map(|result| vec![result])
947            .unwrap_or_else(|| self.hit_test(render_api, cursor));
948
949        // Iterate through all hit test results, processing only the first node of each pipeline.
950        // This is needed to propagate the scroll events from a pipeline representing an iframe to
951        // its ancestor pipelines.
952        let mut previous_pipeline_id = None;
953        for hit_test_result in hit_test_results {
954            let pipeline_details = self.pipelines.get_mut(&hit_test_result.pipeline_id)?;
955            if previous_pipeline_id.replace(hit_test_result.pipeline_id) !=
956                Some(hit_test_result.pipeline_id)
957            {
958                let scroll_result = pipeline_details.scroll_tree.scroll_node_or_ancestor(
959                    hit_test_result.external_scroll_id,
960                    scroll_location,
961                    scroll_type,
962                );
963                if let Some((external_scroll_id, offset)) = scroll_result {
964                    // We would like to cache the hit test for the node that that actually scrolls
965                    // while panning, which we don't know until right now (as some nodes
966                    // might be at the end of their scroll area). In particular, directionality of
967                    // scroll matters. That's why this is done here and not as soon as the touch
968                    // starts.
969                    self.touch_handler.set_hit_test_result_cache_value(
970                        hit_test_result.clone(),
971                        self.device_pixels_per_page_pixel(),
972                    );
973                    return Some(ScrollResult {
974                        hit_test_result,
975                        external_scroll_id,
976                        offset,
977                    });
978                }
979            }
980        }
981        None
982    }
983
984    /// Scroll the viewport (root pipeline, root scroll node) of this WebView, but first
985    /// attempting to pan the pinch zoom viewport. This is called when processing
986    /// key-based scrolling from script.
987    pub(crate) fn scroll_viewport_by_delta(
988        &mut self,
989        delta: LayoutVector2D,
990    ) -> (PinchZoomResult, Vec<ScrollResult>) {
991        let device_pixels_per_page_pixel = self.device_pixels_per_page_pixel();
992        let delta_in_device_pixels = delta.cast_unit() * device_pixels_per_page_pixel;
993        let remaining = self.pinch_zoom.pan_with_device_scroll(
994            Scroll::Delta(delta_in_device_pixels.into()),
995            device_pixels_per_page_pixel,
996        );
997
998        let pinch_zoom_result = match remaining == delta_in_device_pixels {
999            true => PinchZoomResult::DidNotPinchZoom,
1000            false => PinchZoomResult::DidPinchZoom,
1001        };
1002        if remaining == Vector2D::zero() {
1003            return (pinch_zoom_result, vec![]);
1004        }
1005
1006        let Some(root_pipeline_id) = self.root_pipeline_id else {
1007            return (pinch_zoom_result, vec![]);
1008        };
1009        let Some(root_pipeline) = self.pipelines.get_mut(&root_pipeline_id) else {
1010            return (pinch_zoom_result, vec![]);
1011        };
1012
1013        let remaining = remaining / device_pixels_per_page_pixel;
1014        let Some((external_scroll_id, offset)) = root_pipeline.scroll_tree.scroll_node_or_ancestor(
1015            ExternalScrollId(0, root_pipeline_id.into()),
1016            ScrollLocation::Delta(remaining.cast_unit()),
1017            // These are initiated only by keyboard events currently.
1018            ScrollType::InputEvents,
1019        ) else {
1020            return (pinch_zoom_result, vec![]);
1021        };
1022
1023        let hit_test_result = PaintHitTestResult {
1024            pipeline_id: root_pipeline_id,
1025            // It's difficult to get a good value for this as it needs to be piped
1026            // all the way through script and back here.
1027            point_in_viewport: Default::default(),
1028            external_scroll_id,
1029        };
1030
1031        self.send_scroll_positions_to_layout_for_pipeline(root_pipeline_id, external_scroll_id);
1032
1033        if pinch_zoom_result == PinchZoomResult::DidPinchZoom {
1034            self.send_pinch_zoom_infos_to_script();
1035        }
1036
1037        let scroll_result = ScrollResult {
1038            hit_test_result,
1039            external_scroll_id,
1040            offset,
1041        };
1042        (pinch_zoom_result, vec![scroll_result])
1043    }
1044
1045    /// Send [`PinchZoom`] update to the script's root pipeline.
1046    fn send_pinch_zoom_infos_to_script(&self) {
1047        // Pinch-zoom is applicable only to the root pipeline.
1048        let Some(pipeline_id) = self.root_pipeline_id else {
1049            return;
1050        };
1051
1052        let pinch_zoom_infos = self.pinch_zoom.get_pinch_zoom_infos_for_script(
1053            self.device_pixels_per_page_pixel_not_including_pinch_zoom(),
1054        );
1055
1056        let _ = self.embedder_to_constellation_sender.send(
1057            EmbedderToConstellationMessage::UpdatePinchZoomInfos(pipeline_id, pinch_zoom_infos),
1058        );
1059    }
1060
1061    pub(crate) fn pinch_zoom(&self) -> PinchZoom {
1062        self.pinch_zoom
1063    }
1064
1065    fn set_pinch_zoom(&mut self, requested_pinch_zoom: PinchZoom) -> PinchZoomResult {
1066        if requested_pinch_zoom == self.pinch_zoom {
1067            return PinchZoomResult::DidNotPinchZoom;
1068        }
1069
1070        self.pinch_zoom = requested_pinch_zoom;
1071        PinchZoomResult::DidPinchZoom
1072    }
1073
1074    /// Set the page zoom for this renderer, returning `true` if the value actually changed.
1075    pub(crate) fn set_page_zoom(
1076        &mut self,
1077        new_page_zoom: Scale<f32, CSSPixel, DeviceIndependentPixel>,
1078    ) -> bool {
1079        let new_page_zoom = new_page_zoom.clamp(MIN_PAGE_ZOOM, MAX_PAGE_ZOOM);
1080        let old_zoom = std::mem::replace(&mut self.page_zoom, new_page_zoom);
1081        if old_zoom == self.page_zoom {
1082            return false;
1083        }
1084        self.send_window_size_message();
1085        true
1086    }
1087
1088    /// The scale to use when displaying this [`WebViewRenderer`] in WebRender
1089    /// including both viewport scale (page zoom and hidpi scale) as well as any
1090    /// pinch zoom applied. This is based on the latest display list received,
1091    /// as page zoom changes are applied asynchronously and the rendered view
1092    /// should reflect the latest display list.
1093    pub(crate) fn device_pixels_per_page_pixel(&self) -> Scale<f32, CSSPixel, DevicePixel> {
1094        let viewport_scale = self
1095            .root_pipeline_id
1096            .and_then(|pipeline_id| self.pipelines.get(&pipeline_id))
1097            .and_then(|pipeline| pipeline.viewport_scale)
1098            .unwrap_or_else(|| self.page_zoom * self.hidpi_scale_factor);
1099        viewport_scale * self.pinch_zoom.zoom_factor()
1100    }
1101
1102    /// The current viewport scale (hidpi scale and page zoom and not pinch
1103    /// zoom) based on the current setting of the WebView. Note that this may
1104    /// not be the rendered viewport zoom as that is based on the latest display
1105    /// list and zoom changes are applied asynchronously.
1106    pub(crate) fn device_pixels_per_page_pixel_not_including_pinch_zoom(
1107        &self,
1108    ) -> Scale<f32, CSSPixel, DevicePixel> {
1109        self.page_zoom * self.hidpi_scale_factor
1110    }
1111
1112    /// Adjust the pinch zoom of the [`WebView`] by the given zoom delta.
1113    pub(crate) fn adjust_pinch_zoom(&mut self, magnification: f32, center: DevicePoint) {
1114        if magnification == 1.0 {
1115            return;
1116        }
1117
1118        self.pending_scroll_zoom_events
1119            .push(ScrollZoomEvent::PinchZoom(magnification, center));
1120    }
1121
1122    fn send_window_size_message(&self) {
1123        // The device pixel ratio used by the style system should include the scale from page pixels
1124        // to device pixels, but not including any pinch zoom.
1125        let device_pixel_ratio = self.device_pixels_per_page_pixel_not_including_pinch_zoom();
1126        // From <https://www.w3.org/TR/css-viewport-1/#actual-viewport>:
1127        // This is the viewport you get after processing the viewport <meta> tag.
1128        let layout_viewport = self.rect.size().to_f32() /
1129            (device_pixel_ratio * Scale::new(self.viewport_description.initial_scale.get()));
1130        let _ = self.embedder_to_constellation_sender.send(
1131            EmbedderToConstellationMessage::ChangeViewportDetails(
1132                self.id,
1133                ViewportDetails {
1134                    hidpi_scale_factor: device_pixel_ratio,
1135                    size: layout_viewport,
1136                    device_size: self.screen_size,
1137                },
1138                WindowSizeType::Resize,
1139            ),
1140        );
1141    }
1142
1143    /// Set the `hidpi_scale_factor` for this renderer, returning `true` if the value actually changed.
1144    pub(crate) fn set_hidpi_scale_factor(
1145        &mut self,
1146        new_scale: Scale<f32, DeviceIndependentPixel, DevicePixel>,
1147    ) -> bool {
1148        let old_scale_factor = std::mem::replace(&mut self.hidpi_scale_factor, new_scale);
1149        if self.hidpi_scale_factor == old_scale_factor {
1150            return false;
1151        }
1152
1153        self.send_window_size_message();
1154        true
1155    }
1156
1157    /// Set the `screen_size` for this renderer, returning `true` if the value actually changed.
1158    pub(crate) fn set_screen_size(&mut self, new_size: Size2D<f32, DevicePixel>) -> bool {
1159        if self.screen_size == new_size {
1160            return false;
1161        }
1162        self.screen_size = new_size;
1163
1164        self.send_window_size_message();
1165        true
1166    }
1167
1168    /// Set the `rect` for this renderer, returning `true` if the value actually changed.
1169    pub(crate) fn set_rect(&mut self, new_rect: DeviceRect) -> bool {
1170        let old_rect = std::mem::replace(&mut self.rect, new_rect);
1171        if old_rect.size() != self.rect.size() {
1172            self.send_window_size_message();
1173            self.pinch_zoom.resize_unscaled_viewport(new_rect);
1174            self.send_pinch_zoom_infos_to_script();
1175        }
1176        old_rect != self.rect
1177    }
1178
1179    pub fn set_viewport_description(&mut self, viewport_description: ViewportDescription) {
1180        self.viewport_description = viewport_description;
1181        self.send_window_size_message();
1182        self.adjust_pinch_zoom(
1183            self.viewport_description.initial_scale.get(),
1184            DevicePoint::origin(),
1185        );
1186    }
1187
1188    pub(crate) fn scroll_trees_memory_usage(
1189        &self,
1190        ops: &mut malloc_size_of::MallocSizeOfOps,
1191    ) -> usize {
1192        self.pipelines
1193            .values()
1194            .map(|pipeline| pipeline.scroll_tree.size_of(ops))
1195            .sum::<usize>()
1196    }
1197
1198    pub(crate) fn notify_input_event_handled(
1199        &mut self,
1200        render_api: &RenderApi,
1201        repaint_reason: &Cell<RepaintReason>,
1202        id: InputEventId,
1203        result: InputEventResult,
1204    ) {
1205        if let Some(pending_touch_input_event) =
1206            self.touch_handler.take_pending_touch_input_event(id)
1207        {
1208            self.on_touch_event_processed(render_api, pending_touch_input_event, result);
1209            self.touch_handler
1210                .add_touch_move_refresh_observer_if_necessary(
1211                    self.refresh_driver.clone(),
1212                    repaint_reason,
1213                );
1214        }
1215
1216        if let Some(wheel_event) = self.pending_wheel_events.remove(&id) &&
1217            !result.contains(InputEventResult::DefaultPrevented)
1218        {
1219            // A scroll delta for a wheel event is the inverse of the wheel delta.
1220            let scroll_delta =
1221                DeviceVector2D::new(-wheel_event.delta.x as f32, -wheel_event.delta.y as f32);
1222            self.notify_scroll_event(Scroll::Delta(scroll_delta.into()), wheel_event.point);
1223        }
1224    }
1225}
1226
1227#[derive(Clone, Copy, Debug, PartialEq)]
1228pub struct UnknownWebView(pub WebViewId);