libservo: Add an initial WebView data structure to the API (#35119)

This patch introduces a new handle-based webview API to libservo, with
two main design goals:

1. The lifetime of the handles controls the lifetime of the webview,
   giving the embedder full control over exactly when webviews are
   created and destroyed. This is consistent with how WebKitGTK’s
   WebView works; the engine can only create webviews via a create
   request, and can only destroy them via a close request.
2. All methods are infallible; if the constellation dies, the embedder
   finds out when calling Servo::handle_events.

For the moment, the embedder is only responsible for creating the
WebView id, and not the internal TopLevelBrowsingContext data
structures. This is so that the ScriptThread is able to get a handle on
the new WebView's WindowProxy in the case that it's an auxiliary
browsing context. In the future, the embedder should also be responsible
for creating the TopLevelBrowsingContext and the ScriptThread should
have mechanism to associate the two views so that WebView creation is
always executed through the same code path in the embedding layer. For
now, it's enough that the embedder can get a handle to the new WebView
when it's creation is requested.

Once we replace EmbedderMsg with a webview delegate trait, we will pass
WebView handles to the embedder, rather than webview ids. We’ll also add
detailed docs, once the design settles.

Signed-off-by: Delan Azabani <dazabani@igalia.com>
Co-authored-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
Delan Azabani 2025-01-25 16:17:50 +08:00 committed by GitHub
parent d5d7b0d34f
commit 2ce7709b8b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 348 additions and 104 deletions

1
Cargo.lock generated
View file

@ -4233,6 +4233,7 @@ dependencies = [
"style_traits", "style_traits",
"surfman", "surfman",
"tracing", "tracing",
"url",
"webdriver_server", "webdriver_server",
"webgpu", "webgpu",
"webrender", "webrender",

View file

@ -156,7 +156,7 @@ pub struct IOCompositor {
ready_to_save_state: ReadyState, ready_to_save_state: ReadyState,
/// The webrender renderer. /// The webrender renderer.
webrender: webrender::Renderer, webrender: Option<webrender::Renderer>,
/// The active webrender document. /// The active webrender document.
webrender_document: DocumentId, webrender_document: DocumentId,
@ -386,7 +386,7 @@ impl IOCompositor {
constellation_chan: state.constellation_chan, constellation_chan: state.constellation_chan,
time_profiler_chan: state.time_profiler_chan, time_profiler_chan: state.time_profiler_chan,
ready_to_save_state: ReadyState::Unknown, ready_to_save_state: ReadyState::Unknown,
webrender: state.webrender, webrender: Some(state.webrender),
webrender_document: state.webrender_document, webrender_document: state.webrender_document,
webrender_api: state.webrender_api, webrender_api: state.webrender_api,
rendering_context: state.rendering_context, rendering_context: state.rendering_context,
@ -411,11 +411,13 @@ impl IOCompositor {
compositor compositor
} }
pub fn deinit(self) { pub fn deinit(&mut self) {
if let Err(err) = self.rendering_context.make_current() { if let Err(err) = self.rendering_context.make_current() {
warn!("Failed to make the rendering context current: {:?}", err); warn!("Failed to make the rendering context current: {:?}", err);
} }
self.webrender.deinit(); if let Some(webrender) = self.webrender.take() {
webrender.deinit();
}
} }
fn update_cursor(&mut self, result: CompositorHitTestResult) { fn update_cursor(&mut self, result: CompositorHitTestResult) {
@ -515,10 +517,12 @@ impl IOCompositor {
self.remove_webview(top_level_browsing_context_id); self.remove_webview(top_level_browsing_context_id);
}, },
// TODO: remove this
CompositorMsg::MoveResizeWebView(webview_id, rect) => { CompositorMsg::MoveResizeWebView(webview_id, rect) => {
self.move_resize_webview(webview_id, rect); self.move_resize_webview(webview_id, rect);
}, },
// TODO: remove this
CompositorMsg::ShowWebView(webview_id, hide_others) => { CompositorMsg::ShowWebView(webview_id, hide_others) => {
if let Err(UnknownWebView(webview_id)) = self.show_webview(webview_id, hide_others) if let Err(UnknownWebView(webview_id)) = self.show_webview(webview_id, hide_others)
{ {
@ -526,12 +530,14 @@ impl IOCompositor {
} }
}, },
// TODO: remove this
CompositorMsg::HideWebView(webview_id) => { CompositorMsg::HideWebView(webview_id) => {
if let Err(UnknownWebView(webview_id)) = self.hide_webview(webview_id) { if let Err(UnknownWebView(webview_id)) = self.hide_webview(webview_id) {
warn!("{webview_id}: HideWebView on unknown webview id"); warn!("{webview_id}: HideWebView on unknown webview id");
} }
}, },
// TODO: remove this
CompositorMsg::RaiseWebViewToTop(webview_id, hide_others) => { CompositorMsg::RaiseWebViewToTop(webview_id, hide_others) => {
if let Err(UnknownWebView(webview_id)) = if let Err(UnknownWebView(webview_id)) =
self.raise_webview_to_top(webview_id, hide_others) self.raise_webview_to_top(webview_id, hide_others)
@ -1941,7 +1947,8 @@ impl IOCompositor {
for id in self.pipeline_details.keys() { for id in self.pipeline_details.keys() {
if let Some(WebRenderEpoch(epoch)) = self if let Some(WebRenderEpoch(epoch)) = self
.webrender .webrender
.current_epoch(self.webrender_document, id.into()) .as_ref()
.and_then(|wr| wr.current_epoch(self.webrender_document, id.into()))
{ {
let epoch = Epoch(epoch); let epoch = Epoch(epoch);
pipeline_epochs.insert(*id, epoch); pipeline_epochs.insert(*id, epoch);
@ -2016,7 +2023,9 @@ impl IOCompositor {
} }
self.assert_no_gl_error(); self.assert_no_gl_error();
self.webrender.update(); if let Some(webrender) = self.webrender.as_mut() {
webrender.update();
}
let wait_for_stable_image = matches!( let wait_for_stable_image = matches!(
target, target,
@ -2073,7 +2082,9 @@ impl IOCompositor {
// Paint the scene. // Paint the scene.
// TODO(gw): Take notice of any errors the renderer returns! // TODO(gw): Take notice of any errors the renderer returns!
self.clear_background(); self.clear_background();
self.webrender.render(size, 0 /* buffer_age */).ok(); if let Some(webrender) = self.webrender.as_mut() {
webrender.render(size, 0 /* buffer_age */).ok();
}
}, },
); );
@ -2204,7 +2215,8 @@ impl IOCompositor {
for (pipeline_id, pending_epochs) in pending_paint_metrics.iter_mut() { for (pipeline_id, pending_epochs) in pending_paint_metrics.iter_mut() {
let Some(WebRenderEpoch(current_epoch)) = self let Some(WebRenderEpoch(current_epoch)) = self
.webrender .webrender
.current_epoch(self.webrender_document, pipeline_id.into()) .as_ref()
.and_then(|wr| wr.current_epoch(self.webrender_document, pipeline_id.into()))
else { else {
continue; continue;
}; };
@ -2427,7 +2439,10 @@ impl IOCompositor {
} }
pub fn toggle_webrender_debug(&mut self, option: WebRenderDebugOption) { pub fn toggle_webrender_debug(&mut self, option: WebRenderDebugOption) {
let mut flags = self.webrender.get_debug_flags(); let Some(webrender) = self.webrender.as_mut() else {
return;
};
let mut flags = webrender.get_debug_flags();
let flag = match option { let flag = match option {
WebRenderDebugOption::Profiler => { WebRenderDebugOption::Profiler => {
webrender::DebugFlags::PROFILER_DBG | webrender::DebugFlags::PROFILER_DBG |
@ -2438,7 +2453,7 @@ impl IOCompositor {
WebRenderDebugOption::RenderTargetDebug => webrender::DebugFlags::RENDER_TARGET_DBG, WebRenderDebugOption::RenderTargetDebug => webrender::DebugFlags::RENDER_TARGET_DBG,
}; };
flags.toggle(flag); flags.toggle(flag);
self.webrender.set_debug_flags(flags); webrender.set_debug_flags(flags);
let mut txn = Transaction::new(); let mut txn = Transaction::new();
self.generate_frame(&mut txn, RenderReasons::TESTING); self.generate_frame(&mut txn, RenderReasons::TESTING);

View file

@ -4567,13 +4567,23 @@ where
self.handle_close_top_level_browsing_context(top_level_browsing_context_id); self.handle_close_top_level_browsing_context(top_level_browsing_context_id);
}, },
WebDriverCommandMsg::NewWebView(sender, load_sender) => { WebDriverCommandMsg::NewWebView(sender, load_sender) => {
let top_level_browsing_context_id = TopLevelBrowsingContextId::new(); let (chan, port) = match ipc::channel() {
Ok(result) => result,
Err(error) => return warn!("Failed to create channel: {error:?}"),
};
self.embedder_proxy
.send((None, EmbedderMsg::AllowOpeningWebView(chan)));
let webview_id = match port.recv() {
Ok(Some(webview_id)) => webview_id,
Ok(None) => return warn!("Embedder refused to allow opening webview"),
Err(error) => return warn!("Failed to receive webview id: {error:?}"),
};
self.handle_new_top_level_browsing_context( self.handle_new_top_level_browsing_context(
ServoUrl::parse_with_base(None, "about:blank").expect("Infallible parse"), ServoUrl::parse_with_base(None, "about:blank").expect("Infallible parse"),
top_level_browsing_context_id, webview_id,
Some(load_sender), Some(load_sender),
); );
let _ = sender.send(top_level_browsing_context_id); let _ = sender.send(webview_id);
}, },
WebDriverCommandMsg::FocusWebView(top_level_browsing_context_id) => { WebDriverCommandMsg::FocusWebView(top_level_browsing_context_id) => {
self.handle_focus_web_view(top_level_browsing_context_id); self.handle_focus_web_view(top_level_browsing_context_id);

View file

@ -294,8 +294,7 @@ impl WindowProxy {
.unwrap(); .unwrap();
let msg = EmbedderMsg::AllowOpeningWebView(chan); let msg = EmbedderMsg::AllowOpeningWebView(chan);
window.send_to_embedder(msg); window.send_to_embedder(msg);
if port.recv().unwrap() { if let Some(new_top_level_browsing_context_id) = port.recv().unwrap() {
let new_top_level_browsing_context_id = TopLevelBrowsingContextId::new();
let new_browsing_context_id = let new_browsing_context_id =
BrowsingContextId::from(new_top_level_browsing_context_id); BrowsingContextId::from(new_top_level_browsing_context_id);
let new_pipeline_id = PipelineId::new(); let new_pipeline_id = PipelineId::new();

View file

@ -89,6 +89,7 @@ servo_url = { path = "../url" }
style = { workspace = true } style = { workspace = true }
style_traits = { workspace = true } style_traits = { workspace = true }
tracing = { workspace = true, optional = true } tracing = { workspace = true, optional = true }
url = { workspace = true }
webdriver_server = { path = "../webdriver_server", optional = true } webdriver_server = { path = "../webdriver_server", optional = true }
webgpu = { path = "../webgpu" } webgpu = { path = "../webgpu" }
webrender = { workspace = true } webrender = { workspace = true }

View file

@ -4,19 +4,17 @@
use std::cell::Cell; use std::cell::Cell;
use std::error::Error; use std::error::Error;
use std::mem::replace;
use std::rc::Rc; use std::rc::Rc;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use base::id::WebViewId;
use compositing::windowing::{AnimationState, EmbedderEvent, EmbedderMethods, WindowMethods}; use compositing::windowing::{AnimationState, EmbedderEvent, EmbedderMethods, WindowMethods};
use embedder_traits::EmbedderMsg; use embedder_traits::EmbedderMsg;
use euclid::{Point2D, Scale, Size2D}; use euclid::{Point2D, Scale, Size2D};
use servo::Servo; use servo::{Servo, WebView};
use servo_geometry::DeviceIndependentPixel; use servo_geometry::DeviceIndependentPixel;
use servo_url::ServoUrl;
use surfman::{Connection, SurfaceType}; use surfman::{Connection, SurfaceType};
use tracing::warn; use tracing::warn;
use url::Url;
use webrender_api::units::{DeviceIntPoint, DeviceIntRect, DevicePixel}; use webrender_api::units::{DeviceIntPoint, DeviceIntRect, DevicePixel};
use webrender_traits::SurfmanRenderingContext; use webrender_traits::SurfmanRenderingContext;
use winit::application::ApplicationHandler; use winit::application::ApplicationHandler;
@ -37,6 +35,10 @@ fn main() -> Result<(), Box<dyn Error>> {
let mut app = App::new(&event_loop); let mut app = App::new(&event_loop);
event_loop.run_app(&mut app)?; event_loop.run_app(&mut app)?;
if let App::Running { servo, .. } = app {
servo.deinit();
}
Ok(()) Ok(())
} }
@ -45,8 +47,8 @@ enum App {
Running { Running {
window_delegate: Rc<WindowDelegate>, window_delegate: Rc<WindowDelegate>,
servo: Servo, servo: Servo,
webviews: Vec<WebView>,
}, },
Exiting,
} }
impl App { impl App {
@ -90,7 +92,7 @@ impl ApplicationHandler<WakerEvent> for App {
.make_gl_context_current() .make_gl_context_current()
.expect("Failed to make context current"); .expect("Failed to make context current");
let window_delegate = Rc::new(WindowDelegate::new(window)); let window_delegate = Rc::new(WindowDelegate::new(window));
let mut servo = Servo::new( let servo = Servo::new(
Default::default(), Default::default(),
Default::default(), Default::default(),
Rc::new(rendering_context), Rc::new(rendering_context),
@ -102,14 +104,14 @@ impl ApplicationHandler<WakerEvent> for App {
compositing::CompositeTarget::Window, compositing::CompositeTarget::Window,
); );
servo.setup_logging(); servo.setup_logging();
servo.handle_events([EmbedderEvent::NewWebView( let webviews = vec![servo.new_webview(
ServoUrl::parse("https://demo.servo.org/experiments/twgl-tunnel/") Url::parse("https://demo.servo.org/experiments/twgl-tunnel/")
.expect("Guaranteed by argument"), .expect("Guaranteed by argument"),
WebViewId::new(), )];
)]);
*self = Self::Running { *self = Self::Running {
window_delegate, window_delegate,
servo, servo,
webviews,
}; };
} }
} }
@ -123,46 +125,69 @@ impl ApplicationHandler<WakerEvent> for App {
if let Self::Running { if let Self::Running {
window_delegate, window_delegate,
servo, servo,
webviews,
} = self } = self
{ {
let mut events_for_servo = vec![]; for (_webview_id, message) in servo.get_events().collect::<Vec<_>>() {
for (_webview_id, message) in servo.get_events() {
match message { match message {
// FIXME: rust-analyzer autocompletes this as top_level_browsing_context_id // FIXME: rust-analyzer autocompletes this as top_level_browsing_context_id
EmbedderMsg::WebViewOpened(webview_id) => { EmbedderMsg::WebViewOpened(webview_id) => {
// TODO: We currently assume `webview` refers to the same webview as `_webview_id`
let rect = window_delegate.get_coordinates().get_viewport().to_f32(); let rect = window_delegate.get_coordinates().get_viewport().to_f32();
events_for_servo.extend([ if let Some(webview) =
EmbedderEvent::FocusWebView(webview_id), webviews.iter().find(|webview| webview.id() == webview_id)
EmbedderEvent::MoveResizeWebView(webview_id, rect), {
EmbedderEvent::RaiseWebViewToTop(webview_id, true), webview.focus();
]); webview.move_resize(rect);
webview.raise_to_top(true);
}
},
EmbedderMsg::AllowOpeningWebView(webview_id_sender) => {
let webview = servo.new_auxiliary_webview();
let _ = webview_id_sender.send(Some(webview.id()));
webviews.push(webview);
},
EmbedderMsg::AllowNavigationRequest(pipeline_id, _) => {
servo.handle_events([EmbedderEvent::AllowNavigationResponse(
pipeline_id,
true,
)]);
}, },
_ => {}, _ => {},
} }
} }
servo.handle_events(events_for_servo); // FIXME: still needed for the compositor to actually run
servo.handle_events([]);
} }
match event { match event {
WindowEvent::CloseRequested => { WindowEvent::CloseRequested => {
if matches!(self, Self::Running { .. }) {
let Self::Running { servo, .. } = replace(self, Self::Exiting) else {
unreachable!()
};
// TODO: ask Servo to shut down and wait for EmbedderMsg::Shutdown?
servo.deinit();
}
event_loop.exit(); event_loop.exit();
}, },
WindowEvent::RedrawRequested => { WindowEvent::RedrawRequested => {
if let Self::Running { if let Self::Running {
window_delegate, window_delegate,
servo, servo,
..
} = self } = self
{ {
servo.present(); servo.present();
window_delegate.window.request_redraw(); window_delegate.window.request_redraw();
} }
}, },
WindowEvent::MouseInput { .. } => {
// When the window is clicked, close the last webview by dropping its handle,
// then show the next most recently opened webview.
//
// TODO: Test closing webviews a better way, so that we can use mouse input to test
// input handling.
if let Self::Running { webviews, .. } = self {
let _ = webviews.pop();
match webviews.last() {
Some(last) => last.show(true),
None => event_loop.exit(),
}
}
},
_ => (), _ => (),
} }
} }

View file

@ -17,7 +17,11 @@
//! `Servo` is fed events from a generic type that implements the //! `Servo` is fed events from a generic type that implements the
//! `WindowMethods` trait. //! `WindowMethods` trait.
use std::borrow::{BorrowMut, Cow}; mod proxies;
mod webview;
use std::borrow::Cow;
use std::cell::RefCell;
use std::cmp::max; use std::cmp::max;
use std::path::PathBuf; use std::path::PathBuf;
use std::rc::Rc; use std::rc::Rc;
@ -103,12 +107,15 @@ use webrender_traits::{
}; };
pub use { pub use {
background_hang_monitor, base, bluetooth, bluetooth_traits, canvas, canvas_traits, compositing, background_hang_monitor, base, bluetooth, bluetooth_traits, canvas, canvas_traits, compositing,
constellation, devtools, devtools_traits, embedder_traits, euclid, fonts, ipc_channel, devtools, devtools_traits, embedder_traits, euclid, fonts, ipc_channel, keyboard_types,
keyboard_types, layout_thread_2020, media, net, net_traits, profile, profile_traits, script, layout_thread_2020, media, net, net_traits, profile, profile_traits, script,
script_layout_interface, script_traits, servo_config as config, servo_config, servo_geometry, script_layout_interface, script_traits, servo_config as config, servo_config, servo_geometry,
servo_url as url, servo_url, style, style_traits, webrender_api, webrender_traits, servo_url, style, style_traits, webrender_api, webrender_traits,
}; };
use crate::proxies::ConstellationProxy;
pub use crate::webview::WebView;
#[cfg(feature = "webdriver")] #[cfg(feature = "webdriver")]
fn webdriver(port: u16, constellation: Sender<ConstellationMsg>) { fn webdriver(port: u16, constellation: Sender<ConstellationMsg>) {
webdriver_server::start_server(port, constellation); webdriver_server::start_server(port, constellation);
@ -177,8 +184,8 @@ mod media_platform {
/// loop to pump messages between the embedding application and /// loop to pump messages between the embedding application and
/// various browser components. /// various browser components.
pub struct Servo { pub struct Servo {
compositor: IOCompositor, compositor: Rc<RefCell<IOCompositor>>,
constellation_chan: Sender<ConstellationMsg>, constellation_proxy: ConstellationProxy,
embedder_receiver: EmbedderReceiver, embedder_receiver: EmbedderReceiver,
messages_for_embedder: Vec<(Option<TopLevelBrowsingContextId>, EmbedderMsg)>, messages_for_embedder: Vec<(Option<TopLevelBrowsingContextId>, EmbedderMsg)>,
profiler_enabled: bool, profiler_enabled: bool,
@ -440,7 +447,7 @@ impl Servo {
); );
let (player_context, glplayer_threads) = Self::create_media_window_gl_context( let (player_context, glplayer_threads) = Self::create_media_window_gl_context(
external_image_handlers.borrow_mut(), &mut external_image_handlers,
external_images.clone(), external_images.clone(),
&rendering_context, &rendering_context,
); );
@ -518,8 +525,8 @@ impl Servo {
); );
Servo { Servo {
compositor, compositor: Rc::new(RefCell::new(compositor)),
constellation_chan, constellation_proxy: ConstellationProxy::new(constellation_chan),
embedder_receiver, embedder_receiver,
messages_for_embedder: Vec::new(), messages_for_embedder: Vec::new(),
profiler_enabled: false, profiler_enabled: false,
@ -640,15 +647,15 @@ impl Servo {
EmbedderEvent::Idle => {}, EmbedderEvent::Idle => {},
EmbedderEvent::Refresh => { EmbedderEvent::Refresh => {
self.compositor.composite(); self.compositor.borrow_mut().composite();
}, },
EmbedderEvent::WindowResize => { EmbedderEvent::WindowResize => {
return self.compositor.on_resize_window_event(); return self.compositor.borrow_mut().on_resize_window_event();
}, },
EmbedderEvent::ThemeChange(theme) => { EmbedderEvent::ThemeChange(theme) => {
let msg = ConstellationMsg::ThemeChange(theme); let msg = ConstellationMsg::ThemeChange(theme);
if let Err(e) = self.constellation_chan.send(msg) { if let Err(e) = self.constellation_proxy.try_send(msg) {
warn!( warn!(
"Sending platform theme change to constellation failed ({:?}).", "Sending platform theme change to constellation failed ({:?}).",
e e
@ -656,16 +663,17 @@ impl Servo {
} }
}, },
EmbedderEvent::InvalidateNativeSurface => { EmbedderEvent::InvalidateNativeSurface => {
self.compositor.invalidate_native_surface(); self.compositor.borrow_mut().invalidate_native_surface();
}, },
EmbedderEvent::ReplaceNativeSurface(native_widget, coords) => { EmbedderEvent::ReplaceNativeSurface(native_widget, coords) => {
self.compositor self.compositor
.borrow_mut()
.replace_native_surface(native_widget, coords); .replace_native_surface(native_widget, coords);
self.compositor.composite(); self.compositor.borrow_mut().composite();
}, },
EmbedderEvent::AllowNavigationResponse(pipeline_id, allowed) => { EmbedderEvent::AllowNavigationResponse(pipeline_id, allowed) => {
let msg = ConstellationMsg::AllowNavigationResponse(pipeline_id, allowed); let msg = ConstellationMsg::AllowNavigationResponse(pipeline_id, allowed);
if let Err(e) = self.constellation_chan.send(msg) { if let Err(e) = self.constellation_proxy.try_send(msg) {
warn!( warn!(
"Sending allow navigation to constellation failed ({:?}).", "Sending allow navigation to constellation failed ({:?}).",
e e
@ -675,57 +683,66 @@ impl Servo {
EmbedderEvent::LoadUrl(top_level_browsing_context_id, url) => { EmbedderEvent::LoadUrl(top_level_browsing_context_id, url) => {
let msg = ConstellationMsg::LoadUrl(top_level_browsing_context_id, url); let msg = ConstellationMsg::LoadUrl(top_level_browsing_context_id, url);
if let Err(e) = self.constellation_chan.send(msg) { if let Err(e) = self.constellation_proxy.try_send(msg) {
warn!("Sending load url to constellation failed ({:?}).", e); warn!("Sending load url to constellation failed ({:?}).", e);
} }
}, },
EmbedderEvent::ClearCache => { EmbedderEvent::ClearCache => {
let msg = ConstellationMsg::ClearCache; let msg = ConstellationMsg::ClearCache;
if let Err(e) = self.constellation_chan.send(msg) { if let Err(e) = self.constellation_proxy.try_send(msg) {
warn!("Sending clear cache to constellation failed ({:?}).", e); warn!("Sending clear cache to constellation failed ({:?}).", e);
} }
}, },
EmbedderEvent::MouseWindowEventClass(mouse_window_event) => { EmbedderEvent::MouseWindowEventClass(mouse_window_event) => {
self.compositor self.compositor
.borrow_mut()
.on_mouse_window_event_class(mouse_window_event); .on_mouse_window_event_class(mouse_window_event);
}, },
EmbedderEvent::MouseWindowMoveEventClass(cursor) => { EmbedderEvent::MouseWindowMoveEventClass(cursor) => {
self.compositor.on_mouse_window_move_event_class(cursor); self.compositor
.borrow_mut()
.on_mouse_window_move_event_class(cursor);
}, },
EmbedderEvent::Touch(event_type, identifier, location) => { EmbedderEvent::Touch(event_type, identifier, location) => {
self.compositor self.compositor
.borrow_mut()
.on_touch_event(event_type, identifier, location); .on_touch_event(event_type, identifier, location);
}, },
EmbedderEvent::Wheel(delta, location) => { EmbedderEvent::Wheel(delta, location) => {
self.compositor.on_wheel_event(delta, location); self.compositor.borrow_mut().on_wheel_event(delta, location);
}, },
EmbedderEvent::Scroll(scroll_location, cursor, phase) => { EmbedderEvent::Scroll(scroll_location, cursor, phase) => {
self.compositor self.compositor
.borrow_mut()
.on_scroll_event(scroll_location, cursor, phase); .on_scroll_event(scroll_location, cursor, phase);
}, },
EmbedderEvent::Zoom(magnification) => { EmbedderEvent::Zoom(magnification) => {
self.compositor.on_zoom_window_event(magnification); self.compositor
.borrow_mut()
.on_zoom_window_event(magnification);
}, },
EmbedderEvent::ResetZoom => { EmbedderEvent::ResetZoom => {
self.compositor.on_zoom_reset_window_event(); self.compositor.borrow_mut().on_zoom_reset_window_event();
}, },
EmbedderEvent::PinchZoom(zoom) => { EmbedderEvent::PinchZoom(zoom) => {
self.compositor.on_pinch_zoom_window_event(zoom); self.compositor
.borrow_mut()
.on_pinch_zoom_window_event(zoom);
}, },
EmbedderEvent::Navigation(top_level_browsing_context_id, direction) => { EmbedderEvent::Navigation(top_level_browsing_context_id, direction) => {
let msg = let msg =
ConstellationMsg::TraverseHistory(top_level_browsing_context_id, direction); ConstellationMsg::TraverseHistory(top_level_browsing_context_id, direction);
if let Err(e) = self.constellation_chan.send(msg) { if let Err(e) = self.constellation_proxy.try_send(msg) {
warn!("Sending navigation to constellation failed ({:?}).", e); warn!("Sending navigation to constellation failed ({:?}).", e);
} }
self.messages_for_embedder.push(( self.messages_for_embedder.push((
@ -736,14 +753,14 @@ impl Servo {
EmbedderEvent::Keyboard(key_event) => { EmbedderEvent::Keyboard(key_event) => {
let msg = ConstellationMsg::Keyboard(key_event); let msg = ConstellationMsg::Keyboard(key_event);
if let Err(e) = self.constellation_chan.send(msg) { if let Err(e) = self.constellation_proxy.try_send(msg) {
warn!("Sending keyboard event to constellation failed ({:?}).", e); warn!("Sending keyboard event to constellation failed ({:?}).", e);
} }
}, },
EmbedderEvent::IMEComposition(ime_event) => { EmbedderEvent::IMEComposition(ime_event) => {
let msg = ConstellationMsg::IMECompositionEvent(ime_event); let msg = ConstellationMsg::IMECompositionEvent(ime_event);
if let Err(e) = self.constellation_chan.send(msg) { if let Err(e) = self.constellation_proxy.try_send(msg) {
warn!( warn!(
"Sending composition event to constellation failed ({:?}).", "Sending composition event to constellation failed ({:?}).",
e e
@ -753,7 +770,7 @@ impl Servo {
EmbedderEvent::IMEDismissed => { EmbedderEvent::IMEDismissed => {
let msg = ConstellationMsg::IMEDismissed; let msg = ConstellationMsg::IMEDismissed;
if let Err(e) = self.constellation_chan.send(msg) { if let Err(e) = self.constellation_proxy.try_send(msg) {
warn!( warn!(
"Sending IMEDismissed event to constellation failed ({:?}).", "Sending IMEDismissed event to constellation failed ({:?}).",
e e
@ -762,19 +779,19 @@ impl Servo {
}, },
EmbedderEvent::Quit => { EmbedderEvent::Quit => {
self.compositor.maybe_start_shutting_down(); self.compositor.borrow_mut().maybe_start_shutting_down();
}, },
EmbedderEvent::ExitFullScreen(top_level_browsing_context_id) => { EmbedderEvent::ExitFullScreen(top_level_browsing_context_id) => {
let msg = ConstellationMsg::ExitFullScreen(top_level_browsing_context_id); let msg = ConstellationMsg::ExitFullScreen(top_level_browsing_context_id);
if let Err(e) = self.constellation_chan.send(msg) { if let Err(e) = self.constellation_proxy.try_send(msg) {
warn!("Sending exit fullscreen to constellation failed ({:?}).", e); warn!("Sending exit fullscreen to constellation failed ({:?}).", e);
} }
}, },
EmbedderEvent::Reload(top_level_browsing_context_id) => { EmbedderEvent::Reload(top_level_browsing_context_id) => {
let msg = ConstellationMsg::Reload(top_level_browsing_context_id); let msg = ConstellationMsg::Reload(top_level_browsing_context_id);
if let Err(e) = self.constellation_chan.send(msg) { if let Err(e) = self.constellation_proxy.try_send(msg) {
warn!("Sending reload to constellation failed ({:?}).", e); warn!("Sending reload to constellation failed ({:?}).", e);
} }
}, },
@ -786,22 +803,22 @@ impl Servo {
} else { } else {
ConstellationMsg::DisableProfiler ConstellationMsg::DisableProfiler
}; };
if let Err(e) = self.constellation_chan.send(msg) { if let Err(e) = self.constellation_proxy.try_send(msg) {
warn!("Sending profiler toggle to constellation failed ({:?}).", e); warn!("Sending profiler toggle to constellation failed ({:?}).", e);
} }
}, },
EmbedderEvent::ToggleWebRenderDebug(option) => { EmbedderEvent::ToggleWebRenderDebug(option) => {
self.compositor.toggle_webrender_debug(option); self.compositor.borrow_mut().toggle_webrender_debug(option);
}, },
EmbedderEvent::CaptureWebRender => { EmbedderEvent::CaptureWebRender => {
self.compositor.capture_webrender(); self.compositor.borrow_mut().capture_webrender();
}, },
EmbedderEvent::NewWebView(url, top_level_browsing_context_id) => { EmbedderEvent::NewWebView(url, top_level_browsing_context_id) => {
let msg = ConstellationMsg::NewWebView(url, top_level_browsing_context_id); let msg = ConstellationMsg::NewWebView(url, top_level_browsing_context_id);
if let Err(e) = self.constellation_chan.send(msg) { if let Err(e) = self.constellation_proxy.try_send(msg) {
warn!( warn!(
"Sending NewBrowser message to constellation failed ({:?}).", "Sending NewBrowser message to constellation failed ({:?}).",
e e
@ -811,7 +828,7 @@ impl Servo {
EmbedderEvent::FocusWebView(top_level_browsing_context_id) => { EmbedderEvent::FocusWebView(top_level_browsing_context_id) => {
let msg = ConstellationMsg::FocusWebView(top_level_browsing_context_id); let msg = ConstellationMsg::FocusWebView(top_level_browsing_context_id);
if let Err(e) = self.constellation_chan.send(msg) { if let Err(e) = self.constellation_proxy.try_send(msg) {
warn!( warn!(
"Sending FocusBrowser message to constellation failed ({:?}).", "Sending FocusBrowser message to constellation failed ({:?}).",
e e
@ -821,7 +838,7 @@ impl Servo {
EmbedderEvent::CloseWebView(top_level_browsing_context_id) => { EmbedderEvent::CloseWebView(top_level_browsing_context_id) => {
let msg = ConstellationMsg::CloseWebView(top_level_browsing_context_id); let msg = ConstellationMsg::CloseWebView(top_level_browsing_context_id);
if let Err(e) = self.constellation_chan.send(msg) { if let Err(e) = self.constellation_proxy.try_send(msg) {
warn!( warn!(
"Sending CloseBrowser message to constellation failed ({:?}).", "Sending CloseBrowser message to constellation failed ({:?}).",
e e
@ -830,23 +847,30 @@ impl Servo {
}, },
EmbedderEvent::MoveResizeWebView(webview_id, rect) => { EmbedderEvent::MoveResizeWebView(webview_id, rect) => {
self.compositor.move_resize_webview(webview_id, rect); self.compositor
.borrow_mut()
.move_resize_webview(webview_id, rect);
}, },
EmbedderEvent::ShowWebView(webview_id, hide_others) => { EmbedderEvent::ShowWebView(webview_id, hide_others) => {
if let Err(UnknownWebView(webview_id)) = if let Err(UnknownWebView(webview_id)) = self
self.compositor.show_webview(webview_id, hide_others) .compositor
.borrow_mut()
.show_webview(webview_id, hide_others)
{ {
warn!("{webview_id}: ShowWebView on unknown webview id"); warn!("{webview_id}: ShowWebView on unknown webview id");
} }
}, },
EmbedderEvent::HideWebView(webview_id) => { EmbedderEvent::HideWebView(webview_id) => {
if let Err(UnknownWebView(webview_id)) = self.compositor.hide_webview(webview_id) { if let Err(UnknownWebView(webview_id)) =
self.compositor.borrow_mut().hide_webview(webview_id)
{
warn!("{webview_id}: HideWebView on unknown webview id"); warn!("{webview_id}: HideWebView on unknown webview id");
} }
}, },
EmbedderEvent::RaiseWebViewToTop(webview_id, hide_others) => { EmbedderEvent::RaiseWebViewToTop(webview_id, hide_others) => {
if let Err(UnknownWebView(webview_id)) = self if let Err(UnknownWebView(webview_id)) = self
.compositor .compositor
.borrow_mut()
.raise_webview_to_top(webview_id, hide_others) .raise_webview_to_top(webview_id, hide_others)
{ {
warn!("{webview_id}: RaiseWebViewToTop on unknown webview id"); warn!("{webview_id}: RaiseWebViewToTop on unknown webview id");
@ -858,7 +882,7 @@ impl Servo {
EmbedderEvent::SendError(top_level_browsing_context_id, e) => { EmbedderEvent::SendError(top_level_browsing_context_id, e) => {
let msg = ConstellationMsg::SendError(top_level_browsing_context_id, e); let msg = ConstellationMsg::SendError(top_level_browsing_context_id, e);
if let Err(e) = self.constellation_chan.send(msg) { if let Err(e) = self.constellation_proxy.try_send(msg) {
warn!( warn!(
"Sending SendError message to constellation failed ({:?}).", "Sending SendError message to constellation failed ({:?}).",
e e
@ -868,7 +892,7 @@ impl Servo {
EmbedderEvent::MediaSessionAction(a) => { EmbedderEvent::MediaSessionAction(a) => {
let msg = ConstellationMsg::MediaSessionAction(a); let msg = ConstellationMsg::MediaSessionAction(a);
if let Err(e) = self.constellation_chan.send(msg) { if let Err(e) = self.constellation_proxy.try_send(msg) {
warn!( warn!(
"Sending MediaSessionAction message to constellation failed ({:?}).", "Sending MediaSessionAction message to constellation failed ({:?}).",
e e
@ -878,7 +902,7 @@ impl Servo {
EmbedderEvent::SetWebViewThrottled(webview_id, throttled) => { EmbedderEvent::SetWebViewThrottled(webview_id, throttled) => {
let msg = ConstellationMsg::SetWebViewThrottled(webview_id, throttled); let msg = ConstellationMsg::SetWebViewThrottled(webview_id, throttled);
if let Err(e) = self.constellation_chan.send(msg) { if let Err(e) = self.constellation_proxy.try_send(msg) {
warn!( warn!(
"Sending SetWebViewThrottled to constellation failed ({:?}).", "Sending SetWebViewThrottled to constellation failed ({:?}).",
e e
@ -888,12 +912,12 @@ impl Servo {
EmbedderEvent::Gamepad(gamepad_event) => { EmbedderEvent::Gamepad(gamepad_event) => {
let msg = ConstellationMsg::Gamepad(gamepad_event); let msg = ConstellationMsg::Gamepad(gamepad_event);
if let Err(e) = self.constellation_chan.send(msg) { if let Err(e) = self.constellation_proxy.try_send(msg) {
warn!("Sending Gamepad event to constellation failed ({:?}).", e); warn!("Sending Gamepad event to constellation failed ({:?}).", e);
} }
}, },
EmbedderEvent::Vsync => { EmbedderEvent::Vsync => {
self.compositor.on_vsync(); self.compositor.borrow_mut().on_vsync();
}, },
EmbedderEvent::ClipboardAction(clipboard_event) => { EmbedderEvent::ClipboardAction(clipboard_event) => {
self.send_to_constellation(ConstellationMsg::Clipboard(clipboard_event)); self.send_to_constellation(ConstellationMsg::Clipboard(clipboard_event));
@ -904,7 +928,7 @@ impl Servo {
fn send_to_constellation(&self, msg: ConstellationMsg) { fn send_to_constellation(&self, msg: ConstellationMsg) {
let variant_name = msg.variant_name(); let variant_name = msg.variant_name();
if let Err(e) = self.constellation_chan.send(msg) { if let Err(e) = self.constellation_proxy.try_send(msg) {
warn!("Sending {variant_name} to constellation failed: {e:?}"); warn!("Sending {variant_name} to constellation failed: {e:?}");
} }
} }
@ -913,7 +937,7 @@ impl Servo {
while let Some((top_level_browsing_context, msg)) = while let Some((top_level_browsing_context, msg)) =
self.embedder_receiver.try_recv_embedder_msg() self.embedder_receiver.try_recv_embedder_msg()
{ {
match (msg, self.compositor.shutdown_state) { match (msg, self.compositor.borrow().shutdown_state) {
(_, ShutdownState::FinishedShuttingDown) => { (_, ShutdownState::FinishedShuttingDown) => {
error!( error!(
"embedder shouldn't be handling messages after compositor has shut down" "embedder shouldn't be handling messages after compositor has shut down"
@ -940,7 +964,7 @@ impl Servo {
} }
pub fn handle_events(&mut self, events: impl IntoIterator<Item = EmbedderEvent>) -> bool { pub fn handle_events(&mut self, events: impl IntoIterator<Item = EmbedderEvent>) -> bool {
if self.compositor.receive_messages() { if self.compositor.borrow_mut().receive_messages() {
self.receive_messages(); self.receive_messages();
} }
let mut need_resize = false; let mut need_resize = false;
@ -948,8 +972,8 @@ impl Servo {
trace!("servo <- embedder EmbedderEvent {:?}", event); trace!("servo <- embedder EmbedderEvent {:?}", event);
need_resize |= self.handle_window_event(event); need_resize |= self.handle_window_event(event);
} }
if self.compositor.shutdown_state != ShutdownState::FinishedShuttingDown { if self.compositor.borrow().shutdown_state != ShutdownState::FinishedShuttingDown {
self.compositor.perform_updates(); self.compositor.borrow_mut().perform_updates();
} else { } else {
self.messages_for_embedder self.messages_for_embedder
.push((None, EmbedderMsg::Shutdown)); .push((None, EmbedderMsg::Shutdown));
@ -958,15 +982,15 @@ impl Servo {
} }
pub fn repaint_synchronously(&mut self) { pub fn repaint_synchronously(&mut self) {
self.compositor.repaint_synchronously() self.compositor.borrow_mut().repaint_synchronously()
} }
pub fn pinch_zoom_level(&self) -> f32 { pub fn pinch_zoom_level(&self) -> f32 {
self.compositor.pinch_zoom_level().get() self.compositor.borrow_mut().pinch_zoom_level().get()
} }
pub fn setup_logging(&self) { pub fn setup_logging(&self) {
let constellation_chan = self.constellation_chan.clone(); let constellation_chan = self.constellation_proxy.sender();
let env = env_logger::Env::default(); let env = env_logger::Env::default();
let env_logger = EnvLoggerBuilder::from_env(env).build(); let env_logger = EnvLoggerBuilder::from_env(env).build();
let con_logger = FromCompositorLogger::new(constellation_chan); let con_logger = FromCompositorLogger::new(constellation_chan);
@ -978,22 +1002,27 @@ impl Servo {
log::set_max_level(filter); log::set_max_level(filter);
} }
pub fn window(&self) -> &Rc<dyn WindowMethods> {
&self.compositor.window
}
pub fn deinit(self) { pub fn deinit(self) {
self.compositor.deinit(); self.compositor.borrow_mut().deinit();
} }
pub fn present(&mut self) { pub fn present(&mut self) {
self.compositor.present(); self.compositor.borrow_mut().present();
} }
/// Return the OpenGL framebuffer name of the most-recently-completed frame when compositing to /// Return the OpenGL framebuffer name of the most-recently-completed frame when compositing to
/// [`CompositeTarget::Fbo`], or None otherwise. /// [`CompositeTarget::Fbo`], or None otherwise.
pub fn offscreen_framebuffer_id(&self) -> Option<u32> { pub fn offscreen_framebuffer_id(&self) -> Option<u32> {
self.compositor.offscreen_framebuffer_id() self.compositor.borrow().offscreen_framebuffer_id()
}
pub fn new_webview(&self, url: url::Url) -> WebView {
WebView::new(&self.constellation_proxy, self.compositor.clone(), url)
}
/// FIXME: Remove this once we have a webview delegate.
pub fn new_auxiliary_webview(&self) -> WebView {
WebView::new_auxiliary(&self.constellation_proxy, self.compositor.clone())
} }
} }

View file

@ -0,0 +1,47 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* 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::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use compositing_traits::ConstellationMsg;
use crossbeam_channel::{SendError, Sender};
use log::warn;
#[derive(Clone)]
pub(crate) struct ConstellationProxy {
sender: Sender<ConstellationMsg>,
disconnected: Arc<AtomicBool>,
}
impl ConstellationProxy {
pub fn new(sender: Sender<ConstellationMsg>) -> Self {
Self {
sender,
disconnected: Arc::default(),
}
}
pub fn send(&self, msg: ConstellationMsg) {
if self.try_send(msg).is_err() {
warn!("Lost connection to Constellation. Will report to embedder.")
}
}
pub fn try_send(&self, msg: ConstellationMsg) -> Result<(), SendError<ConstellationMsg>> {
if self.disconnected.load(Ordering::SeqCst) {
return Err(SendError(msg));
}
if let Err(error) = self.sender.send(msg) {
self.disconnected.store(true, Ordering::SeqCst);
return Err(error);
}
Ok(())
}
pub fn sender(&self) -> Sender<ConstellationMsg> {
self.sender.clone()
}
}

117
components/servo/webview.rs Normal file
View file

@ -0,0 +1,117 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* 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::cell::RefCell;
use std::rc::Rc;
use base::id::WebViewId;
use compositing::IOCompositor;
use compositing_traits::ConstellationMsg;
use webrender_api::units::DeviceRect;
use crate::ConstellationProxy;
pub struct WebView(Rc<WebViewInner>);
struct WebViewInner {
// TODO: ensure that WebView instances interact with the correct Servo instance
pub(crate) id: WebViewId,
pub(crate) constellation_proxy: ConstellationProxy,
pub(crate) compositor: Rc<RefCell<IOCompositor>>,
}
impl Drop for WebViewInner {
fn drop(&mut self) {
self.constellation_proxy
.send(ConstellationMsg::CloseWebView(self.id));
}
}
/// Handle for a webview.
///
/// - The webview exists for exactly as long as there are WebView handles
/// (FIXME: this is not true yet; webviews can still close of their own volition)
/// - All methods are infallible; if the constellation dies, the embedder finds out when calling
/// [Servo::handle_events](crate::Servo::handle_events)
impl WebView {
pub(crate) fn new(
constellation_proxy: &ConstellationProxy,
compositor: Rc<RefCell<IOCompositor>>,
url: url::Url,
) -> Self {
let webview_id = WebViewId::new();
constellation_proxy.send(ConstellationMsg::NewWebView(url.into(), webview_id));
Self(Rc::new(WebViewInner {
id: webview_id,
constellation_proxy: constellation_proxy.clone(),
compositor,
}))
}
/// FIXME: Remove this once we have a webview delegate.
pub(crate) fn new_auxiliary(
constellation_proxy: &ConstellationProxy,
compositor: Rc<RefCell<IOCompositor>>,
) -> Self {
let webview_id = WebViewId::new();
Self(
WebViewInner {
id: webview_id,
constellation_proxy: constellation_proxy.clone(),
compositor,
}
.into(),
)
}
/// FIXME: Remove this once we have a webview delegate.
pub fn id(&self) -> WebViewId {
self.0.id
}
pub fn focus(&self) {
self.0
.constellation_proxy
.send(ConstellationMsg::FocusWebView(self.id()));
}
pub fn blur(&self) {
self.0
.constellation_proxy
.send(ConstellationMsg::BlurWebView);
}
pub fn move_resize(&self, rect: DeviceRect) {
self.0
.compositor
.borrow_mut()
.move_resize_webview(self.id(), rect);
}
pub fn show(&self, hide_others: bool) {
self.0
.compositor
.borrow_mut()
.show_webview(self.id(), hide_others)
.expect("BUG: invalid WebView instance");
}
pub fn hide(&self) {
self.0
.compositor
.borrow_mut()
.hide_webview(self.id())
.expect("BUG: invalid WebView instance");
}
pub fn raise_to_top(&self, hide_others: bool) {
self.0
.compositor
.borrow_mut()
.raise_webview_to_top(self.id(), hide_others)
.expect("BUG: invalid WebView instance");
}
}

View file

@ -181,7 +181,7 @@ pub enum EmbedderMsg {
/// Whether or not to allow a pipeline to load a url. /// Whether or not to allow a pipeline to load a url.
AllowNavigationRequest(PipelineId, ServoUrl), AllowNavigationRequest(PipelineId, ServoUrl),
/// Whether or not to allow script to open a new tab/browser /// Whether or not to allow script to open a new tab/browser
AllowOpeningWebView(IpcSender<bool>), AllowOpeningWebView(IpcSender<Option<WebViewId>>),
/// A webview was created. /// A webview was created.
WebViewOpened(TopLevelBrowsingContextId), WebViewOpened(TopLevelBrowsingContextId),
/// A webview was destroyed. /// A webview was destroyed.

View file

@ -19,7 +19,7 @@ use servo::config::opts::Opts;
use servo::config::prefs::Preferences; use servo::config::prefs::Preferences;
use servo::embedder_traits::EventLoopWaker; use servo::embedder_traits::EventLoopWaker;
use servo::servo_config::pref; use servo::servo_config::pref;
use servo::url::ServoUrl; use servo::servo_url::ServoUrl;
use servo::webrender_traits::SurfmanRenderingContext; use servo::webrender_traits::SurfmanRenderingContext;
use servo::Servo; use servo::Servo;
use surfman::Connection; use surfman::Connection;

View file

@ -809,7 +809,7 @@ where
EmbedderMsg::AllowOpeningWebView(response_chan) => { EmbedderMsg::AllowOpeningWebView(response_chan) => {
// Note: would be a place to handle pop-ups config. // 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 // see Step 7 of #the-rules-for-choosing-a-browsing-context-given-a-browsing-context-name
if let Err(e) = response_chan.send(true) { if let Err(e) = response_chan.send(Some(WebViewId::new())) {
warn!("Failed to send AllowOpeningWebView response: {}", e); warn!("Failed to send AllowOpeningWebView response: {}", e);
}; };
}, },

View file

@ -526,7 +526,7 @@ impl ServoGlue {
EmbedderMsg::AllowOpeningWebView(response_chan) => { EmbedderMsg::AllowOpeningWebView(response_chan) => {
// Note: would be a place to handle pop-ups config. // 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 // see Step 7 of #the-rules-for-choosing-a-browsing-context-given-a-browsing-context-name
if let Err(e) = response_chan.send(true) { if let Err(e) = response_chan.send(Some(WebViewId::new())) {
warn!("Failed to send AllowOpeningBrowser response: {}", e); warn!("Failed to send AllowOpeningBrowser response: {}", e);
}; };
}, },

View file

@ -14,7 +14,7 @@ use log::{error, warn};
use serde_json::Value; use serde_json::Value;
use servo::config::opts::{DebugOptions, Opts, OutputOptions}; use servo::config::opts::{DebugOptions, Opts, OutputOptions};
use servo::config::prefs::{PrefValue, Preferences}; use servo::config::prefs::{PrefValue, Preferences};
use servo::url::ServoUrl; use servo::servo_url::ServoUrl;
use url::Url; use url::Url;
#[cfg_attr(any(target_os = "android", target_env = "ohos"), allow(dead_code))] #[cfg_attr(any(target_os = "android", target_env = "ohos"), allow(dead_code))]