libservo: Port desktop servoshell to use the new WebView API (#35183)

This removes all uses of `EmbedderEvent` in the desktop servoshell to
use the new `WebView` API -- filling it out when necessary.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
Co-authored-by: Delan Azabani <dazabani@igalia.com>
This commit is contained in:
Martin Robinson 2025-01-28 15:57:57 +01:00 committed by GitHub
parent a1cf0cbf86
commit 9dbc942bee
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 826 additions and 798 deletions

View file

@ -427,19 +427,19 @@ impl BackgroundHangMonitorWorker {
},
recv(self.control_port) -> event => {
match event {
Ok(BackgroundHangMonitorControlMsg::EnableSampler(rate, max_duration)) => {
println!("Enabling profiler.");
self.sampling_duration = Some(rate);
self.sampling_max_duration = Some(max_duration);
self.sampling_baseline = Instant::now();
Ok(BackgroundHangMonitorControlMsg::ToggleSampler(rate, max_duration)) => {
if self.sampling_duration.is_some() {
println!("Enabling profiler.");
self.finish_sampled_profile();
self.sampling_duration = None;
} else {
println!("Disabling profiler.");
self.sampling_duration = Some(rate);
self.sampling_max_duration = Some(max_duration);
self.sampling_baseline = Instant::now();
}
None
},
Ok(BackgroundHangMonitorControlMsg::DisableSampler) => {
println!("Disabling profiler.");
self.finish_sampled_profile();
self.sampling_duration = None;
return true;
},
Ok(BackgroundHangMonitorControlMsg::Exit(sender)) => {
for component in self.monitored_components.values_mut() {
component.exit_signal.signal_to_exit();

View file

@ -1318,7 +1318,7 @@ impl IOCompositor {
self.pipeline_details.remove(&pipeline_id);
}
pub fn on_resize_window_event(&mut self) -> bool {
pub fn on_rendering_context_resized(&mut self) -> bool {
if self.shutdown_state != ShutdownState::NotShuttingDown {
return false;
}

View file

@ -1443,24 +1443,15 @@ where
self.forward_event(destination_pipeline_id, event);
},
FromCompositorMsg::SetCursor(cursor) => self.handle_set_cursor_msg(cursor),
FromCompositorMsg::EnableProfiler(rate, max_duration) => {
FromCompositorMsg::ToggleProfiler(rate, max_duration) => {
for background_monitor_control_sender in &self.background_monitor_control_senders {
if let Err(e) = background_monitor_control_sender.send(
BackgroundHangMonitorControlMsg::EnableSampler(rate, max_duration),
BackgroundHangMonitorControlMsg::ToggleSampler(rate, max_duration),
) {
warn!("error communicating with sampling profiler: {}", e);
}
}
},
FromCompositorMsg::DisableProfiler => {
for background_monitor_control_sender in &self.background_monitor_control_senders {
if let Err(e) = background_monitor_control_sender
.send(BackgroundHangMonitorControlMsg::DisableSampler)
{
warn!("error communicating with sampling profiler: {}", e);
}
}
},
FromCompositorMsg::ExitFullScreen(top_level_browsing_context_id) => {
self.handle_exit_fullscreen_msg(top_level_browsing_context_id);
},

View file

@ -85,8 +85,7 @@ mod from_compositor {
Self::BlurWebView => target!("BlurWebView"),
Self::ForwardEvent(_, event) => event.log_target(),
Self::SetCursor(..) => target!("SetCursor"),
Self::EnableProfiler(..) => target!("EnableProfiler"),
Self::DisableProfiler => target!("DisableProfiler"),
Self::ToggleProfiler(..) => target!("EnableProfiler"),
Self::ExitFullScreen(_) => target!("ExitFullScreen"),
Self::MediaSessionAction(_) => target!("MediaSessionAction"),
Self::SetWebViewThrottled(_, _) => target!("SetWebViewThrottled"),

View file

@ -30,7 +30,7 @@ use std::thread;
use std::vec::Drain;
pub use base::id::TopLevelBrowsingContextId;
use base::id::{PipelineNamespace, PipelineNamespaceId};
use base::id::{PipelineId, PipelineNamespace, PipelineNamespaceId};
use bluetooth::BluetoothThreadFactory;
use bluetooth_traits::BluetoothRequest;
use canvas::canvas_paint_thread::CanvasPaintThread;
@ -189,7 +189,6 @@ pub struct Servo {
constellation_proxy: ConstellationProxy,
embedder_receiver: EmbedderReceiver,
messages_for_embedder: Vec<(Option<TopLevelBrowsingContextId>, EmbedderMsg)>,
profiler_enabled: bool,
/// For single-process Servo instances, this field controls the initialization
/// and deinitialization of the JS Engine. Multiprocess Servo instances have their
/// own instance that exists in the content process instead.
@ -530,7 +529,6 @@ impl Servo {
constellation_proxy: ConstellationProxy::new(constellation_chan),
embedder_receiver,
messages_for_embedder: Vec::new(),
profiler_enabled: false,
_js_engine_setup: js_engine_setup,
}
}
@ -652,7 +650,7 @@ impl Servo {
},
EmbedderEvent::WindowResize => {
return self.compositor.borrow_mut().on_resize_window_event();
return self.compositor.borrow_mut().on_rendering_context_resized();
},
EmbedderEvent::ThemeChange(theme) => {
let msg = ConstellationMsg::ThemeChange(theme);
@ -798,13 +796,10 @@ impl Servo {
},
EmbedderEvent::ToggleSamplingProfiler(rate, max_duration) => {
self.profiler_enabled = !self.profiler_enabled;
let msg = if self.profiler_enabled {
ConstellationMsg::EnableProfiler(rate, max_duration)
} else {
ConstellationMsg::DisableProfiler
};
if let Err(e) = self.constellation_proxy.try_send(msg) {
if let Err(e) = self
.constellation_proxy
.try_send(ConstellationMsg::ToggleProfiler(rate, max_duration))
{
warn!("Sending profiler toggle to constellation failed ({:?}).", e);
}
},
@ -1003,6 +998,20 @@ impl Servo {
log::set_max_level(filter);
}
pub fn start_shutting_down(&self) {
self.compositor.borrow_mut().maybe_start_shutting_down();
}
pub fn allow_navigation_response(&self, pipeline_id: PipelineId, allow: bool) {
let msg = ConstellationMsg::AllowNavigationResponse(pipeline_id, allow);
if let Err(e) = self.constellation_proxy.try_send(msg) {
warn!(
"Sending allow navigation to constellation failed ({:?}).",
e
);
}
}
pub fn deinit(self) {
self.compositor.borrow_mut().deinit();
}

View file

@ -3,15 +3,26 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::cell::RefCell;
use std::ffi::c_void;
use std::rc::Rc;
use std::time::Duration;
use base::id::WebViewId;
use compositing::windowing::{MouseWindowEvent, WebRenderDebugOption};
use compositing::IOCompositor;
use compositing_traits::ConstellationMsg;
use webrender_api::units::DeviceRect;
use embedder_traits::{
ClipboardEventType, GamepadEvent, MediaSessionActionType, Theme, TouchEventType, TouchId,
TraversalDirection, WheelDelta,
};
use keyboard_types::{CompositionEvent, KeyboardEvent};
use url::Url;
use webrender_api::units::{DeviceIntPoint, DeviceIntSize, DevicePoint, DeviceRect};
use webrender_api::ScrollLocation;
use crate::ConstellationProxy;
#[derive(Clone)]
pub struct WebView(Rc<WebViewInner>);
struct WebViewInner {
@ -38,7 +49,7 @@ impl WebView {
pub(crate) fn new(
constellation_proxy: &ConstellationProxy,
compositor: Rc<RefCell<IOCompositor>>,
url: url::Url,
url: Url,
) -> Self {
let webview_id = WebViewId::new();
constellation_proxy.send(ConstellationMsg::NewWebView(url.into(), webview_id));
@ -114,4 +125,188 @@ impl WebView {
.raise_webview_to_top(self.id(), hide_others)
.expect("BUG: invalid WebView instance");
}
pub fn notify_theme_change(&self, theme: Theme) {
self.0
.constellation_proxy
.send(ConstellationMsg::ThemeChange(theme))
}
pub fn load(&self, url: Url) {
self.0
.constellation_proxy
.send(ConstellationMsg::LoadUrl(self.id(), url.into()))
}
pub fn reload(&self) {
self.0
.constellation_proxy
.send(ConstellationMsg::Reload(self.id()))
}
pub fn go_back(&self, amount: usize) {
self.0
.constellation_proxy
.send(ConstellationMsg::TraverseHistory(
self.id(),
TraversalDirection::Back(amount),
))
}
pub fn go_forward(&self, amount: usize) {
self.0
.constellation_proxy
.send(ConstellationMsg::TraverseHistory(
self.id(),
TraversalDirection::Forward(amount),
))
}
pub fn notify_pointer_button_event(&self, event: MouseWindowEvent) {
self.0
.compositor
.borrow_mut()
.on_mouse_window_event_class(event);
}
pub fn notify_pointer_move_event(&self, event: DevicePoint) {
self.0
.compositor
.borrow_mut()
.on_mouse_window_move_event_class(event);
}
pub fn notify_touch_event(&self, event_type: TouchEventType, id: TouchId, point: DevicePoint) {
self.0
.compositor
.borrow_mut()
.on_touch_event(event_type, id, point);
}
pub fn notify_wheel_event(&self, delta: WheelDelta, point: DevicePoint) {
self.0.compositor.borrow_mut().on_wheel_event(delta, point);
}
pub fn notify_scroll_event(
&self,
location: ScrollLocation,
point: DeviceIntPoint,
touch_event_type: TouchEventType,
) {
self.0
.compositor
.borrow_mut()
.on_scroll_event(location, point, touch_event_type);
}
pub fn notify_keyboard_event(&self, event: KeyboardEvent) {
self.0
.constellation_proxy
.send(ConstellationMsg::Keyboard(event))
}
pub fn notify_ime_event(&self, event: CompositionEvent) {
self.0
.constellation_proxy
.send(ConstellationMsg::IMECompositionEvent(event))
}
pub fn notify_ime_dismissed_event(&self) {
self.0
.constellation_proxy
.send(ConstellationMsg::IMEDismissed);
}
pub fn notify_gamepad_event(&self, event: GamepadEvent) {
self.0
.constellation_proxy
.send(ConstellationMsg::Gamepad(event));
}
pub fn notify_media_session_action_event(&self, event: MediaSessionActionType) {
self.0
.constellation_proxy
.send(ConstellationMsg::MediaSessionAction(event));
}
pub fn notify_clipboard_event(&self, event: ClipboardEventType) {
self.0
.constellation_proxy
.send(ConstellationMsg::Clipboard(event));
}
pub fn notify_vsync(&self) {
self.0.compositor.borrow_mut().on_vsync();
}
pub fn notify_rendering_context_resized(&self) {
self.0
.compositor
.borrow_mut()
.on_rendering_context_resized();
}
pub fn set_zoom(&self, new_zoom: f32) {
self.0
.compositor
.borrow_mut()
.on_zoom_window_event(new_zoom);
}
pub fn reset_zoom(&self) {
self.0.compositor.borrow_mut().on_zoom_reset_window_event();
}
pub fn set_pinch_zoom(&self, new_pinch_zoom: f32) {
self.0
.compositor
.borrow_mut()
.on_pinch_zoom_window_event(new_pinch_zoom);
}
pub fn exit_fullscreen(&self) {
self.0
.constellation_proxy
.send(ConstellationMsg::ExitFullScreen(self.id()));
}
pub fn set_throttled(&self, throttled: bool) {
self.0
.constellation_proxy
.send(ConstellationMsg::SetWebViewThrottled(self.id(), throttled));
}
pub fn toggle_webrender_debugging(&self, debugging: WebRenderDebugOption) {
self.0
.compositor
.borrow_mut()
.toggle_webrender_debug(debugging);
}
pub fn capture_webrender(&self) {
self.0.compositor.borrow_mut().capture_webrender();
}
pub fn invalidate_native_surface(&self) {
self.0.compositor.borrow_mut().invalidate_native_surface();
}
pub fn replace_native_surface(&self, native_widget: *mut c_void, size: DeviceIntSize) {
self.0
.compositor
.borrow_mut()
.replace_native_surface(native_widget, size);
}
pub fn toggle_sampling_profiler(&self, rate: Duration, max_duration: Duration) {
self.0
.constellation_proxy
.send(ConstellationMsg::ToggleProfiler(rate, max_duration));
}
pub fn send_error(&self, message: String) {
self.0
.constellation_proxy
.send(ConstellationMsg::SendError(Some(self.id()), message));
}
}

View file

@ -205,9 +205,8 @@ pub trait BackgroundHangMonitorExitSignal: Send {
/// Messages to control the sampling profiler.
#[derive(Deserialize, Serialize)]
pub enum BackgroundHangMonitorControlMsg {
/// Enable the sampler, with a given sampling rate and max total sampling duration.
EnableSampler(Duration, Duration),
DisableSampler,
/// Toggle the sampler, with a given sampling rate and max total sampling duration.
ToggleSampler(Duration, Duration),
/// Exit, and propagate the signal to monitored components.
Exit(IpcSender<()>),
}

View file

@ -75,9 +75,7 @@ pub enum ConstellationMsg {
/// Requesting a change to the onscreen cursor.
SetCursor(Cursor),
/// Enable the sampling profiler, with a given sampling rate and max total sampling duration.
EnableProfiler(Duration, Duration),
/// Disable the sampling profiler.
DisableProfiler,
ToggleProfiler(Duration, Duration),
/// Request to exit from fullscreen mode
ExitFullScreen(TopLevelBrowsingContextId),
/// Media session action.
@ -129,8 +127,7 @@ impl ConstellationMsg {
SendError(..) => "SendError",
ForwardEvent(..) => "ForwardEvent",
SetCursor(..) => "SetCursor",
EnableProfiler(..) => "EnableProfiler",
DisableProfiler => "DisableProfiler",
ToggleProfiler(..) => "EnableProfiler",
ExitFullScreen(..) => "ExitFullScreen",
MediaSessionAction(..) => "MediaSessionAction",
SetWebViewThrottled(..) => "SetWebViewThrottled",

View file

@ -10,10 +10,10 @@ use std::rc::Rc;
use std::time::Instant;
use std::{env, fs};
use log::{info, trace};
use arboard::Clipboard;
use log::{info, trace, warn};
use raw_window_handle::HasDisplayHandle;
use servo::base::id::WebViewId;
use servo::compositing::windowing::{AnimationState, EmbedderEvent, WindowMethods};
use servo::compositing::windowing::{AnimationState, WindowMethods};
use servo::compositing::CompositeTarget;
use servo::config::opts::Opts;
use servo::config::prefs::Preferences;
@ -22,6 +22,7 @@ use servo::servo_url::ServoUrl;
use servo::webrender_traits::SurfmanRenderingContext;
use servo::{EventLoopWaker, Servo};
use surfman::Connection;
use url::Url;
use webxr::glwindow::GlWindowDiscovery;
#[cfg(target_os = "windows")]
use webxr::openxr::{AppInfo, OpenXrDiscovery};
@ -31,22 +32,22 @@ use winit::event_loop::{ActiveEventLoop, ControlFlow};
use winit::window::WindowId;
use super::events_loop::{EventsLoop, WakerEvent};
use super::minibrowser::Minibrowser;
use super::minibrowser::{Minibrowser, MinibrowserEvent};
use super::webview::WebViewManager;
use super::{headed_window, headless_window};
use crate::desktop::embedder::{EmbedderCallbacks, XrDiscovery};
use crate::desktop::tracing::trace_winit_event;
use crate::desktop::window_trait::WindowPortsMethods;
use crate::parser::get_default_url;
use crate::parser::{get_default_url, location_bar_input_to_url};
use crate::prefs::ServoShellPreferences;
pub struct App {
opts: Opts,
preferences: Preferences,
servo_shell_preferences: ServoShellPreferences,
clipboard: Option<Clipboard>,
servo: Option<Servo>,
webviews: Option<WebViewManager<dyn WindowPortsMethods>>,
event_queue: Vec<EmbedderEvent>,
webviews: Option<WebViewManager>,
suspended: Cell<bool>,
windows: HashMap<WindowId, Rc<dyn WindowPortsMethods>>,
minibrowser: Option<Minibrowser>,
@ -91,7 +92,7 @@ impl App {
opts,
preferences,
servo_shell_preferences,
event_queue: vec![],
clipboard: Clipboard::new().ok(),
webviews: None,
servo: None,
suspended: Cell::new(false),
@ -164,7 +165,7 @@ impl App {
minibrowser.update(
window.winit_window().unwrap(),
self.webviews.as_mut().unwrap(),
None,
self.servo.as_mut(),
"init",
);
window.set_toolbar_height(minibrowser.toolbar_height);
@ -173,7 +174,6 @@ impl App {
self.windows.insert(window.id(), window);
self.suspended.set(false);
self.event_queue.push(EmbedderEvent::Idle);
let (_, window) = self.windows.iter().next().unwrap();
let xr_discovery = if pref!(dom_webxr_openxr_enabled) && !self.opts.headless {
@ -213,7 +213,7 @@ impl App {
} else {
CompositeTarget::Window
};
let mut servo = Servo::new(
let servo = Servo::new(
self.opts.clone(),
self.preferences.clone(),
Rc::new(rendering_context),
@ -223,12 +223,11 @@ impl App {
composite_target,
);
servo.handle_events(vec![EmbedderEvent::NewWebView(
self.initial_url.to_owned(),
WebViewId::new(),
)]);
servo.setup_logging();
let webview = servo.new_webview(self.initial_url.clone().into_url());
self.webviews.as_mut().unwrap().add(webview);
self.servo = Some(servo);
}
@ -236,61 +235,47 @@ impl App {
self.windows.iter().any(|(_, window)| window.is_animating())
}
fn get_events(&mut self) -> Vec<EmbedderEvent> {
std::mem::take(&mut self.event_queue)
}
/// Pumps events and messages between the embedder and Servo, where embedder events flow
/// towards Servo and embedder messages flow away from Servo, and also runs the compositor.
/// Spins the Servo event loop, and (for now) handles a few other tasks:
/// - Notifying Servo about incoming gamepad events
/// - Receiving updates from Servo
/// - Performing updates in the compositor, such as queued pinch zoom events
///
/// As the embedder, we push embedder events through our event queues, from the App queue and
/// Window queues to the WebViewManager queue, and from the WebViewManager queue to Servo. We
/// receive and collect embedder messages from the various Servo components, then take them out
/// of the Servo interface so that the WebViewManager can handle them.
/// In the future, these tasks may be decoupled.
fn handle_events(&mut self) -> PumpResult {
let mut embedder_events = self.get_events();
let webviews = self.webviews.as_mut().unwrap();
// Take any outstanding embedder events from the App and its Windows.
for window in self.windows.values() {
embedder_events.extend(window.get_events());
}
// Catch some keyboard events, and push the rest onto the WebViewManager event queue.
webviews.handle_window_events(embedder_events);
// If the Gamepad API is enabled, handle gamepad events from GilRs.
// Checking for focused_webview_id should ensure we'll have a valid browsing context.
if pref!(dom_gamepad_enabled) && webviews.focused_webview_id().is_some() {
webviews.handle_gamepad_events();
}
// Take any new embedder messages from Servo itself.
let mut embedder_messages = self.servo.as_mut().unwrap().get_events();
// Take any new embedder messages from Servo.
let servo = self.servo.as_mut().expect("Servo should be running.");
let mut embedder_messages: Vec<_> = servo.get_events().collect();
let mut need_resize = false;
let mut need_present = false;
let mut need_update = false;
loop {
// Consume and handle those embedder messages.
let servo_event_response = webviews.handle_servo_events(&self.opts, embedder_messages);
let servo_event_response = webviews.handle_servo_events(
servo,
&mut self.clipboard,
&self.opts,
embedder_messages,
);
need_present |= servo_event_response.need_present;
need_update |= servo_event_response.need_update;
// Route embedder events from the WebViewManager to the relevant Servo components,
// receives and collects embedder messages from various Servo components,
// and runs the compositor.
need_resize |= self
.servo
.as_mut()
.unwrap()
.handle_events(webviews.get_events());
// Runs the compositor, and receives and collects embedder messages from various Servo components.
need_resize |= servo.handle_events(vec![]);
if webviews.shutdown_requested() {
return PumpResult::Shutdown;
}
// Take any new embedder messages from Servo itself.
embedder_messages = self.servo.as_mut().unwrap().get_events();
if embedder_messages.len() == 0 {
embedder_messages = servo.get_events().collect();
if embedder_messages.is_empty() {
break;
}
}
@ -333,7 +318,7 @@ impl App {
minibrowser.update(
window.winit_window().unwrap(),
webviews,
self.servo.as_ref().unwrap().offscreen_framebuffer_id(),
self.servo.as_mut(),
"update_location_in_toolbar",
);
}
@ -351,7 +336,7 @@ impl App {
minibrowser.update(
window.winit_window().unwrap(),
self.webviews.as_mut().unwrap(),
self.servo.as_ref().unwrap().offscreen_framebuffer_id(),
self.servo.as_mut(),
"PumpResult::Present::Immediate",
);
minibrowser.paint(window.winit_window().unwrap());
@ -391,7 +376,6 @@ impl App {
if self.servo.is_none() {
return false;
}
self.event_queue.push(EmbedderEvent::Idle);
let mut exit = false;
match self.handle_events() {
@ -421,6 +405,62 @@ impl App {
}
exit
}
/// Takes any events generated during `egui` updates and performs their actions.
fn handle_servoshell_ui_events(&mut self) {
let Some(webviews) = self.webviews.as_mut() else {
return;
};
let Some(minibrowser) = self.minibrowser.as_ref() else {
return;
};
let Some(servo) = self.servo.as_ref() else {
return;
};
for event in minibrowser.take_events() {
match event {
MinibrowserEvent::Go(location) => {
minibrowser.update_location_dirty(false);
let Some(url) = location_bar_input_to_url(
&location.clone(),
&self.servo_shell_preferences.searchpage,
) else {
warn!("failed to parse location");
break;
};
if let Some(focused_webview) = webviews.focused_webview() {
focused_webview.servo_webview.load(url.into_url());
}
},
MinibrowserEvent::Back => {
if let Some(focused_webview) = webviews.focused_webview() {
focused_webview.servo_webview.go_back(1);
}
},
MinibrowserEvent::Forward => {
if let Some(focused_webview) = webviews.focused_webview() {
focused_webview.servo_webview.go_forward(1);
}
},
MinibrowserEvent::Reload => {
minibrowser.update_location_dirty(false);
if let Some(focused_webview) = webviews.focused_webview() {
focused_webview.servo_webview.reload();
}
},
MinibrowserEvent::NewWebView => {
minibrowser.update_location_dirty(false);
let webview = servo.new_webview(Url::parse("servo:newtab").unwrap());
webviews.add(webview);
},
MinibrowserEvent::CloseWebView(id) => {
minibrowser.update_location_dirty(false);
webviews.close_webview(servo, id);
},
}
}
}
}
impl ApplicationHandler<WakerEvent> for App {
@ -463,7 +503,7 @@ impl ApplicationHandler<WakerEvent> for App {
minibrowser.update(
window.winit_window().unwrap(),
self.webviews.as_mut().unwrap(),
self.servo.as_ref().unwrap().offscreen_framebuffer_id(),
self.servo.as_mut(),
"RedrawRequested",
);
minibrowser.paint(window.winit_window().unwrap());
@ -505,7 +545,7 @@ impl ApplicationHandler<WakerEvent> for App {
minibrowser.update(
window.winit_window().unwrap(),
self.webviews.as_mut().unwrap(),
self.servo.as_ref().unwrap().offscreen_framebuffer_id(),
self.servo.as_mut(),
"Sync WebView size with Window Resize event",
);
}
@ -522,11 +562,9 @@ impl ApplicationHandler<WakerEvent> for App {
}
}
if !consumed {
if event == winit::event::WindowEvent::RedrawRequested {
self.event_queue.push(EmbedderEvent::Idle);
if let (Some(servo), Some(webviews)) = (self.servo.as_ref(), self.webviews.as_mut()) {
window.handle_winit_event(servo, &mut self.clipboard, webviews, event);
}
window.queue_embedder_events_for_winit_event(event);
}
let animating = self.is_animating();
@ -538,16 +576,8 @@ impl ApplicationHandler<WakerEvent> for App {
event_loop.set_control_flow(ControlFlow::Poll);
}
// Consume and handle any events from the Minibrowser.
if let Some(ref minibrowser) = self.minibrowser {
let webviews = &mut self.webviews.as_mut().unwrap();
let app_event_queue = &mut self.event_queue;
minibrowser.queue_embedder_events_for_minibrowser_events(
webviews,
app_event_queue,
&self.servo_shell_preferences,
);
}
// Consume and handle any events from the servoshell UI.
self.handle_servoshell_ui_events();
self.handle_events_with_winit(event_loop, window);
}
@ -567,7 +597,6 @@ impl ApplicationHandler<WakerEvent> for App {
if self.servo.is_none() {
return;
}
self.event_queue.push(EmbedderEvent::Idle);
let Some(window) = self.windows.values().next() else {
return;
@ -584,15 +613,7 @@ impl ApplicationHandler<WakerEvent> for App {
}
// Consume and handle any events from the Minibrowser.
if let Some(ref minibrowser) = self.minibrowser {
let webviews = &mut self.webviews.as_mut().unwrap();
let app_event_queue = &mut self.event_queue;
minibrowser.queue_embedder_events_for_minibrowser_events(
webviews,
app_event_queue,
&self.servo_shell_preferences,
);
}
self.handle_servoshell_ui_events();
self.handle_events_with_winit(event_loop, window);
}

View file

@ -6,13 +6,17 @@
use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::env;
use std::rc::Rc;
use std::time::Duration;
use arboard::Clipboard;
use euclid::{Angle, Length, Point2D, Rotation3D, Scale, Size2D, UnknownUnit, Vector2D, Vector3D};
use log::{debug, info, trace};
use keyboard_types::{Modifiers, ShortcutMatcher};
use log::{debug, info};
use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
use servo::compositing::windowing::{
AnimationState, EmbedderCoordinates, EmbedderEvent, MouseWindowEvent, WindowMethods,
AnimationState, EmbedderCoordinates, MouseWindowEvent, WebRenderDebugOption, WindowMethods,
};
use servo::config::opts::Opts;
use servo::servo_config::pref;
@ -21,10 +25,11 @@ use servo::webrender_api::units::{DeviceIntPoint, DeviceIntRect, DeviceIntSize,
use servo::webrender_api::ScrollLocation;
use servo::webrender_traits::SurfmanRenderingContext;
use servo::{
Cursor, Key, KeyState, KeyboardEvent, MouseButton as ServoMouseButton, Theme, TouchEventType,
TouchId, WheelDelta, WheelMode,
ClipboardEventType, Cursor, Key, KeyState, KeyboardEvent, MouseButton as ServoMouseButton,
Servo, Theme, TouchEventType, TouchId, WebView, WheelDelta, WheelMode,
};
use surfman::{Context, Device, SurfaceType};
use url::Url;
use winit::dpi::{LogicalSize, PhysicalPosition, PhysicalSize};
use winit::event::{ElementState, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase};
use winit::event_loop::ActiveEventLoop;
@ -33,8 +38,10 @@ use winit::keyboard::{Key as LogicalKey, ModifiersState, NamedKey};
use winit::window::Icon;
use super::geometry::{winit_position_to_euclid_point, winit_size_to_euclid_size};
use super::keyutils::keyboard_event_from_winit;
use super::keyutils::{keyboard_event_from_winit, CMD_OR_ALT};
use super::webview::{WebView as ServoShellWebView, WebViewManager};
use super::window_trait::{WindowPortsMethods, LINE_HEIGHT};
use crate::desktop::keyutils::CMD_OR_CONTROL;
pub struct Window {
winit_window: winit::window::Window,
@ -44,7 +51,6 @@ pub struct Window {
mouse_down_button: Cell<Option<MouseButton>>,
mouse_down_point: Cell<Point2D<i32, DevicePixel>>,
monitor: winit::monitor::MonitorHandle,
event_queue: RefCell<Vec<EmbedderEvent>>,
mouse_pos: Cell<Point2D<i32, DevicePixel>>,
last_pressed: Cell<Option<(KeyboardEvent, Option<LogicalKey>)>>,
/// A map of winit's key codes to key values that are interpreted from
@ -130,7 +136,6 @@ impl Window {
debug!("Created window {:?}", winit_window.id());
Window {
winit_window,
event_queue: RefCell::new(vec![]),
mouse_down_button: Cell::new(None),
mouse_down_point: Cell::new(Point2D::zero()),
mouse_pos: Cell::new(Point2D::zero()),
@ -148,19 +153,19 @@ impl Window {
}
}
fn handle_received_character(&self, mut ch: char) {
info!("winit received character: {:?}", ch);
if ch.is_control() {
if ch as u8 >= 32 {
fn handle_received_character(&self, webview: &WebView, mut character: char) {
info!("winit received character: {:?}", character);
if character.is_control() {
if character as u8 >= 32 {
return;
}
// shift ASCII control characters to lowercase
ch = (ch as u8 + 96) as char;
character = (character as u8 + 96) as char;
}
let (mut event, key_code) = if let Some((event, key_code)) = self.last_pressed.replace(None)
{
(event, key_code)
} else if ch.is_ascii() {
} else if character.is_ascii() {
// Some keys like Backspace emit a control character in winit
// but they are already dealt with in handle_keyboard_input
// so just ignore the character.
@ -170,7 +175,7 @@ impl Window {
// no keyboard event is emitted. A dummy event is created in this case.
(KeyboardEvent::default(), None)
};
event.key = Key::Character(ch.to_string());
event.key = Key::Character(character.to_string());
if event.state == KeyState::Down {
// Ensure that when we receive a keyup event from winit, we are able
@ -187,50 +192,69 @@ impl Window {
for xr_window_pose in &*xr_poses {
xr_window_pose.handle_xr_translation(&event);
}
self.event_queue
.borrow_mut()
.push(EmbedderEvent::Keyboard(event));
webview.notify_keyboard_event(event);
}
fn handle_keyboard_input(&self, input: KeyEvent) {
if let Some(input_text) = &input.text {
for ch in input_text.chars() {
self.handle_received_character(ch);
fn handle_keyboard_input(
&self,
servo: &Servo,
clipboard: &mut Option<Clipboard>,
webviews: &mut WebViewManager,
winit_event: KeyEvent,
) {
// First, handle servoshell key bindings that are not overridable by, or visible to, the page.
let mut keyboard_event =
keyboard_event_from_winit(&winit_event, self.modifiers_state.get());
if self.handle_intercepted_key_bindings(servo, clipboard, webviews, &keyboard_event) {
return;
}
// Then we deliver character and keyboard events to the page in the focused webview.
let Some(webview) = webviews
.focused_webview()
.map(|webview| webview.servo_webview.clone())
else {
return;
};
if let Some(input_text) = &winit_event.text {
for character in input_text.chars() {
self.handle_received_character(&webview, character);
}
}
let mut event = keyboard_event_from_winit(&input, self.modifiers_state.get());
trace!("handling {:?}", event);
if event.state == KeyState::Down && event.key == Key::Unidentified {
if keyboard_event.state == KeyState::Down && keyboard_event.key == Key::Unidentified {
// If pressed and probably printable, we expect a ReceivedCharacter event.
// Wait for that to be received and don't queue any event right now.
self.last_pressed
.set(Some((event, Some(input.logical_key))));
.set(Some((keyboard_event, Some(winit_event.logical_key))));
return;
} else if event.state == KeyState::Up && event.key == Key::Unidentified {
} else if keyboard_event.state == KeyState::Up && keyboard_event.key == Key::Unidentified {
// If release and probably printable, this is following a ReceiverCharacter event.
if let Some(key) = self.keys_down.borrow_mut().remove(&input.logical_key) {
event.key = key;
if let Some(key) = self.keys_down.borrow_mut().remove(&winit_event.logical_key) {
keyboard_event.key = key;
}
}
if event.key != Key::Unidentified {
if keyboard_event.key != Key::Unidentified {
self.last_pressed.set(None);
let xr_poses = self.xr_window_poses.borrow();
for xr_window_pose in &*xr_poses {
xr_window_pose.handle_xr_rotation(&input, self.modifiers_state.get());
xr_window_pose.handle_xr_rotation(&winit_event, self.modifiers_state.get());
}
self.event_queue
.borrow_mut()
.push(EmbedderEvent::Keyboard(event));
webview.notify_keyboard_event(keyboard_event);
}
// servoshell also has key bindings that are visible to, and overridable by, the page.
// See the handler for EmbedderMsg::Keyboard in webview.rs for those.
}
/// Helper function to handle a click
fn handle_mouse(
&self,
button: MouseButton,
action: ElementState,
webview: &WebView,
button: winit::event::MouseButton,
action: winit::event::ElementState,
coords: Point2D<i32, DevicePixel>,
) {
let max_pixel_dist = 10.0 * self.hidpi_factor().get();
@ -256,9 +280,7 @@ impl Window {
((pixel_dist.x * pixel_dist.x + pixel_dist.y * pixel_dist.y) as f32)
.sqrt();
if pixel_dist < max_pixel_dist {
self.event_queue
.borrow_mut()
.push(EmbedderEvent::MouseWindowEventClass(mouse_up_event));
webview.notify_pointer_button_event(mouse_up_event);
MouseWindowEvent::Click(mouse_button, coords.to_f32())
} else {
mouse_up_event
@ -268,17 +290,138 @@ impl Window {
}
},
};
self.event_queue
.borrow_mut()
.push(EmbedderEvent::MouseWindowEventClass(event));
webview.notify_pointer_button_event(event);
}
/// Handle key events before sending them to Servo.
fn handle_intercepted_key_bindings(
&self,
servo: &Servo,
clipboard: &mut Option<Clipboard>,
webviews: &mut WebViewManager,
key_event: &KeyboardEvent,
) -> bool {
let Some(focused_webview) = webviews
.focused_webview()
.map(|webview| webview.servo_webview.clone())
else {
return false;
};
let mut handled = true;
ShortcutMatcher::from_event(key_event.clone())
.shortcut(CMD_OR_CONTROL, 'R', || focused_webview.reload())
.shortcut(CMD_OR_CONTROL, 'W', || {
webviews.close_webview(servo, focused_webview.id());
})
.shortcut(CMD_OR_CONTROL, 'P', || {
let rate = env::var("SAMPLING_RATE")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(10);
let duration = env::var("SAMPLING_DURATION")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(10);
focused_webview.toggle_sampling_profiler(
Duration::from_millis(rate),
Duration::from_secs(duration),
);
})
.shortcut(CMD_OR_CONTROL, 'X', || {
focused_webview.notify_clipboard_event(ClipboardEventType::Cut);
})
.shortcut(CMD_OR_CONTROL, 'C', || {
focused_webview.notify_clipboard_event(ClipboardEventType::Copy);
})
.shortcut(CMD_OR_CONTROL, 'V', || {
let text = clipboard
.as_mut()
.and_then(|clipboard| clipboard.get_text().ok())
.unwrap_or_default();
focused_webview.notify_clipboard_event(ClipboardEventType::Paste(text));
})
.shortcut(Modifiers::CONTROL, Key::F9, || {
focused_webview.capture_webrender();
})
.shortcut(Modifiers::CONTROL, Key::F10, || {
focused_webview.toggle_webrender_debugging(WebRenderDebugOption::RenderTargetDebug);
})
.shortcut(Modifiers::CONTROL, Key::F11, || {
focused_webview.toggle_webrender_debugging(WebRenderDebugOption::TextureCacheDebug);
})
.shortcut(Modifiers::CONTROL, Key::F12, || {
focused_webview.toggle_webrender_debugging(WebRenderDebugOption::Profiler);
})
.shortcut(CMD_OR_ALT, Key::ArrowRight, || {
focused_webview.go_forward(1);
})
.optional_shortcut(
cfg!(not(target_os = "windows")),
CMD_OR_CONTROL,
']',
|| {
focused_webview.go_forward(1);
},
)
.shortcut(CMD_OR_ALT, Key::ArrowLeft, || {
focused_webview.go_back(1);
})
.optional_shortcut(
cfg!(not(target_os = "windows")),
CMD_OR_CONTROL,
'[',
|| {
focused_webview.go_back(1);
},
)
.optional_shortcut(
self.get_fullscreen(),
Modifiers::empty(),
Key::Escape,
|| focused_webview.exit_fullscreen(),
)
// Select the first 8 tabs via shortcuts
.shortcut(CMD_OR_CONTROL, '1', || webviews.focus_webview_by_index(0))
.shortcut(CMD_OR_CONTROL, '2', || webviews.focus_webview_by_index(1))
.shortcut(CMD_OR_CONTROL, '3', || webviews.focus_webview_by_index(2))
.shortcut(CMD_OR_CONTROL, '4', || webviews.focus_webview_by_index(3))
.shortcut(CMD_OR_CONTROL, '5', || webviews.focus_webview_by_index(4))
.shortcut(CMD_OR_CONTROL, '6', || webviews.focus_webview_by_index(5))
.shortcut(CMD_OR_CONTROL, '7', || webviews.focus_webview_by_index(6))
.shortcut(CMD_OR_CONTROL, '8', || webviews.focus_webview_by_index(7))
// Cmd/Ctrl 9 is a bit different in that it focuses the last tab instead of the 9th
.shortcut(CMD_OR_CONTROL, '9', || {
let len = webviews.webviews().len();
if len > 0 {
webviews.focus_webview_by_index(len - 1)
}
})
.shortcut(Modifiers::CONTROL, Key::PageDown, || {
if let Some(index) = webviews.get_focused_webview_index() {
webviews.focus_webview_by_index((index + 1) % webviews.webviews().len())
}
})
.shortcut(Modifiers::CONTROL, Key::PageUp, || {
if let Some(index) = webviews.get_focused_webview_index() {
let new_index = if index == 0 {
webviews.webviews().len() - 1
} else {
index - 1
};
webviews.focus_webview_by_index(new_index)
}
})
.shortcut(CMD_OR_CONTROL, 'T', || {
webviews.add(servo.new_webview(Url::parse("servo:newtab").unwrap()));
})
.shortcut(CMD_OR_CONTROL, 'Q', || servo.start_shutting_down())
.otherwise(|| handled = false);
handled
}
}
impl WindowPortsMethods for Window {
fn get_events(&self) -> Vec<EmbedderEvent> {
std::mem::take(&mut *self.event_queue.borrow_mut())
}
fn device_hidpi_factor(&self) -> Scale<f32, DeviceIndependentPixel, DevicePixel> {
Scale::new(self.winit_window.scale_factor() as f32)
}
@ -299,7 +442,7 @@ impl WindowPortsMethods for Window {
self.winit_window.set_title(title);
}
fn request_inner_size(&self, size: DeviceIntSize) -> Option<DeviceIntSize> {
fn request_resize(&self, _: &ServoShellWebView, size: DeviceIntSize) -> Option<DeviceIntSize> {
let toolbar_height = self.toolbar_height() * self.hidpi_factor();
let toolbar_height = toolbar_height.get().ceil() as i32;
let total_size = PhysicalSize::new(size.width, size.height + toolbar_height);
@ -393,25 +536,36 @@ impl WindowPortsMethods for Window {
self.winit_window.id()
}
fn queue_embedder_events_for_winit_event(&self, event: winit::event::WindowEvent) {
fn handle_winit_event(
&self,
servo: &Servo,
clipboard: &mut Option<Clipboard>,
webviews: &mut WebViewManager,
event: winit::event::WindowEvent,
) {
let Some(webview) = webviews
.focused_webview()
.map(|webview| webview.servo_webview.clone())
else {
return;
};
match event {
winit::event::WindowEvent::KeyboardInput { event, .. } => {
self.handle_keyboard_input(event)
self.handle_keyboard_input(servo, clipboard, webviews, event)
},
winit::event::WindowEvent::ModifiersChanged(modifiers) => {
self.modifiers_state.set(modifiers.state())
},
winit::event::WindowEvent::MouseInput { state, button, .. } => {
if button == MouseButton::Left || button == MouseButton::Right {
self.handle_mouse(button, state, self.mouse_pos.get());
self.handle_mouse(&webview, button, state, self.mouse_pos.get());
}
},
winit::event::WindowEvent::CursorMoved { position, .. } => {
let position = winit_position_to_euclid_point(position);
self.mouse_pos.set(position.to_i32());
self.event_queue
.borrow_mut()
.push(EmbedderEvent::MouseWindowMoveEventClass(position.to_f32()));
webview.notify_pointer_move_event(position.to_f32());
},
winit::event::WindowEvent::MouseWheel { delta, phase, .. } => {
let (mut dx, mut dy, mode) = match delta {
@ -434,7 +588,6 @@ impl WindowPortsMethods for Window {
};
let pos = self.mouse_pos.get();
let position = Point2D::new(pos.x as f32, pos.y as f32);
let wheel_event = EmbedderEvent::Wheel(wheel_delta, position);
// Scroll events snap to the major axis of movement, with vertical
// preferred over horizontal.
@ -446,47 +599,35 @@ impl WindowPortsMethods for Window {
let scroll_location = ScrollLocation::Delta(Vector2D::new(dx as f32, dy as f32));
let phase = winit_phase_to_touch_event_type(phase);
let scroll_event =
EmbedderEvent::Scroll(scroll_location, self.mouse_pos.get(), phase);
// Send events
self.event_queue.borrow_mut().push(wheel_event);
self.event_queue.borrow_mut().push(scroll_event);
webview.notify_wheel_event(wheel_delta, position);
webview.notify_scroll_event(scroll_location, self.mouse_pos.get(), phase);
},
winit::event::WindowEvent::Touch(touch) => {
let phase = winit_phase_to_touch_event_type(touch.phase);
let touch_event_type = winit_phase_to_touch_event_type(touch.phase);
let id = TouchId(touch.id as i32);
let position = touch.location;
let point = Point2D::new(position.x as f32, position.y as f32);
self.event_queue
.borrow_mut()
.push(EmbedderEvent::Touch(phase, id, point));
webview.notify_touch_event(touch_event_type, id, point);
},
winit::event::WindowEvent::PinchGesture { delta, .. } => {
let magnification = delta as f32 + 1.0;
self.event_queue
.borrow_mut()
.push(EmbedderEvent::PinchZoom(magnification));
webview.set_pinch_zoom(delta as f32 + 1.0);
},
winit::event::WindowEvent::CloseRequested => {
self.event_queue.borrow_mut().push(EmbedderEvent::Quit);
servo.start_shutting_down();
},
winit::event::WindowEvent::Resized(new_size) => {
if self.inner_size.get() != new_size {
self.inner_size.set(new_size);
self.event_queue
.borrow_mut()
.push(EmbedderEvent::WindowResize);
webview.notify_rendering_context_resized();
}
},
winit::event::WindowEvent::ThemeChanged(theme) => {
let theme = match theme {
webview.notify_theme_change(match theme {
winit::window::Theme::Light => Theme::Light,
winit::window::Theme::Dark => Theme::Dark,
};
self.event_queue
.borrow_mut()
.push(EmbedderEvent::ThemeChange(theme));
});
},
_ => {},
}

View file

@ -6,16 +6,16 @@
use std::cell::Cell;
use std::rc::Rc;
use std::sync::RwLock;
use arboard::Clipboard;
use euclid::num::Zero;
use euclid::{Box2D, Length, Point2D, Scale, Size2D};
use servo::compositing::windowing::{
AnimationState, EmbedderCoordinates, EmbedderEvent, WindowMethods,
};
use servo::compositing::windowing::{AnimationState, EmbedderCoordinates, WindowMethods};
use servo::servo_geometry::DeviceIndependentPixel;
use servo::webrender_api::units::{DeviceIntSize, DevicePixel};
use servo::Servo;
use super::webview::{WebView, WebViewManager};
use crate::desktop::window_trait::WindowPortsMethods;
pub struct Window {
@ -25,7 +25,6 @@ pub struct Window {
inner_size: Cell<DeviceIntSize>,
screen_size: Size2D<i32, DeviceIndependentPixel>,
window_rect: Box2D<i32, DeviceIndependentPixel>,
event_queue: RwLock<Vec<EmbedderEvent>>,
}
impl Window {
@ -55,7 +54,6 @@ impl Window {
inner_size,
screen_size,
window_rect,
event_queue: RwLock::new(Vec::new()),
};
Rc::new(window)
@ -63,18 +61,11 @@ impl Window {
}
impl WindowPortsMethods for Window {
fn get_events(&self) -> Vec<EmbedderEvent> {
match self.event_queue.write() {
Ok(ref mut event_queue) => std::mem::take(event_queue),
Err(_) => vec![],
}
}
fn id(&self) -> winit::window::WindowId {
winit::window::WindowId::dummy()
}
fn request_inner_size(&self, size: DeviceIntSize) -> Option<DeviceIntSize> {
fn request_resize(&self, webview: &WebView, size: DeviceIntSize) -> Option<DeviceIntSize> {
// Surfman doesn't support zero-sized surfaces.
let new_size = DeviceIntSize::new(size.width.max(1), size.height.max(1));
if self.inner_size.get() == new_size {
@ -82,9 +73,12 @@ impl WindowPortsMethods for Window {
}
self.inner_size.set(new_size);
if let Ok(ref mut queue) = self.event_queue.write() {
queue.push(EmbedderEvent::WindowResize);
}
// Because we are managing the rendering surface ourselves, there will be no other
// notification (such as from the display manager) that it has changed size, so we
// must notify the compositor here.
webview.servo_webview.notify_rendering_context_resized();
Some(new_size)
}
@ -116,7 +110,13 @@ impl WindowPortsMethods for Window {
self.animation_state.get() == AnimationState::Animating
}
fn queue_embedder_events_for_winit_event(&self, _event: winit::event::WindowEvent) {
fn handle_winit_event(
&self,
_: &Servo,
_: &mut Option<Clipboard>,
_: &mut WebViewManager,
_: winit::event::WindowEvent,
) {
// Not expecting any winit events.
}

View file

@ -20,22 +20,18 @@ use gleam::gl;
use glow::NativeFramebuffer;
use log::{trace, warn};
use servo::base::id::WebViewId;
use servo::compositing::windowing::EmbedderEvent;
use servo::servo_geometry::DeviceIndependentPixel;
use servo::servo_url::ServoUrl;
use servo::webrender_api::units::DevicePixel;
use servo::webrender_traits::SurfmanRenderingContext;
use servo::{TopLevelBrowsingContextId, TraversalDirection};
use servo::Servo;
use winit::event::{ElementState, MouseButton, WindowEvent};
use winit::event_loop::ActiveEventLoop;
use winit::window::Window;
use super::egui_glue::EguiGlow;
use super::geometry::winit_position_to_euclid_point;
use super::webview::{LoadStatus, WebViewManager};
use super::window_trait::WindowPortsMethods;
use crate::parser::location_bar_input_to_url;
use crate::prefs::ServoShellPreferences;
use super::webview::{LoadStatus, WebView, WebViewManager};
pub struct Minibrowser {
pub context: EguiGlow,
@ -60,11 +56,12 @@ pub struct Minibrowser {
pub enum MinibrowserEvent {
/// Go button clicked.
Go,
Go(String),
Back,
Forward,
Reload,
NewWebView,
CloseWebView(WebViewId),
}
fn truncate_with_ellipsis(input: &str, max_length: usize) -> String {
@ -116,6 +113,10 @@ impl Minibrowser {
}
}
pub(crate) fn take_events(&self) -> Vec<MinibrowserEvent> {
self.event_queue.take()
}
/// Preprocess the given [winit::event::WindowEvent], returning unconsumed for mouse events in
/// the Servo browser rect. This is needed because the CentralPanel we create for our webview
/// would otherwise make egui report events in that area as consumed.
@ -169,15 +170,15 @@ impl Minibrowser {
.min_size(Vec2 { x: 20.0, y: 20.0 })
}
/// Draws a browser tab, checking for clicks and returns an appropriate [EmbedderEvent]
/// Draws a browser tab, checking for clicks and queues appropriate `MinibrowserEvent`s.
/// Using a custom widget here would've been nice, but it doesn't seem as though egui
/// supports that, so we arrange multiple Widgets in a way that they look connected.
fn browser_tab(
ui: &mut egui::Ui,
label: &str,
selected: bool,
webview_id: TopLevelBrowsingContextId,
) -> Option<EmbedderEvent> {
webview: &WebView,
event_queue: &mut Vec<MinibrowserEvent>,
) {
let old_item_spacing = ui.spacing().item_spacing;
let old_visuals = ui.visuals().clone();
let active_bg_color = old_visuals.widgets.active.weak_bg_fill;
@ -213,6 +214,7 @@ impl Minibrowser {
visuals.widgets.hovered.rounding = rounding;
visuals.widgets.inactive.rounding = rounding;
let selected = webview.focused;
let tab = ui.add(SelectableLabel::new(
selected,
truncate_with_ellipsis(label, 20),
@ -242,11 +244,9 @@ impl Minibrowser {
let close_button = ui.add(egui::Button::new("X").fill(fill_color));
*ui.visuals_mut() = old_visuals;
if close_button.clicked() || close_button.middle_clicked() || tab.middle_clicked() {
Some(EmbedderEvent::CloseWebView(webview_id))
event_queue.push(MinibrowserEvent::CloseWebView(webview.servo_webview.id()))
} else if !selected && tab.clicked() {
Some(EmbedderEvent::FocusWebView(webview_id))
} else {
None
webview.servo_webview.focus();
}
}
@ -256,8 +256,8 @@ impl Minibrowser {
pub fn update(
&mut self,
window: &Window,
webviews: &mut WebViewManager<dyn WindowPortsMethods>,
servo_framebuffer_id: Option<gl::GLuint>,
webviews: &mut WebViewManager,
servo: Option<&mut Servo>,
reason: &'static str,
) {
let now = Instant::now();
@ -277,6 +277,9 @@ impl Minibrowser {
..
} = self;
let widget_fbo = *widget_surface_fbo;
let servo_framebuffer_id = servo
.as_ref()
.and_then(|servo| servo.offscreen_framebuffer_id());
let _duration = context.run(window, |ctx| {
// TODO: While in fullscreen add some way to mitigate the increased phishing risk
// when not displaying the URL bar: https://github.com/servo/servo/issues/32443
@ -342,8 +345,9 @@ impl Minibrowser {
if location_field.lost_focus() &&
ui.input(|i| i.clone().key_pressed(Key::Enter))
{
event_queue.borrow_mut().push(MinibrowserEvent::Go);
location_dirty.set(false);
event_queue
.borrow_mut()
.push(MinibrowserEvent::Go(location.borrow().clone()));
}
},
);
@ -352,26 +356,19 @@ impl Minibrowser {
});
};
let mut embedder_events = vec![];
// A simple Tab header strip
TopBottomPanel::top("tabs").show(ctx, |ui| {
ui.allocate_ui_with_layout(
ui.available_size(),
egui::Layout::left_to_right(egui::Align::Center),
|ui| {
for (webview_id, webview) in webviews.webviews().into_iter() {
for (_, webview) in webviews.webviews().into_iter() {
let label = match (&webview.title, &webview.url) {
(Some(title), _) if !title.is_empty() => title,
(_, Some(url)) => &url.to_string(),
_ => "New Tab",
};
if let Some(event) =
Self::browser_tab(ui, label, webview.focused, webview_id)
{
location_dirty.set(false);
embedder_events.push(event);
}
Self::browser_tab(ui, label, webview, &mut event_queue.borrow_mut());
}
if ui.add(Minibrowser::toolbar_button("+")).clicked() {
event_queue.borrow_mut().push(MinibrowserEvent::NewWebView);
@ -408,8 +405,7 @@ impl Minibrowser {
) * scale;
if rect != webview.rect {
webview.rect = rect;
embedder_events
.push(EmbedderEvent::MoveResizeWebView(focused_webview_id, rect));
webview.servo_webview.move_resize(rect)
}
let min = ui.cursor().min;
let size = ui.available_size();
@ -471,10 +467,6 @@ impl Minibrowser {
});
});
if !embedder_events.is_empty() {
webviews.handle_window_events(embedder_events);
}
*last_update = now;
});
}
@ -491,58 +483,9 @@ impl Minibrowser {
self.context.paint(window);
}
/// Takes any outstanding events from the [Minibrowser], converting them to [EmbedderEvent] and
/// routing those to the App event queue.
pub fn queue_embedder_events_for_minibrowser_events(
&self,
browser: &WebViewManager<dyn WindowPortsMethods>,
app_event_queue: &mut Vec<EmbedderEvent>,
preferences: &ServoShellPreferences,
) {
for event in self.event_queue.borrow_mut().drain(..) {
let browser_id = browser.focused_webview_id().unwrap();
match event {
MinibrowserEvent::Go => {
let location = self.location.borrow();
let Some(url) =
location_bar_input_to_url(&location.clone(), &preferences.searchpage)
else {
warn!("failed to parse location");
break;
};
app_event_queue.push(EmbedderEvent::LoadUrl(browser_id, url));
},
MinibrowserEvent::Back => {
app_event_queue.push(EmbedderEvent::Navigation(
browser_id,
TraversalDirection::Back(1),
));
},
MinibrowserEvent::Forward => {
app_event_queue.push(EmbedderEvent::Navigation(
browser_id,
TraversalDirection::Forward(1),
));
},
MinibrowserEvent::Reload => {
let browser_id = browser.focused_webview_id().unwrap();
app_event_queue.push(EmbedderEvent::Reload(browser_id));
},
MinibrowserEvent::NewWebView => {
self.location_dirty.set(false);
let url = ServoUrl::parse("servo:newtab").unwrap();
app_event_queue.push(EmbedderEvent::NewWebView(url, WebViewId::new()));
},
}
}
}
/// Updates the location field from the given [WebViewManager], unless the user has started
/// editing it without clicking Go, returning true iff it has changed (needing an egui update).
pub fn update_location_in_toolbar(
&mut self,
browser: &mut WebViewManager<dyn WindowPortsMethods>,
) -> bool {
pub fn update_location_in_toolbar(&mut self, browser: &mut WebViewManager) -> bool {
// User edited without clicking Go?
if self.location_dirty.get() {
return false;
@ -557,21 +500,19 @@ impl Minibrowser {
}
}
pub fn update_location_dirty(&self, dirty: bool) {
self.location_dirty.set(dirty);
}
/// Updates the spinner from the given [WebViewManager], returning true iff it has changed
/// (needing an egui update).
pub fn update_spinner_in_toolbar(
&mut self,
browser: &mut WebViewManager<dyn WindowPortsMethods>,
) -> bool {
pub fn update_spinner_in_toolbar(&mut self, browser: &mut WebViewManager) -> bool {
let need_update = browser.load_status() != self.load_status;
self.load_status = browser.load_status();
need_update
}
pub fn update_status_text(
&mut self,
browser: &mut WebViewManager<dyn WindowPortsMethods>,
) -> bool {
pub fn update_status_text(&mut self, browser: &mut WebViewManager) -> bool {
let need_update = browser.status_text() != self.status_text;
self.status_text = browser.status_text();
need_update
@ -579,10 +520,7 @@ impl Minibrowser {
/// Updates all fields taken from the given [WebViewManager], such as the location field.
/// Returns true iff the egui needs an update.
pub fn update_webview_data(
&mut self,
browser: &mut WebViewManager<dyn WindowPortsMethods>,
) -> bool {
pub fn update_webview_data(&mut self, browser: &mut WebViewManager) -> bool {
// Note: We must use the "bitwise OR" (|) operator here instead of "logical OR" (||)
// because logical OR would short-circuit if any of the functions return true.
// We want to ensure that all functions are called. The "bitwise OR" operator

View file

@ -36,21 +36,7 @@ macro_rules! trace_embedder_msg {
};
}
/// Log an event to servo ([servo::compositing::windowing::EmbedderEvent]) at trace level.
/// - To disable tracing: RUST_LOG='servoshell>servo@=off'
/// - To enable tracing: RUST_LOG='servoshell>servo@'
/// - Recommended filters when tracing is enabled:
/// - servoshell>servo@Idle=off
/// - servoshell>servo@MouseWindowMoveEventClass=off
macro_rules! trace_embedder_event {
// This macro only exists to put the docs in the same file as the target prefix,
// so the macro definition is always the same.
($event:expr, $($rest:tt)+) => {
::log::trace!(target: $crate::desktop::tracing::LogTarget::log_target(&$event), $($rest)+)
};
}
pub(crate) use {trace_embedder_event, trace_embedder_msg, trace_winit_event};
pub(crate) use {trace_embedder_msg, trace_winit_event};
/// Get the log target for an event, as a static string.
pub(crate) trait LogTarget {
@ -185,61 +171,3 @@ mod from_servo {
}
}
}
mod to_servo {
use super::LogTarget;
macro_rules! target {
($($name:literal)+) => {
concat!("servoshell>servo@", $($name),+)
};
}
impl LogTarget for servo::compositing::windowing::EmbedderEvent {
fn log_target(&self) -> &'static str {
match self {
Self::Idle => target!("Idle"),
Self::Refresh => target!("Refresh"),
Self::WindowResize => target!("WindowResize"),
Self::ThemeChange(..) => target!("ThemeChange"),
Self::AllowNavigationResponse(..) => target!("AllowNavigationResponse"),
Self::LoadUrl(..) => target!("LoadUrl"),
Self::MouseWindowEventClass(..) => target!("MouseWindowEventClass"),
Self::MouseWindowMoveEventClass(..) => target!("MouseWindowMoveEventClass"),
Self::Touch(..) => target!("Touch"),
Self::Wheel(..) => target!("Wheel"),
Self::Scroll(..) => target!("Scroll"),
Self::Zoom(..) => target!("Zoom"),
Self::PinchZoom(..) => target!("PinchZoom"),
Self::ResetZoom => target!("ResetZoom"),
Self::Navigation(..) => target!("Navigation"),
Self::Quit => target!("Quit"),
Self::ExitFullScreen(..) => target!("ExitFullScreen"),
Self::Keyboard(..) => target!("Keyboard"),
Self::Reload(..) => target!("Reload"),
Self::NewWebView(..) => target!("NewWebView"),
Self::CloseWebView(..) => target!("CloseWebView"),
Self::SendError(..) => target!("SendError"),
Self::MoveResizeWebView(..) => target!("MoveResizeWebView"),
Self::ShowWebView(..) => target!("ShowWebView"),
Self::HideWebView(..) => target!("HideWebView"),
Self::RaiseWebViewToTop(..) => target!("RaiseWebViewToTop"),
Self::FocusWebView(..) => target!("FocusWebView"),
Self::BlurWebView => target!("BlurWebView"),
Self::ToggleWebRenderDebug(..) => target!("ToggleWebRenderDebug"),
Self::CaptureWebRender => target!("CaptureWebRender"),
Self::ClearCache => target!("ClearCache"),
Self::ToggleSamplingProfiler(..) => target!("ToggleSamplingProfiler"),
Self::MediaSessionAction(..) => target!("MediaSessionAction"),
Self::SetWebViewThrottled(..) => target!("SetWebViewThrottled"),
Self::IMEComposition(..) => target!("IMEComposition"),
Self::IMEDismissed => target!("IMEDismissed"),
Self::InvalidateNativeSurface => target!("InvalidateNativeSurface"),
Self::ReplaceNativeSurface(..) => target!("ReplaceNativeSurface"),
Self::Gamepad(..) => target!("Gamepad"),
Self::Vsync => target!("Vsync"),
Self::ClipboardAction(..) => target!("ClipboardAction"),
}
}
}
}

View file

@ -2,42 +2,37 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::fs::File;
use std::io::Write;
use std::rc::Rc;
use std::time::Duration;
use std::vec::Drain;
use std::{env, thread};
use arboard::Clipboard;
use euclid::{Point2D, Vector2D};
use euclid::Vector2D;
use gilrs::ff::{BaseEffect, BaseEffectType, Effect, EffectBuilder, Repeat, Replay, Ticks};
use gilrs::{EventType, Gilrs};
use keyboard_types::{Key, KeyboardEvent, Modifiers, ShortcutMatcher};
use log::{debug, error, info, trace, warn};
use servo::base::id::TopLevelBrowsingContextId as WebViewId;
use servo::compositing::windowing::{EmbedderEvent, WebRenderDebugOption};
use log::{debug, error, info, warn};
use servo::base::id::WebViewId;
use servo::config::opts::Opts;
use servo::ipc_channel::ipc::IpcSender;
use servo::servo_url::ServoUrl;
use servo::webrender_api::units::DeviceRect;
use servo::webrender_api::ScrollLocation;
use servo::{
ClipboardEventType, CompositorEventVariant, ContextMenuResult, DualRumbleEffectParams,
EmbedderMsg, FilterPattern, GamepadEvent, GamepadHapticEffectType, GamepadIndex,
GamepadInputBounds, GamepadSupportedHapticEffects, GamepadUpdateType, PermissionPrompt,
PermissionRequest, PromptCredentialsInput, PromptDefinition, PromptOrigin, PromptResult,
TouchEventType, TraversalDirection,
CompositorEventVariant, ContextMenuResult, DualRumbleEffectParams, EmbedderMsg, FilterPattern,
GamepadEvent, GamepadHapticEffectType, GamepadIndex, GamepadInputBounds,
GamepadSupportedHapticEffects, GamepadUpdateType, PermissionPrompt, PermissionRequest,
PromptCredentialsInput, PromptDefinition, PromptOrigin, PromptResult, Servo, TouchEventType,
};
use tinyfiledialogs::{self, MessageBoxIcon, OkCancel, YesNo};
use super::keyutils::{CMD_OR_ALT, CMD_OR_CONTROL};
use super::keyutils::CMD_OR_CONTROL;
use super::window_trait::{WindowPortsMethods, LINE_HEIGHT};
use crate::desktop::tracing::{trace_embedder_event, trace_embedder_msg};
use crate::desktop::tracing::trace_embedder_msg;
pub struct WebViewManager<Window: WindowPortsMethods + ?Sized> {
pub struct WebViewManager {
status_text: Option<String>,
/// List of top-level browsing contexts.
@ -52,25 +47,12 @@ pub struct WebViewManager<Window: WindowPortsMethods + ?Sized> {
/// Modified by EmbedderMsg::WebViewFocused and EmbedderMsg::WebViewBlurred.
focused_webview_id: Option<WebViewId>,
/// Pre-creation state for WebViews.
/// This is needed because in some situations the WebViewOpened event is sent
/// after ChangePageTitle and HistoryChanged
webview_preload_data: HashMap<WebViewId, WebViewPreloadData>,
window: Rc<Window>,
event_queue: Vec<EmbedderEvent>,
clipboard: Option<Clipboard>,
window: Rc<dyn WindowPortsMethods>,
gamepad: Option<Gilrs>,
haptic_effects: HashMap<usize, HapticEffect>,
shutdown_requested: bool,
}
#[derive(Clone, Default)]
struct WebViewPreloadData {
title: Option<String>,
url: Option<ServoUrl>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum LoadStatus {
HeadParsed,
@ -79,23 +61,24 @@ pub enum LoadStatus {
}
// The state of each Tab/WebView
#[derive(Debug)]
pub struct WebView {
pub rect: DeviceRect,
pub title: Option<String>,
pub url: Option<ServoUrl>,
pub focused: bool,
pub load_status: LoadStatus,
pub servo_webview: ::servo::WebView,
}
impl WebView {
fn new(rect: DeviceRect, preload_data: WebViewPreloadData) -> Self {
fn new(servo_webview: ::servo::WebView) -> Self {
Self {
rect,
title: preload_data.title,
url: preload_data.url,
rect: DeviceRect::zero(),
title: None,
url: None,
focused: false,
load_status: LoadStatus::LoadComplete,
servo_webview,
}
}
}
@ -110,25 +93,14 @@ pub struct HapticEffect {
pub sender: IpcSender<bool>,
}
impl<Window> WebViewManager<Window>
where
Window: WindowPortsMethods + ?Sized,
{
pub fn new(window: Rc<Window>) -> WebViewManager<Window> {
impl WebViewManager {
pub fn new(window: Rc<dyn WindowPortsMethods>) -> WebViewManager {
WebViewManager {
status_text: None,
webviews: HashMap::default(),
creation_order: vec![],
focused_webview_id: None,
webview_preload_data: HashMap::default(),
window,
clipboard: match Clipboard::new() {
Ok(c) => Some(c),
Err(e) => {
warn!("Error creating clipboard context ({})", e);
None
},
},
gamepad: match Gilrs::new() {
Ok(g) => Some(g),
Err(e) => {
@ -137,8 +109,6 @@ where
},
},
haptic_effects: HashMap::default(),
event_queue: Vec::new(),
shutdown_requested: false,
}
}
@ -147,18 +117,41 @@ where
self.webviews.get_mut(&webview_id)
}
// Returns the existing preload data for the given WebView, or a new one.
fn ensure_preload_data_mut(&mut self, webview_id: &WebViewId) -> &mut WebViewPreloadData {
if let Entry::Vacant(entry) = self.webview_preload_data.entry(*webview_id) {
entry.insert(WebViewPreloadData::default());
}
self.webview_preload_data.get_mut(webview_id).unwrap()
pub fn get(&self, webview_id: WebViewId) -> Option<&WebView> {
self.webviews.get(&webview_id)
}
pub(crate) fn add(&mut self, webview: ::servo::WebView) {
self.creation_order.push(webview.id());
self.webviews.insert(webview.id(), WebView::new(webview));
}
pub fn focused_webview_id(&self) -> Option<WebViewId> {
self.focused_webview_id
}
pub fn close_webview(&mut self, servo: &Servo, webview_id: WebViewId) {
// This can happen because we can trigger a close with a UI action and then get the
// close event from Servo later.
if !self.webviews.contains_key(&webview_id) {
return;
}
self.webviews.retain(|&id, _| id != webview_id);
self.creation_order.retain(|&id| id != webview_id);
self.focused_webview_id = None;
match self.last_created_webview() {
Some(last_created_webview) => last_created_webview.servo_webview.focus(),
None => servo.start_shutting_down(),
}
}
fn last_created_webview(&self) -> Option<&WebView> {
self.creation_order
.last()
.and_then(|id| self.webviews.get(id))
}
pub fn current_url_string(&self) -> Option<String> {
match self.focused_webview() {
Some(webview) => webview.url.as_ref().map(|url| url.to_string()),
@ -167,10 +160,8 @@ where
}
pub fn focused_webview(&self) -> Option<&WebView> {
match self.focused_webview_id {
Some(id) => self.webviews.get(&id),
None => None,
}
self.focused_webview_id
.and_then(|id| self.webviews.get(&id))
}
pub fn load_status(&self) -> LoadStatus {
@ -184,10 +175,6 @@ where
self.status_text.clone()
}
pub fn get_events(&mut self) -> Vec<EmbedderEvent> {
std::mem::take(&mut self.event_queue)
}
// Returns the webviews in the creation order.
pub fn webviews(&self) -> Vec<(WebViewId, &WebView)> {
let mut res = vec![];
@ -197,22 +184,15 @@ where
res
}
pub fn handle_window_events(&mut self, events: Vec<EmbedderEvent>) {
for event in events {
trace_embedder_event!(event, "{event:?}");
match event {
EmbedderEvent::Keyboard(key_event) => {
self.handle_key_from_window(key_event);
},
event => {
self.event_queue.push(event);
},
}
}
}
/// Handle updates to connected gamepads from GilRs
pub fn handle_gamepad_events(&mut self) {
let Some(webview) = self
.focused_webview()
.map(|webview| webview.servo_webview.clone())
else {
return;
};
if let Some(ref mut gilrs) = self.gamepad {
while let Some(event) = gilrs.next_event() {
let gamepad = gilrs.gamepad(event.id);
@ -303,7 +283,7 @@ where
}
if let Some(event) = gamepad_event {
self.event_queue.push(EmbedderEvent::Gamepad(event));
webview.notify_gamepad_event(event);
}
}
}
@ -411,242 +391,33 @@ where
self.shutdown_requested
}
fn focus_webview_by_index(&self, index: usize) -> Option<EmbedderEvent> {
Some(EmbedderEvent::FocusWebView(self.webviews().get(index)?.0))
pub(crate) fn focus_webview_by_index(&self, index: usize) {
if let Some((_, webview)) = self.webviews().get(index) {
webview.servo_webview.focus();
}
}
fn get_focused_webview_index(&self) -> Option<usize> {
pub(crate) fn get_focused_webview_index(&self) -> Option<usize> {
let focused_id = self.focused_webview_id?;
self.webviews()
.iter()
.position(|webview| webview.0 == focused_id)
}
/// Handle key events before sending them to Servo.
fn handle_key_from_window(&mut self, key_event: KeyboardEvent) {
let embedder_event = ShortcutMatcher::from_event(key_event.clone())
.shortcut(CMD_OR_CONTROL, 'R', || {
self.focused_webview_id.map(EmbedderEvent::Reload)
})
// Select the first 8 tabs via shortcuts
.shortcut(CMD_OR_CONTROL, '1', || self.focus_webview_by_index(0))
.shortcut(CMD_OR_CONTROL, '2', || self.focus_webview_by_index(1))
.shortcut(CMD_OR_CONTROL, '3', || self.focus_webview_by_index(2))
.shortcut(CMD_OR_CONTROL, '4', || self.focus_webview_by_index(3))
.shortcut(CMD_OR_CONTROL, '5', || self.focus_webview_by_index(4))
.shortcut(CMD_OR_CONTROL, '6', || self.focus_webview_by_index(5))
.shortcut(CMD_OR_CONTROL, '7', || self.focus_webview_by_index(6))
.shortcut(CMD_OR_CONTROL, '8', || self.focus_webview_by_index(7))
// Cmd/Ctrl 9 is a bit different in that it focuses the last tab instead of the 9th
.shortcut(CMD_OR_CONTROL, '9', || {
let len = self.webviews().len();
if len > 0 {
self.focus_webview_by_index(len - 1)
} else {
None
}
})
.shortcut(Modifiers::CONTROL, Key::PageDown, || {
let i = self.get_focused_webview_index()?;
self.focus_webview_by_index((i + 1) % self.webviews().len())
})
.shortcut(Modifiers::CONTROL, Key::PageUp, || {
let index = self.get_focused_webview_index()?;
let new_index = if index == 0 {
self.webviews().len() - 1
} else {
index - 1
};
self.focus_webview_by_index(new_index)
})
.shortcut(CMD_OR_CONTROL, 'W', || {
self.focused_webview_id.map(EmbedderEvent::CloseWebView)
})
.shortcut(CMD_OR_CONTROL, 'T', || {
let url = ServoUrl::parse("servo:newtab").unwrap();
Some(EmbedderEvent::NewWebView(url, WebViewId::new()))
})
.shortcut(CMD_OR_CONTROL, 'Q', || Some(EmbedderEvent::Quit))
.shortcut(CMD_OR_CONTROL, 'P', || {
let rate = env::var("SAMPLING_RATE")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(10);
let duration = env::var("SAMPLING_DURATION")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(10);
Some(EmbedderEvent::ToggleSamplingProfiler(
Duration::from_millis(rate),
Duration::from_secs(duration),
))
})
.shortcut(CMD_OR_CONTROL, 'X', || {
Some(EmbedderEvent::ClipboardAction(ClipboardEventType::Cut))
})
.shortcut(CMD_OR_CONTROL, 'C', || {
Some(EmbedderEvent::ClipboardAction(ClipboardEventType::Copy))
})
.shortcut(CMD_OR_CONTROL, 'V', || {
Some(EmbedderEvent::ClipboardAction(ClipboardEventType::Paste(
self.clipboard
.as_mut()
.and_then(|clipboard| clipboard.get_text().ok())
.unwrap_or_else(|| {
warn!("Error getting clipboard text. Returning empty string.");
String::new()
}),
)))
})
.shortcut(Modifiers::CONTROL, Key::F9, || {
Some(EmbedderEvent::CaptureWebRender)
})
.shortcut(Modifiers::CONTROL, Key::F10, || {
Some(EmbedderEvent::ToggleWebRenderDebug(
WebRenderDebugOption::RenderTargetDebug,
))
})
.shortcut(Modifiers::CONTROL, Key::F11, || {
Some(EmbedderEvent::ToggleWebRenderDebug(
WebRenderDebugOption::TextureCacheDebug,
))
})
.shortcut(Modifiers::CONTROL, Key::F12, || {
Some(EmbedderEvent::ToggleWebRenderDebug(
WebRenderDebugOption::Profiler,
))
})
.shortcut(CMD_OR_ALT, Key::ArrowRight, || {
self.focused_webview_id
.map(|id| EmbedderEvent::Navigation(id, TraversalDirection::Forward(1)))
})
.optional_shortcut(
cfg!(not(target_os = "windows")),
CMD_OR_CONTROL,
']',
|| {
self.focused_webview_id()
.map(|id| EmbedderEvent::Navigation(id, TraversalDirection::Forward(1)))
},
)
.shortcut(CMD_OR_ALT, Key::ArrowLeft, || {
self.focused_webview_id
.map(|id| EmbedderEvent::Navigation(id, TraversalDirection::Back(1)))
})
.optional_shortcut(
cfg!(not(target_os = "windows")),
CMD_OR_CONTROL,
'[',
|| {
self.focused_webview_id
.map(|id| EmbedderEvent::Navigation(id, TraversalDirection::Back(1)))
},
)
.optional_shortcut(
self.window.get_fullscreen(),
Modifiers::empty(),
Key::Escape,
|| self.focused_webview_id.map(EmbedderEvent::ExitFullScreen),
)
.otherwise(|| {
self.focused_webview_id
.map(|_id| EmbedderEvent::Keyboard(key_event))
})
.flatten();
if let Some(event) = embedder_event {
self.event_queue.push(event);
}
}
/// Handle key events after they have been handled by Servo.
fn handle_key_from_servo(&mut self, webview_id: Option<WebViewId>, event: KeyboardEvent) {
ShortcutMatcher::from_event(event)
.shortcut(CMD_OR_CONTROL, '=', || {
self.event_queue.push(EmbedderEvent::Zoom(1.1))
})
.shortcut(CMD_OR_CONTROL, '+', || {
self.event_queue.push(EmbedderEvent::Zoom(1.1))
})
.shortcut(CMD_OR_CONTROL, '-', || {
self.event_queue.push(EmbedderEvent::Zoom(1.0 / 1.1))
})
.shortcut(CMD_OR_CONTROL, '0', || {
self.event_queue.push(EmbedderEvent::ResetZoom)
})
.shortcut(Modifiers::empty(), Key::PageDown, || {
let scroll_location = ScrollLocation::Delta(Vector2D::new(
0.0,
-self.window.page_height() + 2.0 * LINE_HEIGHT,
));
self.scroll_window_from_key(scroll_location, TouchEventType::Move, webview_id);
})
.shortcut(Modifiers::empty(), Key::PageUp, || {
let scroll_location = ScrollLocation::Delta(Vector2D::new(
0.0,
self.window.page_height() - 2.0 * LINE_HEIGHT,
));
self.scroll_window_from_key(scroll_location, TouchEventType::Move, webview_id);
})
.shortcut(Modifiers::empty(), Key::Home, || {
self.scroll_window_from_key(
ScrollLocation::Start,
TouchEventType::Move,
webview_id,
);
})
.shortcut(Modifiers::empty(), Key::End, || {
self.scroll_window_from_key(ScrollLocation::End, TouchEventType::Move, webview_id);
})
.shortcut(Modifiers::empty(), Key::ArrowUp, || {
self.scroll_window_from_key(
ScrollLocation::Delta(Vector2D::new(0.0, 3.0 * LINE_HEIGHT)),
TouchEventType::Move,
webview_id,
);
})
.shortcut(Modifiers::empty(), Key::ArrowDown, || {
self.scroll_window_from_key(
ScrollLocation::Delta(Vector2D::new(0.0, -3.0 * LINE_HEIGHT)),
TouchEventType::Move,
webview_id,
);
})
.shortcut(Modifiers::empty(), Key::ArrowLeft, || {
self.scroll_window_from_key(
ScrollLocation::Delta(Vector2D::new(LINE_HEIGHT, 0.0)),
TouchEventType::Move,
webview_id,
);
})
.shortcut(Modifiers::empty(), Key::ArrowRight, || {
self.scroll_window_from_key(
ScrollLocation::Delta(Vector2D::new(-LINE_HEIGHT, 0.0)),
TouchEventType::Move,
webview_id,
);
});
}
fn scroll_window_from_key(
&mut self,
scroll_location: ScrollLocation,
phase: TouchEventType,
webview_id: Option<WebViewId>,
) {
// In minibrowser mode the webview is offset by the toolbar
let origin = webview_id
.and_then(|id| self.webviews.get(&id))
.map(|webview| webview.rect.min.ceil().to_i32())
.unwrap_or(Point2D::zero());
let event = EmbedderEvent::Scroll(scroll_location, origin, phase);
self.event_queue.push(event);
fn send_error(&self, webview_id: Option<WebViewId>, error: String) {
let Some(webview) = webview_id.and_then(|id| self.get(id)) else {
return warn!("{error}");
};
webview.servo_webview.send_error(error);
}
/// Returns true if the caller needs to manually present a new frame.
pub fn handle_servo_events(
&mut self,
servo: &mut Servo,
clipboard: &mut Option<Clipboard>,
opts: &Opts,
events: Drain<'_, (Option<WebViewId>, EmbedderMsg)>,
events: Vec<(Option<WebViewId>, EmbedderMsg)>,
) -> ServoEventResponse {
let mut need_present = self.load_status() != LoadStatus::LoadComplete;
let mut need_update = false;
@ -656,6 +427,7 @@ where
} else {
trace_embedder_msg!(msg, "{msg:?}");
}
match msg {
EmbedderMsg::Status(status) => {
self.status_text = status;
@ -674,31 +446,24 @@ where
));
}
need_update = true;
} else {
let data = self.ensure_preload_data_mut(&webview_id);
data.title = title.clone();
}
}
},
EmbedderMsg::MoveTo(point) => {
self.window.set_position(point);
},
EmbedderMsg::ResizeTo(size) => {
EmbedderMsg::ResizeTo(inner_size) => {
if let Some(webview_id) = webview_id {
let new_rect = self.get_mut(webview_id).and_then(|webview| {
if webview.rect.size() != size.to_f32() {
webview.rect.set_size(size.to_f32());
Some(webview.rect)
} else {
None
if let Some(webview) = self.get_mut(webview_id) {
if webview.rect.size() != inner_size.to_f32() {
webview.rect.set_size(inner_size.to_f32());
webview.servo_webview.move_resize(webview.rect);
}
});
if let Some(new_rect) = new_rect {
self.event_queue
.push(EmbedderEvent::MoveResizeWebView(webview_id, new_rect));
};
if let Some(webview) = self.get(webview_id) {
self.window.request_resize(webview, inner_size);
}
}
self.window.request_inner_size(size);
},
EmbedderMsg::Prompt(definition, origin) => {
let res = if opts.headless {
@ -785,33 +550,32 @@ where
.expect("Thread spawning failed")
};
if let Err(e) = res {
let reason = format!("Failed to send Prompt response: {}", e);
self.event_queue
.push(EmbedderEvent::SendError(webview_id, reason));
self.send_error(webview_id, format!("Failed to send Prompt response: {e}"))
}
},
EmbedderMsg::AllowUnload(sender) => {
// Always allow unload for now.
if let Err(e) = sender.send(true) {
let reason = format!("Failed to send AllowUnload response: {}", e);
self.event_queue
.push(EmbedderEvent::SendError(webview_id, reason));
self.send_error(
webview_id,
format!("Failed to send AllowUnload response: {e}"),
)
}
},
EmbedderMsg::AllowNavigationRequest(pipeline_id, _url) => {
if let Some(_webview_id) = webview_id {
self.event_queue
.push(EmbedderEvent::AllowNavigationResponse(pipeline_id, true));
servo.allow_navigation_response(pipeline_id, true);
}
},
EmbedderMsg::AllowOpeningWebView(response_chan) => {
// Note: would be a place to handle pop-ups config.
// see Step 7 of #the-rules-for-choosing-a-browsing-context-given-a-browsing-context-name
if let Err(e) = response_chan.send(Some(WebViewId::new())) {
warn!("Failed to send AllowOpeningWebView response: {}", e);
};
let webview = servo.new_auxiliary_webview();
match response_chan.send(Some(webview.id())) {
Ok(()) => self.add(webview),
Err(error) => warn!("Failed to send AllowOpeningWebView response: {error}"),
}
},
EmbedderMsg::WebViewOpened(new_webview_id) => {
println!("WebView opened: {:?}", new_webview_id);
let scale = self.window.hidpi_factor().get();
let toolbar = self.window.toolbar_height().get();
@ -820,41 +584,30 @@ where
let mut rect = self.window.get_coordinates().get_viewport().to_f32();
rect.min.y += toolbar * scale;
// Make sure to not add duplicates into the creation_order vector.
// This can happen as explained in https://github.com/servo/servo/issues/33075
let preload_data = self.ensure_preload_data_mut(&new_webview_id).clone();
if let Entry::Vacant(entry) = self.webviews.entry(new_webview_id) {
entry.insert(WebView::new(rect, preload_data));
self.creation_order.push(new_webview_id);
self.event_queue
.push(EmbedderEvent::FocusWebView(new_webview_id));
self.event_queue
.push(EmbedderEvent::MoveResizeWebView(new_webview_id, rect));
self.event_queue
.push(EmbedderEvent::RaiseWebViewToTop(new_webview_id, true));
}
let webview = self
.webviews
.get(&new_webview_id)
.expect("Unknown webview opened.");
webview.servo_webview.focus();
webview.servo_webview.move_resize(rect);
webview.servo_webview.raise_to_top(true);
},
EmbedderMsg::WebViewClosed(webview_id) => {
self.webviews.retain(|&id, _| id != webview_id);
self.creation_order.retain(|&id| id != webview_id);
self.focused_webview_id = None;
if let Some(&newest_webview_id) = self.creation_order.last() {
self.event_queue
.push(EmbedderEvent::FocusWebView(newest_webview_id));
} else {
self.event_queue.push(EmbedderEvent::Quit);
}
self.close_webview(servo, webview_id);
},
EmbedderMsg::WebViewFocused(webview_id) => {
for (id, webview) in &mut self.webviews {
webview.focused = *id == webview_id;
if let Some(webview) = self.focused_webview_id.and_then(|id| self.get_mut(id)) {
webview.focused = false;
}
self.focused_webview_id = Some(webview_id);
need_update = true;
// Show the most recently created webview and hide all others.
// TODO: Stop doing this once we have full multiple webviews support
self.event_queue
.push(EmbedderEvent::ShowWebView(webview_id, true));
if let Some(webview) = self.get_mut(webview_id) {
webview.focused = true;
webview.servo_webview.show(true);
self.focused_webview_id = Some(webview_id);
need_update = true;
};
},
EmbedderMsg::WebViewBlurred => {
for webview in self.webviews.values_mut() {
@ -863,28 +616,24 @@ where
self.focused_webview_id = None;
},
EmbedderMsg::Keyboard(key_event) => {
self.handle_key_from_servo(webview_id, key_event);
self.handle_overridable_key_bindings(webview_id, key_event);
},
EmbedderMsg::ClearClipboardContents => {
self.clipboard
clipboard
.as_mut()
.and_then(|clipboard| clipboard.clear().ok());
},
EmbedderMsg::GetClipboardContents(sender) => {
let contents = self
.clipboard
let contents = clipboard
.as_mut()
.and_then(|clipboard| clipboard.get_text().ok())
.unwrap_or_else(|| {
warn!("Error getting clipboard text. Returning empty string.");
String::new()
});
.unwrap_or_default();
if let Err(e) = sender.send(contents) {
warn!("Failed to send clipboard ({})", e);
}
},
EmbedderMsg::SetClipboardContents(text) => {
if let Some(ref mut clipboard) = self.clipboard {
if let Some(clipboard) = clipboard.as_mut() {
if let Err(e) = clipboard.set_text(text) {
warn!("Error setting clipboard contents ({})", e);
}
@ -897,42 +646,31 @@ where
// FIXME: show favicons in the UI somehow
},
EmbedderMsg::HeadParsed => {
if let Some(webview_id) = webview_id {
if let Some(webview) = self.get_mut(webview_id) {
webview.load_status = LoadStatus::HeadParsed;
need_update = true;
}
}
if let Some(webview) = webview_id.and_then(|id| self.get_mut(id)) {
webview.load_status = LoadStatus::HeadParsed;
need_update = true;
};
},
EmbedderMsg::HistoryChanged(urls, current) => {
if let Some(webview_id) = webview_id {
if let Some(webview) = self.get_mut(webview_id) {
webview.url = Some(urls[current].clone());
need_update = true;
} else {
let data = self.ensure_preload_data_mut(&webview_id);
data.url = Some(urls[current].clone());
}
}
if let Some(webview) = webview_id.and_then(|id| self.get_mut(id)) {
webview.url = Some(urls[current].clone());
need_update = true;
};
},
EmbedderMsg::SetFullscreenState(state) => {
self.window.set_fullscreen(state);
},
EmbedderMsg::LoadStart => {
if let Some(webview_id) = webview_id {
if let Some(webview) = self.get_mut(webview_id) {
webview.load_status = LoadStatus::LoadStart;
need_update = true;
}
}
if let Some(webview) = webview_id.and_then(|id| self.get_mut(id)) {
webview.load_status = LoadStatus::LoadStart;
need_update = true;
};
},
EmbedderMsg::LoadComplete => {
if let Some(webview_id) = webview_id {
if let Some(webview) = self.get_mut(webview_id) {
webview.load_status = LoadStatus::LoadComplete;
need_update = true;
}
}
if let Some(webview) = webview_id.and_then(|id| self.get_mut(id)) {
webview.load_status = LoadStatus::LoadComplete;
need_update = true;
};
},
EmbedderMsg::WebResourceRequested(_web_resource_request, _response_sender) => {},
EmbedderMsg::Shutdown => {
@ -942,21 +680,23 @@ where
EmbedderMsg::GetSelectedBluetoothDevice(devices, sender) => {
let selected = platform_get_selected_devices(devices);
if let Err(e) = sender.send(selected) {
let reason =
format!("Failed to send GetSelectedBluetoothDevice response: {}", e);
self.event_queue
.push(EmbedderEvent::SendError(None, reason));
self.send_error(
webview_id,
format!("Failed to send GetSelectedBluetoothDevice response: {e}"),
);
};
},
EmbedderMsg::SelectFiles(patterns, multiple_files, sender) => {
let res = match (opts.headless, get_selected_files(patterns, multiple_files)) {
let result = match (opts.headless, get_selected_files(patterns, multiple_files))
{
(true, _) | (false, None) => sender.send(None),
(false, Some(files)) => sender.send(Some(files)),
};
if let Err(e) = res {
let reason = format!("Failed to send SelectFiles response: {}", e);
self.event_queue
.push(EmbedderEvent::SendError(None, reason));
if let Err(e) = result {
self.send_error(
webview_id,
format!("Failed to send SelectFiles response: {e}"),
);
};
},
EmbedderMsg::PromptPermission(prompt, sender) => {
@ -993,15 +733,12 @@ where
need_present = true;
},
EmbedderMsg::EventDelivered(event) => {
if let (Some(webview_id), CompositorEventVariant::MouseButtonEvent) =
(webview_id, event)
{
trace!("{}: Got a mouse button event", webview_id);
self.event_queue
.push(EmbedderEvent::RaiseWebViewToTop(webview_id, true));
self.event_queue
.push(EmbedderEvent::FocusWebView(webview_id));
}
if let Some(webview) = webview_id.and_then(|id| self.get_mut(id)) {
if let CompositorEventVariant::MouseButtonEvent = event {
webview.servo_webview.raise_to_top(true);
webview.servo_webview.focus();
}
};
},
EmbedderMsg::PlayGamepadHapticEffect(index, effect, effect_complete_sender) => {
match effect {
@ -1024,6 +761,69 @@ where
need_update,
}
}
/// Handle servoshell key bindings that may have been prevented by the page in the focused webview.
fn handle_overridable_key_bindings(
&mut self,
webview_id: Option<WebViewId>,
event: KeyboardEvent,
) {
let Some(webview) = webview_id.and_then(|id| self.get(id)) else {
return;
};
let origin = webview.rect.min.ceil().to_i32();
let webview = &webview.servo_webview;
ShortcutMatcher::from_event(event)
.shortcut(CMD_OR_CONTROL, '=', || {
webview.set_zoom(1.1);
})
.shortcut(CMD_OR_CONTROL, '+', || {
webview.set_zoom(1.1);
})
.shortcut(CMD_OR_CONTROL, '-', || {
webview.set_zoom(1.0 / 1.1);
})
.shortcut(CMD_OR_CONTROL, '0', || {
webview.reset_zoom();
})
.shortcut(Modifiers::empty(), Key::PageDown, || {
let scroll_location = ScrollLocation::Delta(Vector2D::new(
0.0,
-self.window.page_height() + 2.0 * LINE_HEIGHT,
));
webview.notify_scroll_event(scroll_location, origin, TouchEventType::Move);
})
.shortcut(Modifiers::empty(), Key::PageUp, || {
let scroll_location = ScrollLocation::Delta(Vector2D::new(
0.0,
self.window.page_height() - 2.0 * LINE_HEIGHT,
));
webview.notify_scroll_event(scroll_location, origin, TouchEventType::Move);
})
.shortcut(Modifiers::empty(), Key::Home, || {
webview.notify_scroll_event(ScrollLocation::Start, origin, TouchEventType::Move);
})
.shortcut(Modifiers::empty(), Key::End, || {
webview.notify_scroll_event(ScrollLocation::End, origin, TouchEventType::Move);
})
.shortcut(Modifiers::empty(), Key::ArrowUp, || {
let location = ScrollLocation::Delta(Vector2D::new(0.0, 3.0 * LINE_HEIGHT));
webview.notify_scroll_event(location, origin, TouchEventType::Move);
})
.shortcut(Modifiers::empty(), Key::ArrowDown, || {
let location = ScrollLocation::Delta(Vector2D::new(0.0, -3.0 * LINE_HEIGHT));
webview.notify_scroll_event(location, origin, TouchEventType::Move);
})
.shortcut(Modifiers::empty(), Key::ArrowLeft, || {
let location = ScrollLocation::Delta(Vector2D::new(LINE_HEIGHT, 0.0));
webview.notify_scroll_event(location, origin, TouchEventType::Move);
})
.shortcut(Modifiers::empty(), Key::ArrowRight, || {
let location = ScrollLocation::Delta(Vector2D::new(-LINE_HEIGHT, 0.0));
webview.notify_scroll_event(location, origin, TouchEventType::Move);
});
}
}
#[cfg(target_os = "linux")]

View file

@ -7,17 +7,19 @@
use std::rc::Rc;
use arboard::Clipboard;
use euclid::{Length, Scale};
use servo::compositing::windowing::{EmbedderEvent, WindowMethods};
use servo::compositing::windowing::WindowMethods;
use servo::servo_geometry::DeviceIndependentPixel;
use servo::webrender_api::units::{DeviceIntPoint, DeviceIntSize, DevicePixel};
use servo::Cursor;
use servo::{Cursor, Servo};
use super::webview::{WebView, WebViewManager};
// This should vary by zoom level and maybe actual text size (focused or under cursor)
pub const LINE_HEIGHT: f32 = 38.0;
pub trait WindowPortsMethods: WindowMethods {
fn get_events(&self) -> Vec<EmbedderEvent>;
fn id(&self) -> winit::window::WindowId;
fn hidpi_factor(&self) -> Scale<f32, DeviceIndependentPixel, DevicePixel> {
self.device_pixel_ratio_override()
@ -29,10 +31,18 @@ pub trait WindowPortsMethods: WindowMethods {
) -> Option<Scale<f32, DeviceIndependentPixel, DevicePixel>>;
fn page_height(&self) -> f32;
fn get_fullscreen(&self) -> bool;
fn queue_embedder_events_for_winit_event(&self, event: winit::event::WindowEvent);
fn handle_winit_event(
&self,
servo: &Servo,
clipboard: &mut Option<Clipboard>,
webviews: &mut WebViewManager,
event: winit::event::WindowEvent,
);
fn is_animating(&self) -> bool;
fn set_title(&self, _title: &str) {}
fn request_inner_size(&self, size: DeviceIntSize) -> Option<DeviceIntSize>;
/// Request a new inner size for the window, not including external decorations.
fn request_resize(&self, webview: &WebView, inner_size: DeviceIntSize)
-> Option<DeviceIntSize>;
fn set_position(&self, _point: DeviceIntPoint) {}
fn set_fullscreen(&self, _state: bool) {}
fn set_cursor(&self, _cursor: Cursor) {}