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