Fix pinch zoom and enable it for TouchpadMagnify events (#30459)

Pinch zoom was broken because pinch zoom events were triggered at -1, -1
in the compositor. This change:

1. Differentiates between magnify and scroll events in the compositor to
   remove the question of where to trigger them.
2. Converts winit TouchpadMagnify events into pinch zoom events so that
   this works properly on MacOS.
This commit is contained in:
Martin Robinson 2023-10-03 14:49:36 +02:00 committed by GitHub
parent f78d53daaa
commit dfd14aabef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 149 additions and 111 deletions

View file

@ -248,9 +248,7 @@ pub struct IOCompositor<Window: WindowMethods + ?Sized> {
} }
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
struct ScrollZoomEvent { struct ScrollEvent {
/// Change the pinch zoom level by this factor
magnification: f32,
/// Scroll by this offset, or to Start or End /// Scroll by this offset, or to Start or End
scroll_location: ScrollLocation, scroll_location: ScrollLocation,
/// Apply changes to the frame at this location /// Apply changes to the frame at this location
@ -259,6 +257,15 @@ struct ScrollZoomEvent {
event_count: u32, event_count: u32,
} }
#[derive(Clone, Copy)]
enum ScrollZoomEvent {
/// An pinch zoom event that magnifies the view by the given factor.
PinchZoom(f32),
/// A scroll event that scrolls the scroll node at the given location by the
/// given amount.
Scroll(ScrollEvent),
}
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
enum CompositionRequest { enum CompositionRequest {
NoCompositingNecessary, NoCompositingNecessary,
@ -375,7 +382,7 @@ impl<Window: WindowMethods + ?Sized> IOCompositor<Window> {
shutdown_state: ShutdownState::NotShuttingDown, shutdown_state: ShutdownState::NotShuttingDown,
page_zoom: Scale::new(1.0), page_zoom: Scale::new(1.0),
viewport_zoom: PinchZoomFactor::new(1.0), viewport_zoom: PinchZoomFactor::new(1.0),
min_viewport_zoom: None, min_viewport_zoom: Some(PinchZoomFactor::new(1.0)),
max_viewport_zoom: None, max_viewport_zoom: None,
zoom_action: false, zoom_action: false,
zoom_time: 0f64, zoom_time: 0f64,
@ -867,10 +874,11 @@ impl<Window: WindowMethods + ?Sized> IOCompositor<Window> {
} }
} }
/// Create a WebRender display list for the root pipeline in the given transaction. /// Set the root pipeline for our WebRender scene. If there is no pinch zoom applied,
/// This display list contains a full-screen iframe with the root content pipeline /// the root pipeline is the root content pipeline. If there is pinch zoom, the root
/// with any pinch zoom transform applied to it. /// content pipeline is wrapped in a display list that applies a pinch zoom
fn set_root_display_list(&self, transaction: &mut Transaction) { /// transformation to it.
fn set_root_content_pipeline_handling_pinch_zoom(&self, transaction: &mut Transaction) {
let root_content_pipeline = match self.root_content_pipeline.id { let root_content_pipeline = match self.root_content_pipeline.id {
Some(id) => id.to_webrender(), Some(id) => id.to_webrender(),
None => return, None => return,
@ -941,7 +949,7 @@ impl<Window: WindowMethods + ?Sized> IOCompositor<Window> {
}; };
let mut txn = Transaction::new(); let mut txn = Transaction::new();
self.set_root_display_list(&mut txn); self.set_root_content_pipeline_handling_pinch_zoom(&mut txn);
txn.generate_frame(0); txn.generate_frame(0);
self.webrender_api self.webrender_api
.send_transaction(self.webrender_document, txn); .send_transaction(self.webrender_document, txn);
@ -1219,14 +1227,20 @@ impl<Window: WindowMethods + ?Sized> IOCompositor<Window> {
), ),
TouchAction::Zoom(magnification, scroll_delta) => { TouchAction::Zoom(magnification, scroll_delta) => {
let cursor = Point2D::new(-1, -1); // Make sure this hits the base layer. let cursor = Point2D::new(-1, -1); // Make sure this hits the base layer.
self.pending_scroll_zoom_events.push(ScrollZoomEvent {
magnification: magnification, // The order of these events doesn't matter, because zoom is handled by
scroll_location: ScrollLocation::Delta(LayoutVector2D::from_untyped( // a root display list and the scroll event here is handled by the scroll
scroll_delta.to_untyped(), // applied to the content display list.
)), self.pending_scroll_zoom_events
cursor: cursor, .push(ScrollZoomEvent::PinchZoom(magnification));
event_count: 1, self.pending_scroll_zoom_events
}); .push(ScrollZoomEvent::Scroll(ScrollEvent {
scroll_location: ScrollLocation::Delta(LayoutVector2D::from_untyped(
scroll_delta.to_untyped(),
)),
cursor: cursor,
event_count: 1,
}));
}, },
TouchAction::DispatchEvent => { TouchAction::DispatchEvent => {
self.send_touch_event(TouchEventType::Move, identifier, point); self.send_touch_event(TouchEventType::Move, identifier, point);
@ -1280,107 +1294,126 @@ impl<Window: WindowMethods + ?Sized> IOCompositor<Window> {
} }
fn on_scroll_window_event(&mut self, scroll_location: ScrollLocation, cursor: DeviceIntPoint) { fn on_scroll_window_event(&mut self, scroll_location: ScrollLocation, cursor: DeviceIntPoint) {
self.pending_scroll_zoom_events.push(ScrollZoomEvent { self.pending_scroll_zoom_events
magnification: 1.0, .push(ScrollZoomEvent::Scroll(ScrollEvent {
scroll_location, scroll_location: scroll_location,
cursor: cursor, cursor,
event_count: 1, event_count: 1,
}); }));
} }
fn process_pending_scroll_events(&mut self) { fn process_pending_scroll_events(&mut self) {
// Batch up all scroll events into one, or else we'll do way too much painting. // Batch up all scroll events into one, or else we'll do way too much painting.
let mut last_combined_event: Option<ScrollZoomEvent> = None; let mut combined_scroll_event: Option<ScrollEvent> = None;
let mut combined_magnification = 1.0;
for scroll_event in self.pending_scroll_zoom_events.drain(..) { for scroll_event in self.pending_scroll_zoom_events.drain(..) {
let this_cursor = scroll_event.cursor; match scroll_event {
ScrollZoomEvent::PinchZoom(magnification) => {
let this_delta = match scroll_event.scroll_location { combined_magnification *= magnification
ScrollLocation::Delta(delta) => delta,
ScrollLocation::Start | ScrollLocation::End => {
// If this is an event which is scrolling to the start or end of the page,
// disregard other pending events and exit the loop.
last_combined_event = Some(scroll_event);
break;
}, },
}; ScrollZoomEvent::Scroll(scroll_event_info) => {
let combined_event = match combined_scroll_event.as_mut() {
None => {
combined_scroll_event = Some(scroll_event_info);
continue;
},
Some(combined_event) => combined_event,
};
match &mut last_combined_event { match (
last_combined_event @ &mut None => { combined_event.scroll_location,
*last_combined_event = Some(ScrollZoomEvent { scroll_event_info.scroll_location,
magnification: scroll_event.magnification, ) {
scroll_location: ScrollLocation::Delta(LayoutVector2D::from_untyped( (ScrollLocation::Delta(old_delta), ScrollLocation::Delta(new_delta)) => {
this_delta.to_untyped(), // Mac OS X sometimes delivers scroll events out of vsync during a
)), // fling. This causes events to get bunched up occasionally, causing
cursor: this_cursor, // nasty-looking "pops". To mitigate this, during a fling we average
event_count: 1, // deltas instead of summing them.
}) let old_event_count = Scale::new(combined_event.event_count as f32);
}, combined_event.event_count += 1;
&mut Some(ref mut last_combined_event) => { let new_event_count = Scale::new(combined_event.event_count as f32);
// Mac OS X sometimes delivers scroll events out of vsync during a combined_event.scroll_location = ScrollLocation::Delta(
// fling. This causes events to get bunched up occasionally, causing (old_delta * old_event_count + new_delta) / new_event_count,
// nasty-looking "pops". To mitigate this, during a fling we average );
// deltas instead of summing them. },
if let ScrollLocation::Delta(delta) = last_combined_event.scroll_location { (ScrollLocation::Start, _) | (ScrollLocation::End, _) => {
let old_event_count = Scale::new(last_combined_event.event_count as f32); // Once we see Start or End, we shouldn't process any more events.
last_combined_event.event_count += 1; break;
let new_event_count = Scale::new(last_combined_event.event_count as f32); },
last_combined_event.scroll_location = ScrollLocation::Delta( (_, ScrollLocation::Start) | (_, ScrollLocation::End) => {
(delta * old_event_count + this_delta) / new_event_count, // If this is an event which is scrolling to the start or end of the page,
); // disregard other pending events and exit the loop.
*combined_event = scroll_event_info;
break;
},
} }
last_combined_event.magnification *= scroll_event.magnification;
}, },
} }
} }
if let Some(combined_event) = last_combined_event { let zoom_changed =
let scroll_location = match combined_event.scroll_location { self.set_pinch_zoom_level(self.pinch_zoom_level() * combined_magnification);
ScrollLocation::Delta(delta) => { let scroll_result = combined_scroll_event.and_then(|combined_event| {
let scaled_delta =
(Vector2D::from_untyped(delta.to_untyped()) / self.scale).to_untyped();
let calculated_delta = LayoutVector2D::from_untyped(scaled_delta);
ScrollLocation::Delta(calculated_delta)
},
// Leave ScrollLocation unchanged if it is Start or End location.
sl @ ScrollLocation::Start | sl @ ScrollLocation::End => sl,
};
let cursor = (combined_event.cursor.to_f32() / self.scale).to_untyped(); let cursor = (combined_event.cursor.to_f32() / self.scale).to_untyped();
let cursor = WorldPoint::from_untyped(cursor); self.scroll_node_at_world_point(
let mut txn = Transaction::new(); WorldPoint::from_untyped(cursor),
combined_event.scroll_location,
)
});
if !zoom_changed && scroll_result.is_none() {
return;
}
let result = match self.hit_test_at_point(cursor) { let mut transaction = Transaction::new();
Some(result) => result, if zoom_changed {
None => return, self.set_root_content_pipeline_handling_pinch_zoom(&mut transaction);
}; }
if let Some(details) = self.pipeline_details.get_mut(&result.pipeline_id) { if let Some((pipeline_id, external_id, offset)) = scroll_result {
match details let scroll_origin = LayoutPoint::new(-offset.x, -offset.y);
.scroll_tree transaction.scroll_node_with_id(scroll_origin, external_id, ScrollClamping::NoClamping);
.scroll_node_or_ancestor(&result.scroll_tree_node, scroll_location) self.send_scroll_positions_to_layout_for_pipeline(&pipeline_id);
{
Some((external_id, offset)) => {
let scroll_origin = LayoutPoint::new(-offset.x, -offset.y);
txn.scroll_node_with_id(
scroll_origin,
external_id,
ScrollClamping::NoClamping,
);
},
None => {},
}
}
self.send_scroll_positions_to_layout_for_pipeline(&result.pipeline_id);
if combined_event.magnification != 1.0 {
let old_zoom = self.pinch_zoom_level();
self.set_pinch_zoom_level(old_zoom * combined_event.magnification);
self.set_root_display_list(&mut txn);
}
txn.generate_frame(0);
self.webrender_api
.send_transaction(self.webrender_document, txn);
self.waiting_for_results_of_scroll = true self.waiting_for_results_of_scroll = true
} }
transaction.generate_frame(0);
self.webrender_api
.send_transaction(self.webrender_document, transaction);
}
/// Perform a hit test at the given [`WorldPoint`] and apply the [`ScrollLocation`]
/// scrolling to the applicable scroll node under that point. If a scroll was
/// performed, returns the [`PipelineId`] of the node scrolled, the id, and the final
/// scroll delta.
fn scroll_node_at_world_point(
&mut self,
cursor: WorldPoint,
scroll_location: ScrollLocation,
) -> Option<(PipelineId, ExternalScrollId, LayoutVector2D)> {
let scroll_location = match scroll_location {
ScrollLocation::Delta(delta) => {
let scaled_delta =
(Vector2D::from_untyped(delta.to_untyped()) / self.scale).to_untyped();
let calculated_delta = LayoutVector2D::from_untyped(scaled_delta);
ScrollLocation::Delta(calculated_delta)
},
// Leave ScrollLocation unchanged if it is Start or End location.
ScrollLocation::Start | ScrollLocation::End => scroll_location,
};
let hit_test_result = match self.hit_test_at_point(cursor) {
Some(result) => result,
None => return None,
};
let pipeline_details = match self.pipeline_details.get_mut(&hit_test_result.pipeline_id) {
Some(details) => details,
None => return None,
};
pipeline_details
.scroll_tree
.scroll_node_or_ancestor(&hit_test_result.scroll_tree_node, scroll_location)
.map(|(external_id, offset)| (hit_test_result.pipeline_id, external_id, offset))
} }
/// If there are any animations running, dispatches appropriate messages to the constellation. /// If there are any animations running, dispatches appropriate messages to the constellation.
@ -1472,12 +1505,9 @@ impl<Window: WindowMethods + ?Sized> IOCompositor<Window> {
/// Simulate a pinch zoom /// Simulate a pinch zoom
pub fn on_pinch_zoom_window_event(&mut self, magnification: f32) { pub fn on_pinch_zoom_window_event(&mut self, magnification: f32) {
self.pending_scroll_zoom_events.push(ScrollZoomEvent { // TODO: Scroll to keep the center in view?
magnification: magnification, self.pending_scroll_zoom_events
scroll_location: ScrollLocation::Delta(Vector2D::zero()), // TODO: Scroll to keep the center in view? .push(ScrollZoomEvent::PinchZoom(magnification));
cursor: Point2D::new(-1, -1), // Make sure this hits the base layer.
event_count: 1,
});
} }
fn send_scroll_positions_to_layout_for_pipeline(&self, pipeline_id: &PipelineId) { fn send_scroll_positions_to_layout_for_pipeline(&self, pipeline_id: &PipelineId) {
@ -1950,14 +1980,16 @@ impl<Window: WindowMethods + ?Sized> IOCompositor<Window> {
self.viewport_zoom.get() self.viewport_zoom.get()
} }
fn set_pinch_zoom_level(&mut self, mut zoom: f32) { fn set_pinch_zoom_level(&mut self, mut zoom: f32) -> bool {
if let Some(min) = self.min_viewport_zoom { if let Some(min) = self.min_viewport_zoom {
zoom = f32::max(min.get(), zoom); zoom = f32::max(min.get(), zoom);
} }
if let Some(max) = self.max_viewport_zoom { if let Some(max) = self.max_viewport_zoom {
zoom = f32::min(max.get(), zoom); zoom = f32::min(max.get(), zoom);
} }
self.viewport_zoom = PinchZoomFactor::new(zoom);
let old_zoom = std::mem::replace(&mut self.viewport_zoom, PinchZoomFactor::new(zoom));
old_zoom != self.viewport_zoom
} }
pub fn toggle_webrender_debug(&mut self, option: WebRenderDebugOption) { pub fn toggle_webrender_debug(&mut self, option: WebRenderDebugOption) {

View file

@ -548,8 +548,8 @@ where
self.compositor.on_zoom_reset_window_event(); self.compositor.on_zoom_reset_window_event();
}, },
EmbedderEvent::PinchZoom(magnification) => { EmbedderEvent::PinchZoom(zoom) => {
self.compositor.on_pinch_zoom_window_event(magnification); self.compositor.on_pinch_zoom_window_event(zoom);
}, },
EmbedderEvent::Navigation(top_level_browsing_context_id, direction) => { EmbedderEvent::Navigation(top_level_browsing_context_id, direction) => {

View file

@ -462,6 +462,12 @@ impl WindowPortsMethods for Window {
.borrow_mut() .borrow_mut()
.push(EmbedderEvent::Touch(phase, id, point)); .push(EmbedderEvent::Touch(phase, id, point));
}, },
winit::event::WindowEvent::TouchpadMagnify { delta, .. } => {
let magnification = delta as f32 + 1.0;
self.event_queue
.borrow_mut()
.push(EmbedderEvent::PinchZoom(magnification));
},
winit::event::WindowEvent::CloseRequested => { winit::event::WindowEvent::CloseRequested => {
self.event_queue.borrow_mut().push(EmbedderEvent::Quit); self.event_queue.borrow_mut().push(EmbedderEvent::Quit);
}, },