| /* 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::borrow::ToOwned; |
| use std::cell::{Cell, RefCell, RefMut}; |
| use std::cmp; |
| use std::collections::hash_map::Entry; |
| use std::collections::{HashMap, HashSet}; |
| use std::default::Default; |
| use std::ffi::c_void; |
| use std::io::{Write, stderr, stdout}; |
| use std::rc::Rc; |
| use std::sync::Arc; |
| use std::time::{Duration, Instant}; |
| |
| use app_units::Au; |
| use backtrace::Backtrace; |
| use base::cross_process_instant::CrossProcessInstant; |
| use base::generic_channel; |
| use base::generic_channel::GenericSender; |
| use base::id::{BrowsingContextId, PipelineId, WebViewId}; |
| use base64::Engine; |
| #[cfg(feature = "bluetooth")] |
| use bluetooth_traits::BluetoothRequest; |
| use canvas_traits::webgl::WebGLChan; |
| use compositing_traits::CrossProcessCompositorApi; |
| use constellation_traits::{ |
| DocumentState, LoadData, LoadOrigin, NavigationHistoryBehavior, ScriptToConstellationChan, |
| ScriptToConstellationMessage, StructuredSerializedData, WindowSizeType, |
| }; |
| use crossbeam_channel::{Sender, unbounded}; |
| use cssparser::SourceLocation; |
| use devtools_traits::{ScriptToDevtoolsControlMsg, TimelineMarker, TimelineMarkerType}; |
| use dom_struct::dom_struct; |
| use embedder_traits::user_content_manager::{UserContentManager, UserScript}; |
| use embedder_traits::{ |
| AlertResponse, ConfirmResponse, EmbedderMsg, PromptResponse, ScriptToEmbedderChan, |
| SimpleDialog, Theme, UntrustedNodeAddress, ViewportDetails, WebDriverJSError, |
| WebDriverJSResult, WebDriverLoadStatus, |
| }; |
| use euclid::default::{Point2D as UntypedPoint2D, Rect as UntypedRect, Size2D as UntypedSize2D}; |
| use euclid::{Point2D, Scale, Size2D, Vector2D}; |
| use fonts::FontContext; |
| use ipc_channel::ipc::{self, IpcSender}; |
| use js::glue::DumpJSStack; |
| use js::jsapi::{ |
| GCReason, Heap, JS_GC, JSAutoRealm, JSContext as RawJSContext, JSObject, JSPROP_ENUMERATE, |
| }; |
| use js::jsval::{NullValue, UndefinedValue}; |
| use js::rust::wrappers::JS_DefineProperty; |
| use js::rust::{ |
| CustomAutoRooter, CustomAutoRooterGuard, HandleObject, HandleValue, MutableHandleObject, |
| MutableHandleValue, |
| }; |
| use layout_api::{ |
| BoxAreaType, ElementsFromPointFlags, ElementsFromPointResult, FragmentType, Layout, |
| PendingImage, PendingImageState, PendingRasterizationImage, QueryMsg, ReflowGoal, |
| ReflowPhasesRun, ReflowRequest, ReflowRequestRestyle, RestyleReason, ScrollContainerQueryType, |
| ScrollContainerResponse, TrustedNodeAddress, combine_id_with_fragment_type, |
| }; |
| use malloc_size_of::MallocSizeOf; |
| use media::WindowGLContext; |
| use net_traits::ResourceThreads; |
| use net_traits::image_cache::{ |
| ImageCache, ImageCacheResponseMessage, ImageLoadListener, ImageResponse, PendingImageId, |
| PendingImageResponse, RasterizationCompleteResponse, |
| }; |
| use net_traits::storage_thread::StorageType; |
| use num_traits::ToPrimitive; |
| use profile_traits::generic_channel as ProfiledGenericChannel; |
| use profile_traits::mem::ProfilerChan as MemProfilerChan; |
| use profile_traits::time::ProfilerChan as TimeProfilerChan; |
| use script_bindings::codegen::GenericBindings::WindowBinding::ScrollToOptions; |
| use script_bindings::conversions::SafeToJSValConvertible; |
| use script_bindings::interfaces::WindowHelpers; |
| use script_bindings::root::Root; |
| use script_traits::{ConstellationInputEvent, ScriptThreadMessage}; |
| use selectors::attr::CaseSensitivity; |
| use servo_arc::Arc as ServoArc; |
| use servo_config::{opts, pref}; |
| use servo_geometry::{DeviceIndependentIntRect, f32_rect_to_au_rect}; |
| use servo_url::{ImmutableOrigin, MutableOrigin, ServoUrl}; |
| use style::error_reporting::{ContextualParseError, ParseErrorReporter}; |
| use style::properties::PropertyId; |
| use style::properties::style_structs::Font; |
| use style::selector_parser::PseudoElement; |
| use style::str::HTML_SPACE_CHARACTERS; |
| use style::stylesheets::UrlExtraData; |
| use style_traits::CSSPixel; |
| use stylo_atoms::Atom; |
| use url::Position; |
| use webrender_api::ExternalScrollId; |
| use webrender_api::units::{DeviceIntSize, DevicePixel, LayoutPixel, LayoutPoint}; |
| |
| use super::bindings::codegen::Bindings::MessagePortBinding::StructuredSerializeOptions; |
| use super::bindings::trace::HashMapTracedValues; |
| use super::types::SVGSVGElement; |
| use crate::dom::bindings::cell::{DomRefCell, Ref}; |
| use crate::dom::bindings::codegen::Bindings::DocumentBinding::{ |
| DocumentMethods, DocumentReadyState, NamedPropertyValue, |
| }; |
| use crate::dom::bindings::codegen::Bindings::HTMLIFrameElementBinding::HTMLIFrameElementMethods; |
| use crate::dom::bindings::codegen::Bindings::HistoryBinding::History_Binding::HistoryMethods; |
| use crate::dom::bindings::codegen::Bindings::ImageBitmapBinding::{ |
| ImageBitmapOptions, ImageBitmapSource, |
| }; |
| use crate::dom::bindings::codegen::Bindings::MediaQueryListBinding::MediaQueryList_Binding::MediaQueryListMethods; |
| use crate::dom::bindings::codegen::Bindings::ReportingObserverBinding::Report; |
| use crate::dom::bindings::codegen::Bindings::RequestBinding::RequestInit; |
| use crate::dom::bindings::codegen::Bindings::VoidFunctionBinding::VoidFunction; |
| use crate::dom::bindings::codegen::Bindings::WindowBinding::{ |
| self, FrameRequestCallback, ScrollBehavior, WindowMethods, WindowPostMessageOptions, |
| }; |
| use crate::dom::bindings::codegen::UnionTypes::{ |
| RequestOrUSVString, TrustedScriptOrString, TrustedScriptOrStringOrFunction, |
| }; |
| use crate::dom::bindings::error::{Error, ErrorResult, Fallible}; |
| use crate::dom::bindings::inheritance::{Castable, ElementTypeId, HTMLElementTypeId, NodeTypeId}; |
| use crate::dom::bindings::num::Finite; |
| use crate::dom::bindings::refcounted::Trusted; |
| use crate::dom::bindings::reflector::{DomGlobal, DomObject}; |
| use crate::dom::bindings::root::{Dom, DomRoot, MutNullableDom}; |
| use crate::dom::bindings::str::{DOMString, USVString}; |
| use crate::dom::bindings::structuredclone; |
| use crate::dom::bindings::trace::{CustomTraceable, JSTraceable, RootedTraceableBox}; |
| use crate::dom::bindings::utils::GlobalStaticData; |
| use crate::dom::bindings::weakref::DOMTracker; |
| #[cfg(feature = "bluetooth")] |
| use crate::dom::bluetooth::BluetoothExtraPermissionData; |
| use crate::dom::cookiestore::CookieStore; |
| use crate::dom::crypto::Crypto; |
| use crate::dom::cssstyledeclaration::{CSSModificationAccess, CSSStyleDeclaration, CSSStyleOwner}; |
| use crate::dom::customelementregistry::CustomElementRegistry; |
| use crate::dom::document::{AnimationFrameCallback, Document}; |
| use crate::dom::element::Element; |
| use crate::dom::event::{Event, EventBubbles, EventCancelable}; |
| use crate::dom::eventtarget::EventTarget; |
| use crate::dom::globalscope::GlobalScope; |
| use crate::dom::hashchangeevent::HashChangeEvent; |
| use crate::dom::history::History; |
| use crate::dom::html::htmlcollection::{CollectionFilter, HTMLCollection}; |
| use crate::dom::html::htmliframeelement::HTMLIFrameElement; |
| use crate::dom::idbfactory::IDBFactory; |
| use crate::dom::inputevent::HitTestResult; |
| use crate::dom::location::Location; |
| use crate::dom::medialist::MediaList; |
| use crate::dom::mediaquerylist::{MediaQueryList, MediaQueryListMatchState}; |
| use crate::dom::mediaquerylistevent::MediaQueryListEvent; |
| use crate::dom::messageevent::MessageEvent; |
| use crate::dom::navigator::Navigator; |
| use crate::dom::node::{Node, NodeDamage, NodeTraits, from_untrusted_node_address}; |
| use crate::dom::performance::Performance; |
| use crate::dom::promise::Promise; |
| use crate::dom::reportingendpoint::{ReportingEndpoint, SendReportsToEndpoints}; |
| use crate::dom::reportingobserver::ReportingObserver; |
| use crate::dom::screen::Screen; |
| use crate::dom::selection::Selection; |
| use crate::dom::shadowroot::ShadowRoot; |
| use crate::dom::storage::Storage; |
| #[cfg(feature = "bluetooth")] |
| use crate::dom::testrunner::TestRunner; |
| use crate::dom::trustedtypepolicyfactory::TrustedTypePolicyFactory; |
| use crate::dom::types::{ImageBitmap, UIEvent}; |
| use crate::dom::webgl::webglrenderingcontext::WebGLCommandSender; |
| #[cfg(feature = "webgpu")] |
| use crate::dom::webgpu::identityhub::IdentityHub; |
| use crate::dom::windowproxy::{WindowProxy, WindowProxyHandler}; |
| use crate::dom::worklet::Worklet; |
| use crate::dom::workletglobalscope::WorkletGlobalScopeType; |
| use crate::layout_image::fetch_image_for_layout; |
| use crate::messaging::{MainThreadScriptMsg, ScriptEventLoopReceiver, ScriptEventLoopSender}; |
| use crate::microtask::MicrotaskQueue; |
| use crate::realms::{InRealm, enter_realm}; |
| use crate::script_runtime::{CanGc, JSContext, Runtime}; |
| use crate::script_thread::ScriptThread; |
| use crate::timers::{IsInterval, TimerCallback}; |
| use crate::unminify::unminified_path; |
| use crate::webdriver_handlers::{find_node_by_unique_id_in_document, jsval_to_webdriver}; |
| use crate::{fetch, window_named_properties}; |
| |
| /// A callback to call when a response comes back from the `ImageCache`. |
| /// |
| /// This is wrapped in a struct so that we can implement `MallocSizeOf` |
| /// for this type. |
| #[derive(MallocSizeOf)] |
| pub struct PendingImageCallback( |
| #[ignore_malloc_size_of = "dyn Fn is currently impossible to measure"] |
| Box<dyn Fn(PendingImageResponse) + 'static>, |
| ); |
| |
| /// Current state of the window object |
| #[derive(Clone, Copy, Debug, JSTraceable, MallocSizeOf, PartialEq)] |
| enum WindowState { |
| Alive, |
| Zombie, // Pipeline is closed, but the window hasn't been GCed yet. |
| } |
| |
| /// How long we should wait before performing the initial reflow after `<body>` is parsed, |
| /// assuming that `<body>` take this long to parse. |
| const INITIAL_REFLOW_DELAY: Duration = Duration::from_millis(200); |
| |
| /// During loading and parsing, layouts are suppressed to avoid flashing incomplete page |
| /// contents. |
| /// |
| /// Exceptions: |
| /// - Parsing the body takes so long, that layouts are no longer suppressed in order |
| /// to show the user that the page is loading. |
| /// - Script triggers a layout query or scroll event in which case, we want to layout |
| /// but not display the contents. |
| /// |
| /// For more information see: <https://github.com/servo/servo/pull/6028>. |
| #[derive(Clone, Copy, MallocSizeOf)] |
| enum LayoutBlocker { |
| /// The first load event hasn't been fired and we have not started to parse the `<body>` yet. |
| WaitingForParse, |
| /// The body is being parsed the `<body>` starting at the `Instant` specified. |
| Parsing(Instant), |
| /// The body finished parsing and the `load` event has been fired or parsing took so |
| /// long, that we are going to do layout anyway. Note that subsequent changes to the body |
| /// can trigger parsing again, but the `Window` stays in this state. |
| FiredLoadEventOrParsingTimerExpired, |
| } |
| |
| impl LayoutBlocker { |
| fn layout_blocked(&self) -> bool { |
| !matches!(self, Self::FiredLoadEventOrParsingTimerExpired) |
| } |
| } |
| |
| /// An id used to cancel navigations; for now only used for planned form navigations. |
| /// Loosely based on <https://html.spec.whatwg.org/multipage/#ongoing-navigation>. |
| #[derive(Clone, Copy, Debug, Default, JSTraceable, MallocSizeOf, PartialEq)] |
| pub(crate) struct OngoingNavigation(u32); |
| |
| type PendingImageRasterizationKey = (PendingImageId, DeviceIntSize); |
| |
| #[dom_struct] |
| pub(crate) struct Window { |
| globalscope: GlobalScope, |
| /// The webview that contains this [`Window`]. |
| /// |
| /// This may not be the top-level [`Window`], in the case of frames. |
| #[no_trace] |
| webview_id: WebViewId, |
| script_chan: Sender<MainThreadScriptMsg>, |
| #[no_trace] |
| #[ignore_malloc_size_of = "TODO: Add MallocSizeOf support to layout"] |
| layout: RefCell<Box<dyn Layout>>, |
| navigator: MutNullableDom<Navigator>, |
| #[ignore_malloc_size_of = "Arc"] |
| #[no_trace] |
| image_cache: Arc<dyn ImageCache>, |
| #[no_trace] |
| image_cache_sender: IpcSender<ImageCacheResponseMessage>, |
| window_proxy: MutNullableDom<WindowProxy>, |
| document: MutNullableDom<Document>, |
| location: MutNullableDom<Location>, |
| history: MutNullableDom<History>, |
| indexeddb: MutNullableDom<IDBFactory>, |
| custom_element_registry: MutNullableDom<CustomElementRegistry>, |
| performance: MutNullableDom<Performance>, |
| #[no_trace] |
| navigation_start: Cell<CrossProcessInstant>, |
| screen: MutNullableDom<Screen>, |
| session_storage: MutNullableDom<Storage>, |
| local_storage: MutNullableDom<Storage>, |
| status: DomRefCell<DOMString>, |
| trusted_types: MutNullableDom<TrustedTypePolicyFactory>, |
| |
| /// The start of something resembling |
| /// <https://html.spec.whatwg.org/multipage/#ongoing-navigation> |
| ongoing_navigation: Cell<OngoingNavigation>, |
| |
| /// For sending timeline markers. Will be ignored if |
| /// no devtools server |
| #[no_trace] |
| devtools_markers: DomRefCell<HashSet<TimelineMarkerType>>, |
| #[no_trace] |
| devtools_marker_sender: DomRefCell<Option<IpcSender<Option<TimelineMarker>>>>, |
| |
| /// Most recent unhandled resize event, if any. |
| #[no_trace] |
| unhandled_resize_event: DomRefCell<Option<(ViewportDetails, WindowSizeType)>>, |
| |
| /// Platform theme. |
| #[no_trace] |
| theme: Cell<Theme>, |
| |
| /// Parent id associated with this page, if any. |
| #[no_trace] |
| parent_info: Option<PipelineId>, |
| |
| /// Global static data related to the DOM. |
| dom_static: GlobalStaticData, |
| |
| /// The JavaScript runtime. |
| #[ignore_malloc_size_of = "Rc<T> is hard"] |
| js_runtime: DomRefCell<Option<Rc<Runtime>>>, |
| |
| /// The [`ViewportDetails`] of this [`Window`]'s frame. |
| #[no_trace] |
| viewport_details: Cell<ViewportDetails>, |
| |
| /// A handle for communicating messages to the bluetooth thread. |
| #[no_trace] |
| #[cfg(feature = "bluetooth")] |
| bluetooth_thread: IpcSender<BluetoothRequest>, |
| |
| #[cfg(feature = "bluetooth")] |
| bluetooth_extra_permission_data: BluetoothExtraPermissionData, |
| |
| /// See the documentation for [`LayoutBlocker`]. Essentially, this flag prevents |
| /// layouts from happening before the first load event, apart from a few exceptional |
| /// cases. |
| #[no_trace] |
| layout_blocker: Cell<LayoutBlocker>, |
| |
| /// A channel for communicating results of async scripts back to the webdriver server |
| #[no_trace] |
| webdriver_script_chan: DomRefCell<Option<IpcSender<WebDriverJSResult>>>, |
| |
| /// A channel to notify webdriver if there is a navigation |
| #[no_trace] |
| webdriver_load_status_sender: RefCell<Option<GenericSender<WebDriverLoadStatus>>>, |
| |
| /// The current state of the window object |
| current_state: Cell<WindowState>, |
| |
| /// The current size of the viewport. This might change if the `WebView` or containing `<iframe>` |
| /// for this `Window` object change. |
| #[no_trace] |
| current_viewport_size: Cell<UntypedSize2D<Au>>, |
| |
| error_reporter: CSSErrorReporter, |
| |
| /// All the MediaQueryLists we need to update |
| media_query_lists: DOMTracker<MediaQueryList>, |
| |
| #[cfg(feature = "bluetooth")] |
| test_runner: MutNullableDom<TestRunner>, |
| |
| /// A handle for communicating messages to the WebGL thread, if available. |
| #[ignore_malloc_size_of = "channels are hard"] |
| #[no_trace] |
| webgl_chan: Option<WebGLChan>, |
| |
| #[ignore_malloc_size_of = "defined in webxr"] |
| #[no_trace] |
| #[cfg(feature = "webxr")] |
| webxr_registry: Option<webxr_api::Registry>, |
| |
| /// When an element triggers an image load or starts watching an image load from the |
| /// `ImageCache` it adds an entry to this list. When those loads are triggered from |
| /// layout, they also add an etry to [`Self::pending_layout_images`]. |
| #[no_trace] |
| pending_image_callbacks: DomRefCell<HashMap<PendingImageId, Vec<PendingImageCallback>>>, |
| |
| /// All of the elements that have an outstanding image request that was |
| /// initiated by layout during a reflow. They are stored in the [`ScriptThread`] |
| /// to ensure that the element can be marked dirty when the image data becomes |
| /// available at some point in the future. |
| pending_layout_images: DomRefCell<HashMapTracedValues<PendingImageId, Vec<Dom<Node>>>>, |
| |
| /// Vector images for which layout has intiated rasterization at a specific size |
| /// and whose results are not yet available. They are stored in the [`ScriptThread`] |
| /// so that the element can be marked dirty once the rasterization is completed. |
| pending_images_for_rasterization: |
| DomRefCell<HashMapTracedValues<PendingImageRasterizationKey, Vec<Dom<Node>>>>, |
| |
| /// Directory to store unminified css for this window if unminify-css |
| /// opt is enabled. |
| unminified_css_dir: DomRefCell<Option<String>>, |
| |
| /// Directory with stored unminified scripts |
| local_script_source: Option<String>, |
| |
| /// Worklets |
| test_worklet: MutNullableDom<Worklet>, |
| /// <https://drafts.css-houdini.org/css-paint-api-1/#paint-worklet> |
| paint_worklet: MutNullableDom<Worklet>, |
| |
| /// Flag to identify whether mutation observers are present(true)/absent(false) |
| exists_mut_observer: Cell<bool>, |
| |
| /// Cross-process access to the compositor. |
| #[ignore_malloc_size_of = "Wraps an IpcSender"] |
| #[no_trace] |
| compositor_api: CrossProcessCompositorApi, |
| |
| /// Indicate whether a SetDocumentStatus message has been sent after a reflow is complete. |
| /// It is used to avoid sending idle message more than once, which is unneccessary. |
| has_sent_idle_message: Cell<bool>, |
| |
| /// Unminify Css. |
| unminify_css: bool, |
| |
| /// User content manager |
| #[no_trace] |
| user_content_manager: UserContentManager, |
| |
| /// Window's GL context from application |
| #[ignore_malloc_size_of = "defined in script_thread"] |
| #[no_trace] |
| player_context: WindowGLContext, |
| |
| throttled: Cell<bool>, |
| |
| /// A shared marker for the validity of any cached layout values. A value of true |
| /// indicates that any such values remain valid; any new layout that invalidates |
| /// those values will cause the marker to be set to false. |
| #[ignore_malloc_size_of = "Rc is hard"] |
| layout_marker: DomRefCell<Rc<Cell<bool>>>, |
| |
| /// <https://dom.spec.whatwg.org/#window-current-event> |
| current_event: DomRefCell<Option<Dom<Event>>>, |
| |
| /// <https://w3c.github.io/reporting/#windoworworkerglobalscope-registered-reporting-observer-list> |
| reporting_observer_list: DomRefCell<Vec<DomRoot<ReportingObserver>>>, |
| |
| /// <https://w3c.github.io/reporting/#windoworworkerglobalscope-reports> |
| report_list: DomRefCell<Vec<Report>>, |
| |
| /// <https://w3c.github.io/reporting/#windoworworkerglobalscope-endpoints> |
| #[no_trace] |
| endpoints_list: DomRefCell<Vec<ReportingEndpoint>>, |
| } |
| |
| impl Window { |
| pub(crate) fn webview_id(&self) -> WebViewId { |
| self.webview_id |
| } |
| |
| pub(crate) fn as_global_scope(&self) -> &GlobalScope { |
| self.upcast::<GlobalScope>() |
| } |
| |
| pub(crate) fn layout(&self) -> Ref<'_, Box<dyn Layout>> { |
| self.layout.borrow() |
| } |
| |
| pub(crate) fn layout_mut(&self) -> RefMut<'_, Box<dyn Layout>> { |
| self.layout.borrow_mut() |
| } |
| |
| pub(crate) fn get_exists_mut_observer(&self) -> bool { |
| self.exists_mut_observer.get() |
| } |
| |
| pub(crate) fn set_exists_mut_observer(&self) { |
| self.exists_mut_observer.set(true); |
| } |
| |
| #[allow(unsafe_code)] |
| pub(crate) fn clear_js_runtime_for_script_deallocation(&self) { |
| self.as_global_scope() |
| .remove_web_messaging_and_dedicated_workers_infra(); |
| unsafe { |
| *self.js_runtime.borrow_for_script_deallocation() = None; |
| self.window_proxy.set(None); |
| self.current_state.set(WindowState::Zombie); |
| self.as_global_scope() |
| .task_manager() |
| .cancel_all_tasks_and_ignore_future_tasks(); |
| } |
| } |
| |
| /// A convenience method for |
| /// <https://html.spec.whatwg.org/multipage/#a-browsing-context-is-discarded> |
| pub(crate) fn discard_browsing_context(&self) { |
| let proxy = match self.window_proxy.get() { |
| Some(proxy) => proxy, |
| None => panic!("Discarding a BC from a window that has none"), |
| }; |
| proxy.discard_browsing_context(); |
| // Step 4 of https://html.spec.whatwg.org/multipage/#discard-a-document |
| // Other steps performed when the `PipelineExit` message |
| // is handled by the ScriptThread. |
| self.as_global_scope() |
| .task_manager() |
| .cancel_all_tasks_and_ignore_future_tasks(); |
| } |
| |
| /// Get a sender to the time profiler thread. |
| pub(crate) fn time_profiler_chan(&self) -> &TimeProfilerChan { |
| self.globalscope.time_profiler_chan() |
| } |
| |
| pub(crate) fn origin(&self) -> &MutableOrigin { |
| self.globalscope.origin() |
| } |
| |
| #[allow(unsafe_code)] |
| pub(crate) fn get_cx(&self) -> JSContext { |
| unsafe { JSContext::from_ptr(self.js_runtime.borrow().as_ref().unwrap().cx()) } |
| } |
| |
| pub(crate) fn get_js_runtime(&self) -> Ref<'_, Option<Rc<Runtime>>> { |
| self.js_runtime.borrow() |
| } |
| |
| pub(crate) fn main_thread_script_chan(&self) -> &Sender<MainThreadScriptMsg> { |
| &self.script_chan |
| } |
| |
| pub(crate) fn parent_info(&self) -> Option<PipelineId> { |
| self.parent_info |
| } |
| |
| pub(crate) fn new_script_pair(&self) -> (ScriptEventLoopSender, ScriptEventLoopReceiver) { |
| let (sender, receiver) = unbounded(); |
| ( |
| ScriptEventLoopSender::MainThread(sender), |
| ScriptEventLoopReceiver::MainThread(receiver), |
| ) |
| } |
| |
| pub(crate) fn event_loop_sender(&self) -> ScriptEventLoopSender { |
| ScriptEventLoopSender::MainThread(self.script_chan.clone()) |
| } |
| |
| pub(crate) fn image_cache(&self) -> Arc<dyn ImageCache> { |
| self.image_cache.clone() |
| } |
| |
| /// This can panic if it is called after the browsing context has been discarded |
| pub(crate) fn window_proxy(&self) -> DomRoot<WindowProxy> { |
| self.window_proxy.get().unwrap() |
| } |
| |
| pub(crate) fn append_reporting_observer(&self, reporting_observer: DomRoot<ReportingObserver>) { |
| self.reporting_observer_list |
| .borrow_mut() |
| .push(reporting_observer); |
| } |
| |
| pub(crate) fn remove_reporting_observer(&self, reporting_observer: &ReportingObserver) { |
| if let Some(index) = self |
| .reporting_observer_list |
| .borrow() |
| .iter() |
| .position(|observer| &**observer == reporting_observer) |
| { |
| self.reporting_observer_list.borrow_mut().remove(index); |
| } |
| } |
| |
| pub(crate) fn registered_reporting_observers(&self) -> Vec<DomRoot<ReportingObserver>> { |
| self.reporting_observer_list.borrow().clone() |
| } |
| |
| pub(crate) fn append_report(&self, report: Report) { |
| self.report_list.borrow_mut().push(report); |
| let trusted_window = Trusted::new(self); |
| self.upcast::<GlobalScope>() |
| .task_manager() |
| .dom_manipulation_task_source() |
| .queue(task!(send_to_reporting_endpoints: move || { |
| let window = trusted_window.root(); |
| let reports = std::mem::take(&mut *window.report_list.borrow_mut()); |
| window.upcast::<GlobalScope>().send_reports_to_endpoints( |
| reports, |
| window.endpoints_list.borrow().clone(), |
| ); |
| })); |
| } |
| |
| pub(crate) fn buffered_reports(&self) -> Vec<Report> { |
| self.report_list.borrow().clone() |
| } |
| |
| pub(crate) fn set_endpoints_list(&self, endpoints: Vec<ReportingEndpoint>) { |
| *self.endpoints_list.borrow_mut() = endpoints; |
| } |
| |
| /// Returns the window proxy if it has not been discarded. |
| /// <https://html.spec.whatwg.org/multipage/#a-browsing-context-is-discarded> |
| pub(crate) fn undiscarded_window_proxy(&self) -> Option<DomRoot<WindowProxy>> { |
| self.window_proxy.get().and_then(|window_proxy| { |
| if window_proxy.is_browsing_context_discarded() { |
| None |
| } else { |
| Some(window_proxy) |
| } |
| }) |
| } |
| |
| /// Returns the window proxy of the webview, which is the top-level ancestor browsing context. |
| /// <https://html.spec.whatwg.org/multipage/#top-level-browsing-context> |
| pub(crate) fn webview_window_proxy(&self) -> Option<DomRoot<WindowProxy>> { |
| self.undiscarded_window_proxy().and_then(|window_proxy| { |
| ScriptThread::find_window_proxy(window_proxy.webview_id().into()) |
| }) |
| } |
| |
| #[cfg(feature = "bluetooth")] |
| pub(crate) fn bluetooth_thread(&self) -> IpcSender<BluetoothRequest> { |
| self.bluetooth_thread.clone() |
| } |
| |
| #[cfg(feature = "bluetooth")] |
| pub(crate) fn bluetooth_extra_permission_data(&self) -> &BluetoothExtraPermissionData { |
| &self.bluetooth_extra_permission_data |
| } |
| |
| pub(crate) fn css_error_reporter(&self) -> Option<&dyn ParseErrorReporter> { |
| Some(&self.error_reporter) |
| } |
| |
| pub(crate) fn webgl_chan(&self) -> Option<WebGLCommandSender> { |
| self.webgl_chan |
| .as_ref() |
| .map(|chan| WebGLCommandSender::new(chan.clone())) |
| } |
| |
| #[cfg(feature = "webxr")] |
| pub(crate) fn webxr_registry(&self) -> Option<webxr_api::Registry> { |
| self.webxr_registry.clone() |
| } |
| |
| fn new_paint_worklet(&self, can_gc: CanGc) -> DomRoot<Worklet> { |
| debug!("Creating new paint worklet."); |
| Worklet::new(self, WorkletGlobalScopeType::Paint, can_gc) |
| } |
| |
| pub(crate) fn register_image_cache_listener( |
| &self, |
| id: PendingImageId, |
| callback: impl Fn(PendingImageResponse) + 'static, |
| ) -> IpcSender<ImageCacheResponseMessage> { |
| self.pending_image_callbacks |
| .borrow_mut() |
| .entry(id) |
| .or_default() |
| .push(PendingImageCallback(Box::new(callback))); |
| self.image_cache_sender.clone() |
| } |
| |
| fn pending_layout_image_notification(&self, response: PendingImageResponse) { |
| let mut images = self.pending_layout_images.borrow_mut(); |
| let nodes = images.entry(response.id); |
| let nodes = match nodes { |
| Entry::Occupied(nodes) => nodes, |
| Entry::Vacant(_) => return, |
| }; |
| if matches!(response.response, ImageResponse::Loaded(_, _)) { |
| for node in nodes.get() { |
| node.dirty(NodeDamage::Other); |
| } |
| } |
| match response.response { |
| ImageResponse::MetadataLoaded(_) => {}, |
| ImageResponse::Loaded(_, _) | |
| ImageResponse::PlaceholderLoaded(_, _) | |
| ImageResponse::None => { |
| nodes.remove(); |
| }, |
| } |
| } |
| |
| pub(crate) fn handle_image_rasterization_complete_notification( |
| &self, |
| response: RasterizationCompleteResponse, |
| ) { |
| let mut images = self.pending_images_for_rasterization.borrow_mut(); |
| let nodes = images.entry((response.image_id, response.requested_size)); |
| let nodes = match nodes { |
| Entry::Occupied(nodes) => nodes, |
| Entry::Vacant(_) => return, |
| }; |
| for node in nodes.get() { |
| node.dirty(NodeDamage::Other); |
| } |
| nodes.remove(); |
| } |
| |
| pub(crate) fn pending_image_notification(&self, response: PendingImageResponse) { |
| // We take the images here, in order to prevent maintaining a mutable borrow when |
| // image callbacks are called. These, in turn, can trigger garbage collection. |
| // Normally this shouldn't trigger more pending image notifications, but just in |
| // case we do not want to cause a double borrow here. |
| let mut images = std::mem::take(&mut *self.pending_image_callbacks.borrow_mut()); |
| let Entry::Occupied(callbacks) = images.entry(response.id) else { |
| let _ = std::mem::replace(&mut *self.pending_image_callbacks.borrow_mut(), images); |
| return; |
| }; |
| |
| for callback in callbacks.get() { |
| callback.0(response.clone()); |
| } |
| |
| match response.response { |
| ImageResponse::MetadataLoaded(_) => {}, |
| ImageResponse::Loaded(_, _) | |
| ImageResponse::PlaceholderLoaded(_, _) | |
| ImageResponse::None => { |
| callbacks.remove(); |
| }, |
| } |
| |
| let _ = std::mem::replace(&mut *self.pending_image_callbacks.borrow_mut(), images); |
| } |
| |
| pub(crate) fn compositor_api(&self) -> &CrossProcessCompositorApi { |
| &self.compositor_api |
| } |
| |
| pub(crate) fn userscripts(&self) -> &[UserScript] { |
| self.user_content_manager.scripts() |
| } |
| |
| pub(crate) fn get_player_context(&self) -> WindowGLContext { |
| self.player_context.clone() |
| } |
| |
| // see note at https://dom.spec.whatwg.org/#concept-event-dispatch step 2 |
| pub(crate) fn dispatch_event_with_target_override(&self, event: &Event, can_gc: CanGc) { |
| event.dispatch(self.upcast(), true, can_gc); |
| } |
| |
| pub(crate) fn font_context(&self) -> &Arc<FontContext> { |
| self.as_global_scope() |
| .font_context() |
| .expect("A `Window` should always have a `FontContext`") |
| } |
| |
| pub(crate) fn ongoing_navigation(&self) -> OngoingNavigation { |
| self.ongoing_navigation.get() |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#set-the-ongoing-navigation> |
| pub(crate) fn set_ongoing_navigation(&self) -> OngoingNavigation { |
| // Note: since this value, for now, is only used in a single `ScriptThread`, |
| // we just increment it (it is not a uuid), which implies not |
| // using a `newValue` variable. |
| let new_value = self.ongoing_navigation.get().0.wrapping_add(1); |
| |
| // 1. If navigable's ongoing navigation is equal to newValue, then return. |
| // Note: cannot happen in the way it is currently used. |
| |
| // TODO: 2. Inform the navigation API about aborting navigation given navigable. |
| |
| // 3. Set navigable's ongoing navigation to newValue. |
| self.ongoing_navigation.set(OngoingNavigation(new_value)); |
| |
| // Note: Return the ongoing navigation for the caller to use. |
| OngoingNavigation(new_value) |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#nav-stop> |
| fn stop_loading(&self, can_gc: CanGc) { |
| // 1. Let document be navigable's active document. |
| let doc = self.Document(); |
| |
| // 2. If document's unload counter is 0, |
| // and navigable's ongoing navigation is a navigation ID, |
| // then set the ongoing navigation for navigable to null. |
| // |
| // Note: since the concept of `navigable` is nascent in Servo, |
| // for now we do two things: |
| // - increment the `ongoing_navigation`(preventing planned form navigations). |
| // - Send a `AbortLoadUrl` message(in case the navigation |
| // already started at the constellation). |
| self.set_ongoing_navigation(); |
| |
| // 3. Abort a document and its descendants given document. |
| doc.abort(can_gc); |
| } |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#atob |
| pub(crate) fn base64_btoa(input: DOMString) -> Fallible<DOMString> { |
| // "The btoa() method must throw an InvalidCharacterError exception if |
| // the method's first argument contains any character whose code point |
| // is greater than U+00FF." |
| if input.chars().any(|c: char| c > '\u{FF}') { |
| Err(Error::InvalidCharacter) |
| } else { |
| // "Otherwise, the user agent must convert that argument to a |
| // sequence of octets whose nth octet is the eight-bit |
| // representation of the code point of the nth character of |
| // the argument," |
| let octets = input.chars().map(|c: char| c as u8).collect::<Vec<u8>>(); |
| |
| // "and then must apply the base64 algorithm to that sequence of |
| // octets, and return the result. [RFC4648]" |
| let config = |
| base64::engine::general_purpose::GeneralPurposeConfig::new().with_encode_padding(true); |
| let engine = base64::engine::GeneralPurpose::new(&base64::alphabet::STANDARD, config); |
| Ok(DOMString::from(engine.encode(octets))) |
| } |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#atob |
| pub(crate) fn base64_atob(input: DOMString) -> Fallible<DOMString> { |
| // "Remove all space characters from input." |
| fn is_html_space(c: char) -> bool { |
| HTML_SPACE_CHARACTERS.contains(&c) |
| } |
| let without_spaces = input |
| .chars() |
| .filter(|&c| !is_html_space(c)) |
| .collect::<String>(); |
| let mut input = &*without_spaces; |
| |
| // "If the length of input divides by 4 leaving no remainder, then: |
| // if input ends with one or two U+003D EQUALS SIGN (=) characters, |
| // remove them from input." |
| if input.len() % 4 == 0 { |
| if input.ends_with("==") { |
| input = &input[..input.len() - 2] |
| } else if input.ends_with('=') { |
| input = &input[..input.len() - 1] |
| } |
| } |
| |
| // "If the length of input divides by 4 leaving a remainder of 1, |
| // throw an InvalidCharacterError exception and abort these steps." |
| if input.len() % 4 == 1 { |
| return Err(Error::InvalidCharacter); |
| } |
| |
| // "If input contains a character that is not in the following list of |
| // characters and character ranges, throw an InvalidCharacterError |
| // exception and abort these steps: |
| // |
| // U+002B PLUS SIGN (+) |
| // U+002F SOLIDUS (/) |
| // Alphanumeric ASCII characters" |
| if input |
| .chars() |
| .any(|c| c != '+' && c != '/' && !c.is_alphanumeric()) |
| { |
| return Err(Error::InvalidCharacter); |
| } |
| |
| let config = base64::engine::general_purpose::GeneralPurposeConfig::new() |
| .with_decode_padding_mode(base64::engine::DecodePaddingMode::RequireNone) |
| .with_decode_allow_trailing_bits(true); |
| let engine = base64::engine::GeneralPurpose::new(&base64::alphabet::STANDARD, config); |
| |
| let data = engine.decode(input).map_err(|_| Error::InvalidCharacter)?; |
| Ok(data.iter().map(|&b| b as char).collect::<String>().into()) |
| } |
| |
| impl WindowMethods<crate::DomTypeHolder> for Window { |
| // https://html.spec.whatwg.org/multipage/#dom-alert |
| fn Alert_(&self) { |
| self.Alert(DOMString::new()); |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-alert |
| fn Alert(&self, s: DOMString) { |
| // Print to the console. |
| // Ensure that stderr doesn't trample through the alert() we use to |
| // communicate test results (see executorservo.py in wptrunner). |
| { |
| let stderr = stderr(); |
| let mut stderr = stderr.lock(); |
| let stdout = stdout(); |
| let mut stdout = stdout.lock(); |
| writeln!(&mut stdout, "\nALERT: {}", s).unwrap(); |
| stdout.flush().unwrap(); |
| stderr.flush().unwrap(); |
| } |
| let (sender, receiver) = |
| ProfiledGenericChannel::channel(self.global().time_profiler_chan().clone()).unwrap(); |
| let dialog = SimpleDialog::Alert { |
| message: s.to_string(), |
| response_sender: sender, |
| }; |
| let msg = EmbedderMsg::ShowSimpleDialog(self.webview_id(), dialog); |
| self.send_to_embedder(msg); |
| receiver.recv().unwrap_or_else(|_| { |
| // If the receiver is closed, we assume the dialog was cancelled. |
| debug!("Alert dialog was cancelled or failed to show."); |
| AlertResponse::Ok |
| }); |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-confirm |
| fn Confirm(&self, s: DOMString) -> bool { |
| let (sender, receiver) = |
| ProfiledGenericChannel::channel(self.global().time_profiler_chan().clone()).unwrap(); |
| let dialog = SimpleDialog::Confirm { |
| message: s.to_string(), |
| response_sender: sender, |
| }; |
| let msg = EmbedderMsg::ShowSimpleDialog(self.webview_id(), dialog); |
| self.send_to_embedder(msg); |
| match receiver.recv() { |
| Ok(ConfirmResponse::Ok) => true, |
| Ok(ConfirmResponse::Cancel) => false, |
| Err(_) => { |
| warn!("Confirm dialog was cancelled or failed to show."); |
| false |
| }, |
| } |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-prompt |
| fn Prompt(&self, message: DOMString, default: DOMString) -> Option<DOMString> { |
| let (sender, receiver) = |
| ProfiledGenericChannel::channel(self.global().time_profiler_chan().clone()).unwrap(); |
| let dialog = SimpleDialog::Prompt { |
| message: message.to_string(), |
| default: default.to_string(), |
| response_sender: sender, |
| }; |
| let msg = EmbedderMsg::ShowSimpleDialog(self.webview_id(), dialog); |
| self.send_to_embedder(msg); |
| match receiver.recv() { |
| Ok(PromptResponse::Ok(input)) => Some(input.into()), |
| Ok(PromptResponse::Cancel) => None, |
| Err(_) => { |
| warn!("Prompt dialog was cancelled or failed to show."); |
| None |
| }, |
| } |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-window-stop |
| fn Stop(&self, can_gc: CanGc) { |
| // 1. If this's navigable is null, then return. |
| // Note: Servo doesn't have a concept of navigable yet. |
| |
| // 2. Stop loading this's navigable. |
| self.stop_loading(can_gc); |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#dom-window-focus> |
| fn Focus(&self) { |
| // > 1. Let `current` be this `Window` object's browsing context. |
| // > |
| // > 2. If `current` is null, then return. |
| let current = match self.undiscarded_window_proxy() { |
| Some(proxy) => proxy, |
| None => return, |
| }; |
| |
| // > 3. Run the focusing steps with `current`. |
| current.focus(); |
| |
| // > 4. If current is a top-level browsing context, user agents are |
| // > encouraged to trigger some sort of notification to indicate to |
| // > the user that the page is attempting to gain focus. |
| // |
| // TODO: Step 4 |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-window-blur |
| fn Blur(&self) { |
| // > User agents are encouraged to ignore calls to this `blur()` method |
| // > entirely. |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-open |
| fn Open( |
| &self, |
| url: USVString, |
| target: DOMString, |
| features: DOMString, |
| can_gc: CanGc, |
| ) -> Fallible<Option<DomRoot<WindowProxy>>> { |
| self.window_proxy().open(url, target, features, can_gc) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-opener |
| fn GetOpener( |
| &self, |
| cx: JSContext, |
| in_realm_proof: InRealm, |
| mut retval: MutableHandleValue, |
| ) -> Fallible<()> { |
| // Step 1, Let current be this Window object's browsing context. |
| let current = match self.window_proxy.get() { |
| Some(proxy) => proxy, |
| // Step 2, If current is null, then return null. |
| None => { |
| retval.set(NullValue()); |
| return Ok(()); |
| }, |
| }; |
| // Still step 2, since the window's BC is the associated doc's BC, |
| // see https://html.spec.whatwg.org/multipage/#window-bc |
| // and a doc's BC is null if it has been discarded. |
| // see https://html.spec.whatwg.org/multipage/#concept-document-bc |
| if current.is_browsing_context_discarded() { |
| retval.set(NullValue()); |
| return Ok(()); |
| } |
| // Step 3 to 5. |
| current.opener(*cx, in_realm_proof, retval); |
| Ok(()) |
| } |
| |
| #[allow(unsafe_code)] |
| // https://html.spec.whatwg.org/multipage/#dom-opener |
| fn SetOpener(&self, cx: JSContext, value: HandleValue) -> ErrorResult { |
| // Step 1. |
| if value.is_null() { |
| if let Some(proxy) = self.window_proxy.get() { |
| proxy.disown(); |
| } |
| return Ok(()); |
| } |
| // Step 2. |
| let obj = self.reflector().get_jsobject(); |
| unsafe { |
| let result = |
| JS_DefineProperty(*cx, obj, c"opener".as_ptr(), value, JSPROP_ENUMERATE as u32); |
| |
| if result { Ok(()) } else { Err(Error::JSFailed) } |
| } |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-window-closed |
| fn Closed(&self) -> bool { |
| self.window_proxy |
| .get() |
| .map(|ref proxy| proxy.is_browsing_context_discarded() || proxy.is_closing()) |
| .unwrap_or(true) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-window-close |
| fn Close(&self) { |
| // Step 1, Let current be this Window object's browsing context. |
| // Step 2, If current is null or its is closing is true, then return. |
| let window_proxy = match self.window_proxy.get() { |
| Some(proxy) => proxy, |
| None => return, |
| }; |
| if window_proxy.is_closing() { |
| return; |
| } |
| // Note: check the length of the "session history", as opposed to the joint session history? |
| // see https://github.com/whatwg/html/issues/3734 |
| if let Ok(history_length) = self.History().GetLength() { |
| let is_auxiliary = window_proxy.is_auxiliary(); |
| |
| // https://html.spec.whatwg.org/multipage/#script-closable |
| let is_script_closable = (self.is_top_level() && history_length == 1) || |
| is_auxiliary || |
| pref!(dom_allow_scripts_to_close_windows); |
| |
| // TODO: rest of Step 3: |
| // Is the incumbent settings object's responsible browsing context familiar with current? |
| // Is the incumbent settings object's responsible browsing context allowed to navigate current? |
| if is_script_closable { |
| // Step 3.1, set current's is closing to true. |
| window_proxy.close(); |
| |
| // Step 3.2, queue a task on the DOM manipulation task source to close current. |
| let this = Trusted::new(self); |
| let task = task!(window_close_browsing_context: move || { |
| let window = this.root(); |
| let document = window.Document(); |
| // https://html.spec.whatwg.org/multipage/#closing-browsing-contexts |
| // Step 1, check if traversable is closing, was already done above. |
| // Steps 2 and 3, prompt to unload for all inclusive descendant navigables. |
| // TODO: We should be prompting for all inclusive descendant navigables, |
| // but we pass false here, which suggests we are not doing that. Why? |
| if document.prompt_to_unload(false, CanGc::note()) { |
| // Step 4, unload. |
| document.unload(false, CanGc::note()); |
| |
| // https://html.spec.whatwg.org/multipage/#a-browsing-context-is-discarded |
| // which calls into https://html.spec.whatwg.org/multipage/#discard-a-document. |
| window.discard_browsing_context(); |
| |
| window.send_to_constellation(ScriptToConstellationMessage::DiscardTopLevelBrowsingContext); |
| } |
| }); |
| self.as_global_scope() |
| .task_manager() |
| .dom_manipulation_task_source() |
| .queue(task); |
| } |
| } |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-document-2 |
| fn Document(&self) -> DomRoot<Document> { |
| self.document |
| .get() |
| .expect("Document accessed before initialization.") |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-history |
| fn History(&self) -> DomRoot<History> { |
| self.history.or_init(|| History::new(self, CanGc::note())) |
| } |
| |
| // https://w3c.github.io/IndexedDB/#factory-interface |
| fn IndexedDB(&self) -> DomRoot<IDBFactory> { |
| self.indexeddb.or_init(|| { |
| let global_scope = self.upcast::<GlobalScope>(); |
| IDBFactory::new(global_scope, CanGc::note()) |
| }) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-window-customelements |
| fn CustomElements(&self) -> DomRoot<CustomElementRegistry> { |
| self.custom_element_registry |
| .or_init(|| CustomElementRegistry::new(self, CanGc::note())) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-location |
| fn Location(&self) -> DomRoot<Location> { |
| self.location.or_init(|| Location::new(self, CanGc::note())) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-sessionstorage |
| fn SessionStorage(&self) -> DomRoot<Storage> { |
| self.session_storage |
| .or_init(|| Storage::new(self, StorageType::Session, CanGc::note())) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-localstorage |
| fn LocalStorage(&self) -> DomRoot<Storage> { |
| self.local_storage |
| .or_init(|| Storage::new(self, StorageType::Local, CanGc::note())) |
| } |
| |
| // https://cookiestore.spec.whatwg.org/#Window |
| fn CookieStore(&self, can_gc: CanGc) -> DomRoot<CookieStore> { |
| self.global().cookie_store(can_gc) |
| } |
| |
| // https://dvcs.w3.org/hg/webcrypto-api/raw-file/tip/spec/Overview.html#dfn-GlobalCrypto |
| fn Crypto(&self) -> DomRoot<Crypto> { |
| self.as_global_scope().crypto(CanGc::note()) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-frameelement |
| fn GetFrameElement(&self) -> Option<DomRoot<Element>> { |
| // Steps 1-3. |
| let window_proxy = self.window_proxy.get()?; |
| |
| // Step 4-5. |
| let container = window_proxy.frame_element()?; |
| |
| // Step 6. |
| let container_doc = container.owner_document(); |
| let current_doc = GlobalScope::current() |
| .expect("No current global object") |
| .as_window() |
| .Document(); |
| if !current_doc |
| .origin() |
| .same_origin_domain(container_doc.origin()) |
| { |
| return None; |
| } |
| // Step 7. |
| Some(DomRoot::from_ref(container)) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-navigator |
| fn Navigator(&self) -> DomRoot<Navigator> { |
| self.navigator |
| .or_init(|| Navigator::new(self, CanGc::note())) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-settimeout |
| fn SetTimeout( |
| &self, |
| _cx: JSContext, |
| callback: TrustedScriptOrStringOrFunction, |
| timeout: i32, |
| args: Vec<HandleValue>, |
| can_gc: CanGc, |
| ) -> Fallible<i32> { |
| let callback = match callback { |
| TrustedScriptOrStringOrFunction::String(i) => { |
| TimerCallback::StringTimerCallback(TrustedScriptOrString::String(i)) |
| }, |
| TrustedScriptOrStringOrFunction::TrustedScript(i) => { |
| TimerCallback::StringTimerCallback(TrustedScriptOrString::TrustedScript(i)) |
| }, |
| TrustedScriptOrStringOrFunction::Function(i) => TimerCallback::FunctionTimerCallback(i), |
| }; |
| self.as_global_scope().set_timeout_or_interval( |
| callback, |
| args, |
| Duration::from_millis(timeout.max(0) as u64), |
| IsInterval::NonInterval, |
| can_gc, |
| ) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-windowtimers-cleartimeout |
| fn ClearTimeout(&self, handle: i32) { |
| self.as_global_scope().clear_timeout_or_interval(handle); |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-windowtimers-setinterval |
| fn SetInterval( |
| &self, |
| _cx: JSContext, |
| callback: TrustedScriptOrStringOrFunction, |
| timeout: i32, |
| args: Vec<HandleValue>, |
| can_gc: CanGc, |
| ) -> Fallible<i32> { |
| let callback = match callback { |
| TrustedScriptOrStringOrFunction::String(i) => { |
| TimerCallback::StringTimerCallback(TrustedScriptOrString::String(i)) |
| }, |
| TrustedScriptOrStringOrFunction::TrustedScript(i) => { |
| TimerCallback::StringTimerCallback(TrustedScriptOrString::TrustedScript(i)) |
| }, |
| TrustedScriptOrStringOrFunction::Function(i) => TimerCallback::FunctionTimerCallback(i), |
| }; |
| self.as_global_scope().set_timeout_or_interval( |
| callback, |
| args, |
| Duration::from_millis(timeout.max(0) as u64), |
| IsInterval::Interval, |
| can_gc, |
| ) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-windowtimers-clearinterval |
| fn ClearInterval(&self, handle: i32) { |
| self.ClearTimeout(handle); |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-queuemicrotask |
| fn QueueMicrotask(&self, callback: Rc<VoidFunction>) { |
| self.as_global_scope().queue_function_as_microtask(callback); |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#dom-createimagebitmap> |
| fn CreateImageBitmap( |
| &self, |
| image: ImageBitmapSource, |
| options: &ImageBitmapOptions, |
| can_gc: CanGc, |
| ) -> Rc<Promise> { |
| ImageBitmap::create_image_bitmap( |
| self.as_global_scope(), |
| image, |
| 0, |
| 0, |
| None, |
| None, |
| options, |
| can_gc, |
| ) |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#dom-createimagebitmap> |
| fn CreateImageBitmap_( |
| &self, |
| image: ImageBitmapSource, |
| sx: i32, |
| sy: i32, |
| sw: i32, |
| sh: i32, |
| options: &ImageBitmapOptions, |
| can_gc: CanGc, |
| ) -> Rc<Promise> { |
| ImageBitmap::create_image_bitmap( |
| self.as_global_scope(), |
| image, |
| sx, |
| sy, |
| Some(sw), |
| Some(sh), |
| options, |
| can_gc, |
| ) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-window |
| fn Window(&self) -> DomRoot<WindowProxy> { |
| self.window_proxy() |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-self |
| fn Self_(&self) -> DomRoot<WindowProxy> { |
| self.window_proxy() |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-frames |
| fn Frames(&self) -> DomRoot<WindowProxy> { |
| self.window_proxy() |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#accessing-other-browsing-contexts |
| fn Length(&self) -> u32 { |
| self.Document().iframes().iter().count() as u32 |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-parent |
| fn GetParent(&self) -> Option<DomRoot<WindowProxy>> { |
| // Steps 1-3. |
| let window_proxy = self.undiscarded_window_proxy()?; |
| |
| // Step 4. |
| if let Some(parent) = window_proxy.parent() { |
| return Some(DomRoot::from_ref(parent)); |
| } |
| // Step 5. |
| Some(window_proxy) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-top |
| fn GetTop(&self) -> Option<DomRoot<WindowProxy>> { |
| // Steps 1-3. |
| let window_proxy = self.undiscarded_window_proxy()?; |
| |
| // Steps 4-5. |
| Some(DomRoot::from_ref(window_proxy.top())) |
| } |
| |
| // https://dvcs.w3.org/hg/webperf/raw-file/tip/specs/ |
| // NavigationTiming/Overview.html#sec-window.performance-attribute |
| fn Performance(&self) -> DomRoot<Performance> { |
| self.performance.or_init(|| { |
| Performance::new( |
| self.as_global_scope(), |
| self.navigation_start.get(), |
| CanGc::note(), |
| ) |
| }) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#globaleventhandlers |
| global_event_handlers!(); |
| |
| // https://html.spec.whatwg.org/multipage/#windoweventhandlers |
| window_event_handlers!(); |
| |
| // https://developer.mozilla.org/en-US/docs/Web/API/Window/screen |
| fn Screen(&self) -> DomRoot<Screen> { |
| self.screen.or_init(|| Screen::new(self, CanGc::note())) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-windowbase64-btoa |
| fn Btoa(&self, btoa: DOMString) -> Fallible<DOMString> { |
| base64_btoa(btoa) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-windowbase64-atob |
| fn Atob(&self, atob: DOMString) -> Fallible<DOMString> { |
| base64_atob(atob) |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#dom-window-requestanimationframe> |
| fn RequestAnimationFrame(&self, callback: Rc<FrameRequestCallback>) -> u32 { |
| self.Document() |
| .request_animation_frame(AnimationFrameCallback::FrameRequestCallback { callback }) |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#dom-window-cancelanimationframe> |
| fn CancelAnimationFrame(&self, ident: u32) { |
| let doc = self.Document(); |
| doc.cancel_animation_frame(ident); |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-window-postmessage |
| fn PostMessage( |
| &self, |
| cx: JSContext, |
| message: HandleValue, |
| target_origin: USVString, |
| transfer: CustomAutoRooterGuard<Vec<*mut JSObject>>, |
| ) -> ErrorResult { |
| let incumbent = GlobalScope::incumbent().expect("no incumbent global?"); |
| let source = incumbent.as_window(); |
| let source_origin = source.Document().origin().immutable().clone(); |
| |
| self.post_message_impl(&target_origin, source_origin, source, cx, message, transfer) |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#dom-messageport-postmessage> |
| fn PostMessage_( |
| &self, |
| cx: JSContext, |
| message: HandleValue, |
| options: RootedTraceableBox<WindowPostMessageOptions>, |
| ) -> ErrorResult { |
| let mut rooted = CustomAutoRooter::new( |
| options |
| .parent |
| .transfer |
| .iter() |
| .map(|js: &RootedTraceableBox<Heap<*mut JSObject>>| js.get()) |
| .collect(), |
| ); |
| let transfer = CustomAutoRooterGuard::new(*cx, &mut rooted); |
| |
| let incumbent = GlobalScope::incumbent().expect("no incumbent global?"); |
| let source = incumbent.as_window(); |
| |
| let source_origin = source.Document().origin().immutable().clone(); |
| |
| self.post_message_impl( |
| &options.targetOrigin, |
| source_origin, |
| source, |
| cx, |
| message, |
| transfer, |
| ) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-window-captureevents |
| fn CaptureEvents(&self) { |
| // This method intentionally does nothing |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-window-releaseevents |
| fn ReleaseEvents(&self) { |
| // This method intentionally does nothing |
| } |
| |
| // check-tidy: no specs after this line |
| fn Debug(&self, message: DOMString) { |
| debug!("{}", message); |
| } |
| |
| #[allow(unsafe_code)] |
| fn Gc(&self) { |
| unsafe { |
| JS_GC(*self.get_cx(), GCReason::API); |
| } |
| } |
| |
| #[allow(unsafe_code)] |
| fn Js_backtrace(&self) { |
| unsafe { |
| println!("Current JS stack:"); |
| dump_js_stack(*self.get_cx()); |
| let rust_stack = Backtrace::new(); |
| println!("Current Rust stack:\n{:?}", rust_stack); |
| } |
| } |
| |
| fn WebdriverCallback(&self, cx: JSContext, val: HandleValue, realm: InRealm, can_gc: CanGc) { |
| let rv = jsval_to_webdriver(cx, &self.globalscope, val, realm, can_gc); |
| let opt_chan = self.webdriver_script_chan.borrow_mut().take(); |
| if let Some(chan) = opt_chan { |
| let _ = chan.send(rv); |
| } |
| } |
| |
| fn WebdriverException(&self, cx: JSContext, val: HandleValue, realm: InRealm, can_gc: CanGc) { |
| let rv = jsval_to_webdriver(cx, &self.globalscope, val, realm, can_gc); |
| let opt_chan = self.webdriver_script_chan.borrow_mut().take(); |
| if let Some(chan) = opt_chan { |
| if let Ok(rv) = rv { |
| let _ = chan.send(Err(WebDriverJSError::JSException(rv))); |
| } else { |
| let _ = chan.send(rv); |
| } |
| } |
| } |
| |
| fn WebdriverTimeout(&self) { |
| let opt_chan = self.webdriver_script_chan.borrow_mut().take(); |
| if let Some(chan) = opt_chan { |
| let _ = chan.send(Err(WebDriverJSError::Timeout)); |
| } |
| } |
| |
| fn WebdriverElement(&self, id: DOMString) -> Option<DomRoot<Element>> { |
| find_node_by_unique_id_in_document(&self.Document(), id.into()).and_then(Root::downcast) |
| } |
| |
| fn WebdriverFrame(&self, browsing_context_id: DOMString) -> Option<DomRoot<WindowProxy>> { |
| self.Document() |
| .iframes() |
| .iter() |
| .find(|iframe| { |
| iframe |
| .browsing_context_id() |
| .as_ref() |
| .map(BrowsingContextId::to_string) == |
| Some(browsing_context_id.to_string()) |
| }) |
| .and_then(|iframe| iframe.GetContentWindow()) |
| } |
| |
| fn WebdriverWindow(&self, webview_id: DOMString) -> Option<DomRoot<WindowProxy>> { |
| let window_proxy = self.window_proxy.get()?; |
| |
| // Window must be top level browsing context. |
| if window_proxy.browsing_context_id() != window_proxy.webview_id() { |
| return None; |
| } |
| |
| if self.webview_id().to_string() == webview_id.str() { |
| Some(DomRoot::from_ref(&window_proxy)) |
| } else { |
| None |
| } |
| } |
| |
| fn WebdriverShadowRoot(&self, id: DOMString) -> Option<DomRoot<ShadowRoot>> { |
| find_node_by_unique_id_in_document(&self.Document(), id.into()).and_then(Root::downcast) |
| } |
| |
| // https://drafts.csswg.org/cssom/#dom-window-getcomputedstyle |
| fn GetComputedStyle( |
| &self, |
| element: &Element, |
| pseudo: Option<DOMString>, |
| ) -> DomRoot<CSSStyleDeclaration> { |
| // Step 2: Let obj be elt. |
| // We don't store CSSStyleOwner directly because it stores a `Dom` which must be |
| // rooted. This avoids the rooting the value temporarily. |
| let mut is_null = false; |
| |
| // Step 3: If pseudoElt is provided, is not the empty string, and starts with a colon, then: |
| // Step 3.1: Parse pseudoElt as a <pseudo-element-selector>, and let type be the result. |
| let pseudo = pseudo.map(|mut s| { |
| s.make_ascii_lowercase(); |
| s |
| }); |
| let pseudo = match pseudo { |
| Some(ref pseudo) if pseudo == ":before" || pseudo == "::before" => { |
| Some(PseudoElement::Before) |
| }, |
| Some(ref pseudo) if pseudo == ":after" || pseudo == "::after" => { |
| Some(PseudoElement::After) |
| }, |
| Some(ref pseudo) if pseudo == "::selection" => Some(PseudoElement::Selection), |
| Some(ref pseudo) if pseudo == "::marker" => Some(PseudoElement::Marker), |
| Some(ref pseudo) if pseudo.starts_with(':') => { |
| // Step 3.2: If type is failure, or is a ::slotted() or ::part() |
| // pseudo-element, let obj be null. |
| is_null = true; |
| None |
| }, |
| _ => None, |
| }; |
| |
| // Step 4. Let decls be an empty list of CSS declarations. |
| // Step 5: If obj is not null, and elt is connected, part of the flat tree, and |
| // its shadow-including root has a browsing context which either doesn’t have a |
| // browsing context container, or whose browsing context container is being |
| // rendered, set decls to a list of all longhand properties that are supported CSS |
| // properties, in lexicographical order, with the value being the resolved value |
| // computed for obj using the style rules associated with doc. Additionally, |
| // append to decls all the custom properties whose computed value for obj is not |
| // the guaranteed-invalid value. |
| // |
| // Note: The specification says to generate the list of declarations beforehand, yet |
| // also says the list should be alive. This is why we do not do step 4 and 5 here. |
| // See: https://github.com/w3c/csswg-drafts/issues/6144 |
| // |
| // Step 6: Return a live CSSStyleProperties object with the following properties: |
| CSSStyleDeclaration::new( |
| self, |
| if is_null { |
| CSSStyleOwner::Null |
| } else { |
| CSSStyleOwner::Element(Dom::from_ref(element)) |
| }, |
| pseudo, |
| CSSModificationAccess::Readonly, |
| CanGc::note(), |
| ) |
| } |
| |
| // https://drafts.csswg.org/cssom-view/#dom-window-innerheight |
| // TODO Include Scrollbar |
| fn InnerHeight(&self) -> i32 { |
| self.viewport_details |
| .get() |
| .size |
| .height |
| .to_i32() |
| .unwrap_or(0) |
| } |
| |
| // https://drafts.csswg.org/cssom-view/#dom-window-innerwidth |
| // TODO Include Scrollbar |
| fn InnerWidth(&self) -> i32 { |
| self.viewport_details.get().size.width.to_i32().unwrap_or(0) |
| } |
| |
| /// <https://drafts.csswg.org/cssom-view/#dom-window-scrollx> |
| fn ScrollX(&self) -> i32 { |
| self.scroll_offset().x as i32 |
| } |
| |
| // https://drafts.csswg.org/cssom-view/#dom-window-pagexoffset |
| fn PageXOffset(&self) -> i32 { |
| self.ScrollX() |
| } |
| |
| /// <https://drafts.csswg.org/cssom-view/#dom-window-scrolly> |
| fn ScrollY(&self) -> i32 { |
| self.scroll_offset().y as i32 |
| } |
| |
| // https://drafts.csswg.org/cssom-view/#dom-window-pageyoffset |
| fn PageYOffset(&self) -> i32 { |
| self.ScrollY() |
| } |
| |
| /// <https://drafts.csswg.org/cssom-view/#dom-window-scroll> |
| fn Scroll(&self, options: &ScrollToOptions) { |
| // Step 1: If invoked with one argument, follow these substeps: |
| // Step 1.1: Let options be the argument. |
| // Step 1.2: Let x be the value of the left dictionary member of options, if |
| // present, or the viewport’s current scroll position on the x axis otherwise. |
| let x = options.left.unwrap_or(0.0) as f32; |
| |
| // Step 1.3: Let y be the value of the top dictionary member of options, if |
| // present, or the viewport’s current scroll position on the y axis otherwise. |
| let y = options.top.unwrap_or(0.0) as f32; |
| |
| // The rest of the specification continues from `Self::scroll`. |
| self.scroll(x, y, options.parent.behavior); |
| } |
| |
| /// <https://drafts.csswg.org/cssom-view/#dom-window-scroll> |
| fn Scroll_(&self, x: f64, y: f64) { |
| // Step 2: If invoked with two arguments, follow these substeps: |
| // Step 2.1 Let options be null converted to a ScrollToOptions dictionary. [WEBIDL] |
| // Step 2.2: Let x and y be the arguments, respectively. |
| self.scroll(x as f32, y as f32, ScrollBehavior::Auto); |
| } |
| |
| /// <https://drafts.csswg.org/cssom-view/#dom-window-scrollto> |
| /// |
| /// > When the scrollTo() method is invoked, the user agent must act as if the |
| /// > scroll() method was invoked with the same arguments. |
| fn ScrollTo(&self, options: &ScrollToOptions) { |
| self.Scroll(options); |
| } |
| |
| /// <https://drafts.csswg.org/cssom-view/#dom-window-scrollto>: |
| /// |
| /// > When the scrollTo() method is invoked, the user agent must act as if the |
| /// > scroll() method was invoked with the same arguments. |
| fn ScrollTo_(&self, x: f64, y: f64) { |
| self.Scroll_(x, y) |
| } |
| |
| /// <https://drafts.csswg.org/cssom-view/#dom-window-scrollby> |
| fn ScrollBy(&self, options: &ScrollToOptions) { |
| // When the scrollBy() method is invoked, the user agent must run these steps: |
| // Step 1: If invoked with two arguments, follow these substeps: |
| // This doesn't apply here. |
| |
| // Step 2: Normalize non-finite values for the left and top dictionary members of options. |
| let mut options = options.clone(); |
| let x = options.left.unwrap_or(0.0); |
| let x = if x.is_finite() { x } else { 0.0 }; |
| let y = options.top.unwrap_or(0.0); |
| let y = if y.is_finite() { y } else { 0.0 }; |
| |
| // Step 3: Add the value of scrollX to the left dictionary member. |
| options.left.replace(x + self.ScrollX() as f64); |
| |
| // Step 4. Add the value of scrollY to the top dictionary member. |
| options.top.replace(y + self.ScrollY() as f64); |
| |
| // Step 5: Act as if the scroll() method was invoked with options as the only argument. |
| self.Scroll(&options) |
| } |
| |
| /// <https://drafts.csswg.org/cssom-view/#dom-window-scrollby> |
| fn ScrollBy_(&self, x: f64, y: f64) { |
| // When the scrollBy() method is invoked, the user agent must run these steps: |
| // Step 1: If invoked with two arguments, follow these substeps: |
| // Step 1.1: Let options be null converted to a ScrollToOptions dictionary. |
| let mut options = ScrollToOptions::empty(); |
| |
| // Step 1.2: Let x and y be the arguments, respectively. |
| // Step 1.3: Let the left dictionary member of options have the value x. |
| options.left.replace(x); |
| |
| // Step 1.5: Let the top dictionary member of options have the value y. |
| options.top.replace(y); |
| |
| // Now follow the specification for the one argument option. |
| self.ScrollBy(&options); |
| } |
| |
| // https://drafts.csswg.org/cssom-view/#dom-window-resizeto |
| fn ResizeTo(&self, width: i32, height: i32) { |
| // Step 1 |
| let window_proxy = match self.window_proxy.get() { |
| Some(proxy) => proxy, |
| None => return, |
| }; |
| |
| // If target is not an auxiliary browsing context that was created by a script |
| // (as opposed to by an action of the user), then return. |
| if !window_proxy.is_auxiliary() { |
| return; |
| } |
| |
| let dpr = self.device_pixel_ratio(); |
| let size = Size2D::new(width, height).to_f32() * dpr; |
| self.send_to_embedder(EmbedderMsg::ResizeTo(self.webview_id(), size.to_i32())); |
| } |
| |
| // https://drafts.csswg.org/cssom-view/#dom-window-resizeby |
| fn ResizeBy(&self, x: i32, y: i32) { |
| let size = self.client_window().size(); |
| // Step 1 |
| self.ResizeTo(x + size.width, y + size.height) |
| } |
| |
| // https://drafts.csswg.org/cssom-view/#dom-window-moveto |
| fn MoveTo(&self, x: i32, y: i32) { |
| // Step 1 |
| // TODO determine if this operation is allowed |
| let dpr = self.device_pixel_ratio(); |
| let point = Point2D::new(x, y).to_f32() * dpr; |
| let msg = EmbedderMsg::MoveTo(self.webview_id(), point.to_i32()); |
| self.send_to_embedder(msg); |
| } |
| |
| // https://drafts.csswg.org/cssom-view/#dom-window-moveby |
| fn MoveBy(&self, x: i32, y: i32) { |
| let origin = self.client_window().min; |
| // Step 1 |
| self.MoveTo(x + origin.x, y + origin.y) |
| } |
| |
| // https://drafts.csswg.org/cssom-view/#dom-window-screenx |
| fn ScreenX(&self) -> i32 { |
| self.client_window().min.x |
| } |
| |
| // https://drafts.csswg.org/cssom-view/#dom-window-screeny |
| fn ScreenY(&self) -> i32 { |
| self.client_window().min.y |
| } |
| |
| // https://drafts.csswg.org/cssom-view/#dom-window-outerheight |
| fn OuterHeight(&self) -> i32 { |
| self.client_window().height() |
| } |
| |
| // https://drafts.csswg.org/cssom-view/#dom-window-outerwidth |
| fn OuterWidth(&self) -> i32 { |
| self.client_window().width() |
| } |
| |
| // https://drafts.csswg.org/cssom-view/#dom-window-devicepixelratio |
| fn DevicePixelRatio(&self) -> Finite<f64> { |
| Finite::wrap(self.device_pixel_ratio().get() as f64) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-window-status |
| fn Status(&self) -> DOMString { |
| self.status.borrow().clone() |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-window-status |
| fn SetStatus(&self, status: DOMString) { |
| *self.status.borrow_mut() = status |
| } |
| |
| // https://drafts.csswg.org/cssom-view/#dom-window-matchmedia |
| fn MatchMedia(&self, query: DOMString) -> DomRoot<MediaQueryList> { |
| let media_query_list = MediaList::parse_media_list(&query, self); |
| let document = self.Document(); |
| let mql = MediaQueryList::new(&document, media_query_list, CanGc::note()); |
| self.media_query_lists.track(&*mql); |
| mql |
| } |
| |
| // https://fetch.spec.whatwg.org/#fetch-method |
| fn Fetch( |
| &self, |
| input: RequestOrUSVString, |
| init: RootedTraceableBox<RequestInit>, |
| comp: InRealm, |
| can_gc: CanGc, |
| ) -> Rc<Promise> { |
| fetch::Fetch(self.upcast(), input, init, comp, can_gc) |
| } |
| |
| #[cfg(feature = "bluetooth")] |
| fn TestRunner(&self) -> DomRoot<TestRunner> { |
| self.test_runner |
| .or_init(|| TestRunner::new(self.upcast(), CanGc::note())) |
| } |
| |
| fn RunningAnimationCount(&self) -> u32 { |
| self.document |
| .get() |
| .map_or(0, |d| d.animations().running_animation_count() as u32) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-name |
| fn SetName(&self, name: DOMString) { |
| if let Some(proxy) = self.undiscarded_window_proxy() { |
| proxy.set_name(name); |
| } |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-name |
| fn Name(&self) -> DOMString { |
| match self.undiscarded_window_proxy() { |
| Some(proxy) => proxy.get_name(), |
| None => "".into(), |
| } |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-origin |
| fn Origin(&self) -> USVString { |
| USVString(self.origin().immutable().ascii_serialization()) |
| } |
| |
| // https://w3c.github.io/selection-api/#dom-window-getselection |
| fn GetSelection(&self) -> Option<DomRoot<Selection>> { |
| self.document |
| .get() |
| .and_then(|d| d.GetSelection(CanGc::note())) |
| } |
| |
| // https://dom.spec.whatwg.org/#dom-window-event |
| fn Event(&self, cx: JSContext, rval: MutableHandleValue) { |
| if let Some(ref event) = *self.current_event.borrow() { |
| event.reflector().get_jsobject().safe_to_jsval(cx, rval); |
| } |
| } |
| |
| fn IsSecureContext(&self) -> bool { |
| self.as_global_scope().is_secure_context() |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#dom-window-nameditem> |
| fn NamedGetter(&self, name: DOMString) -> Option<NamedPropertyValue> { |
| if name.is_empty() { |
| return None; |
| } |
| let document = self.Document(); |
| |
| // https://html.spec.whatwg.org/multipage/#document-tree-child-browsing-context-name-property-set |
| let iframes: Vec<_> = document |
| .iframes() |
| .iter() |
| .filter(|iframe| { |
| if let Some(window) = iframe.GetContentWindow() { |
| return window.get_name() == name; |
| } |
| false |
| }) |
| .collect(); |
| |
| let iframe_iter = iframes.iter().map(|iframe| iframe.upcast::<Element>()); |
| |
| let name = Atom::from(&*name); |
| |
| // Step 1. |
| let elements_with_name = document.get_elements_with_name(&name); |
| let name_iter = elements_with_name |
| .iter() |
| .map(|element| &**element) |
| .filter(|elem| is_named_element_with_name_attribute(elem)); |
| let elements_with_id = document.get_elements_with_id(&name); |
| let id_iter = elements_with_id |
| .iter() |
| .map(|element| &**element) |
| .filter(|elem| is_named_element_with_id_attribute(elem)); |
| |
| // Step 2. |
| for elem in iframe_iter.clone() { |
| if let Some(nested_window_proxy) = elem |
| .downcast::<HTMLIFrameElement>() |
| .and_then(|iframe| iframe.GetContentWindow()) |
| { |
| return Some(NamedPropertyValue::WindowProxy(nested_window_proxy)); |
| } |
| } |
| |
| let mut elements = iframe_iter.chain(name_iter).chain(id_iter); |
| |
| let first = elements.next()?; |
| |
| if elements.next().is_none() { |
| // Step 3. |
| return Some(NamedPropertyValue::Element(DomRoot::from_ref(first))); |
| } |
| |
| // Step 4. |
| #[derive(JSTraceable, MallocSizeOf)] |
| struct WindowNamedGetter { |
| #[no_trace] |
| name: Atom, |
| } |
| impl CollectionFilter for WindowNamedGetter { |
| fn filter(&self, elem: &Element, _root: &Node) -> bool { |
| let type_ = match elem.upcast::<Node>().type_id() { |
| NodeTypeId::Element(ElementTypeId::HTMLElement(type_)) => type_, |
| _ => return false, |
| }; |
| if elem.get_id().as_ref() == Some(&self.name) { |
| return true; |
| } |
| match type_ { |
| HTMLElementTypeId::HTMLEmbedElement | |
| HTMLElementTypeId::HTMLFormElement | |
| HTMLElementTypeId::HTMLImageElement | |
| HTMLElementTypeId::HTMLObjectElement => { |
| elem.get_name().as_ref() == Some(&self.name) |
| }, |
| _ => false, |
| } |
| } |
| } |
| let collection = HTMLCollection::create( |
| self, |
| document.upcast(), |
| Box::new(WindowNamedGetter { name }), |
| CanGc::note(), |
| ); |
| Some(NamedPropertyValue::HTMLCollection(collection)) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-tree-accessors:supported-property-names |
| fn SupportedPropertyNames(&self) -> Vec<DOMString> { |
| let mut names_with_first_named_element_map: HashMap<&Atom, &Element> = HashMap::new(); |
| |
| let document = self.Document(); |
| let name_map = document.name_map(); |
| for (name, elements) in &name_map.0 { |
| if name.is_empty() { |
| continue; |
| } |
| let mut name_iter = elements |
| .iter() |
| .filter(|elem| is_named_element_with_name_attribute(elem)); |
| if let Some(first) = name_iter.next() { |
| names_with_first_named_element_map.insert(name, first); |
| } |
| } |
| let id_map = document.id_map(); |
| for (id, elements) in &id_map.0 { |
| if id.is_empty() { |
| continue; |
| } |
| let mut id_iter = elements |
| .iter() |
| .filter(|elem| is_named_element_with_id_attribute(elem)); |
| if let Some(first) = id_iter.next() { |
| match names_with_first_named_element_map.entry(id) { |
| Entry::Vacant(entry) => drop(entry.insert(first)), |
| Entry::Occupied(mut entry) => { |
| if first.upcast::<Node>().is_before(entry.get().upcast()) { |
| *entry.get_mut() = first; |
| } |
| }, |
| } |
| } |
| } |
| |
| let mut names_with_first_named_element_vec: Vec<(&Atom, &Element)> = |
| names_with_first_named_element_map |
| .iter() |
| .map(|(k, v)| (*k, *v)) |
| .collect(); |
| names_with_first_named_element_vec.sort_unstable_by(|a, b| { |
| if a.1 == b.1 { |
| // This can happen if an img has an id different from its name, |
| // spec does not say which string to put first. |
| a.0.cmp(b.0) |
| } else if a.1.upcast::<Node>().is_before(b.1.upcast::<Node>()) { |
| cmp::Ordering::Less |
| } else { |
| cmp::Ordering::Greater |
| } |
| }); |
| |
| names_with_first_named_element_vec |
| .iter() |
| .map(|(k, _v)| DOMString::from(&***k)) |
| .collect() |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#dom-structuredclone> |
| fn StructuredClone( |
| &self, |
| cx: JSContext, |
| value: HandleValue, |
| options: RootedTraceableBox<StructuredSerializeOptions>, |
| retval: MutableHandleValue, |
| ) -> Fallible<()> { |
| self.as_global_scope() |
| .structured_clone(cx, value, options, retval) |
| } |
| |
| fn TrustedTypes(&self, can_gc: CanGc) -> DomRoot<TrustedTypePolicyFactory> { |
| self.trusted_types |
| .or_init(|| TrustedTypePolicyFactory::new(self.as_global_scope(), can_gc)) |
| } |
| } |
| |
| impl Window { |
| pub(crate) fn scroll_offset(&self) -> Vector2D<f32, LayoutPixel> { |
| self.scroll_offset_query_with_external_scroll_id(self.pipeline_id().root_scroll_id()) |
| } |
| |
| // https://heycam.github.io/webidl/#named-properties-object |
| // https://html.spec.whatwg.org/multipage/#named-access-on-the-window-object |
| #[allow(unsafe_code)] |
| pub(crate) fn create_named_properties_object( |
| cx: JSContext, |
| proto: HandleObject, |
| object: MutableHandleObject, |
| ) { |
| window_named_properties::create(cx, proto, object) |
| } |
| |
| pub(crate) fn current_event(&self) -> Option<DomRoot<Event>> { |
| self.current_event |
| .borrow() |
| .as_ref() |
| .map(|e| DomRoot::from_ref(&**e)) |
| } |
| |
| pub(crate) fn set_current_event(&self, event: Option<&Event>) -> Option<DomRoot<Event>> { |
| let current = self.current_event(); |
| *self.current_event.borrow_mut() = event.map(Dom::from_ref); |
| current |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#window-post-message-steps> |
| fn post_message_impl( |
| &self, |
| target_origin: &USVString, |
| source_origin: ImmutableOrigin, |
| source: &Window, |
| cx: JSContext, |
| message: HandleValue, |
| transfer: CustomAutoRooterGuard<Vec<*mut JSObject>>, |
| ) -> ErrorResult { |
| // Step 1-2, 6-8. |
| let data = structuredclone::write(cx, message, Some(transfer))?; |
| |
| // Step 3-5. |
| let target_origin = match target_origin.0[..].as_ref() { |
| "*" => None, |
| "/" => Some(source_origin.clone()), |
| url => match ServoUrl::parse(url) { |
| Ok(url) => Some(url.origin().clone()), |
| Err(_) => return Err(Error::Syntax(None)), |
| }, |
| }; |
| |
| // Step 9. |
| self.post_message(target_origin, source_origin, &source.window_proxy(), data); |
| Ok(()) |
| } |
| |
| // https://drafts.css-houdini.org/css-paint-api-1/#paint-worklet |
| pub(crate) fn paint_worklet(&self) -> DomRoot<Worklet> { |
| self.paint_worklet |
| .or_init(|| self.new_paint_worklet(CanGc::note())) |
| } |
| |
| pub(crate) fn has_document(&self) -> bool { |
| self.document.get().is_some() |
| } |
| |
| pub(crate) fn clear_js_runtime(&self) { |
| self.as_global_scope() |
| .remove_web_messaging_and_dedicated_workers_infra(); |
| |
| // Clean up any active promises |
| // https://github.com/servo/servo/issues/15318 |
| if let Some(custom_elements) = self.custom_element_registry.get() { |
| custom_elements.teardown(); |
| } |
| |
| self.current_state.set(WindowState::Zombie); |
| *self.js_runtime.borrow_mut() = None; |
| |
| // If this is the currently active pipeline, |
| // nullify the window_proxy. |
| if let Some(proxy) = self.window_proxy.get() { |
| let pipeline_id = self.pipeline_id(); |
| if let Some(currently_active) = proxy.currently_active() { |
| if currently_active == pipeline_id { |
| self.window_proxy.set(None); |
| } |
| } |
| } |
| |
| if let Some(performance) = self.performance.get() { |
| performance.clear_and_disable_performance_entry_buffer(); |
| } |
| self.as_global_scope() |
| .task_manager() |
| .cancel_all_tasks_and_ignore_future_tasks(); |
| } |
| |
| /// <https://drafts.csswg.org/cssom-view/#dom-window-scroll> |
| pub(crate) fn scroll(&self, x: f32, y: f32, behavior: ScrollBehavior) { |
| // Step 3: Normalize non-finite values for x and y. |
| let xfinite = if x.is_finite() { x } else { 0.0 }; |
| let yfinite = if y.is_finite() { y } else { 0.0 }; |
| |
| // Step 4: If there is no viewport, abort these steps. |
| // Currently every frame has a viewport in Servo. |
| |
| // Step 5. Let `viewport width` be the width of the viewport excluding the width |
| // of the scroll bar, if any. |
| // Step 6. `Let viewport height` be the height of the viewport excluding the |
| // height of the scroll bar, if any. |
| // |
| // TODO: Servo does not yet support scrollbars. |
| let viewport = self.viewport_details.get().size; |
| |
| // Step 7: |
| // If the viewport has rightward overflow direction |
| // Let x be max(0, min(x, viewport scrolling area width - viewport width)). |
| // If the viewport has leftward overflow direction |
| // Let x be min(0, max(x, viewport width - viewport scrolling area width)). |
| // TODO: Implement this. |
| |
| // Step 8: |
| // If the viewport has downward overflow direction |
| // Let y be max(0, min(y, viewport scrolling area height - viewport height)). |
| // If the viewport has upward overflow direction |
| // Let y be min(0, max(y, viewport height - viewport scrolling area height)). |
| // TODO: Implement this. |
| |
| // Step 9: Let position be the scroll position the viewport would have by aligning |
| // the x-coordinate x of the viewport scrolling area with the left of the viewport |
| // and aligning the y-coordinate y of the viewport scrolling area with the top of |
| // the viewport. |
| let scrolling_area = self.scrolling_area_query(None); |
| let x = xfinite.clamp(0.0, scrolling_area.width() as f32 - viewport.width); |
| let y = yfinite.clamp(0.0, scrolling_area.height() as f32 - viewport.height); |
| |
| // Step 10: If position is the same as the viewport’s current scroll position, and |
| // the viewport does not have an ongoing smooth scroll, abort these steps. |
| let scroll_offset = self.scroll_offset(); |
| if x == scroll_offset.x && y == scroll_offset.y { |
| return; |
| } |
| |
| // Step 11: Let document be the viewport’s associated Document. |
| // Step 12: Perform a scroll of the viewport to position, document’s root element |
| // as the associated element, if there is one, or null otherwise, and the scroll |
| // behavior being the value of the behavior dictionary member of options. |
| self.perform_a_scroll(x, y, self.pipeline_id().root_scroll_id(), behavior, None); |
| } |
| |
| /// <https://drafts.csswg.org/cssom-view/#perform-a-scroll> |
| pub(crate) fn perform_a_scroll( |
| &self, |
| x: f32, |
| y: f32, |
| scroll_id: ExternalScrollId, |
| _behavior: ScrollBehavior, |
| element: Option<&Element>, |
| ) { |
| // TODO Step 1 |
| // TODO(mrobinson, #18709): Add smooth scrolling support to WebRender so that we can |
| // properly process ScrollBehavior here. |
| let reflow_phases_run = |
| self.reflow(ReflowGoal::UpdateScrollNode(scroll_id, Vector2D::new(x, y))); |
| if reflow_phases_run.needs_frame() { |
| self.compositor_api().generate_frame(); |
| } |
| |
| // > If the scroll position did not change as a result of the user interaction or programmatic |
| // > invocation, where no translations were applied as a result, then no scrollend event fires |
| // > because no scrolling occurred. |
| // Even though the note mention the scrollend, it is relevant to the scroll as well. |
| if reflow_phases_run.contains(ReflowPhasesRun::UpdatedScrollNodeOffset) { |
| match element { |
| Some(el) => self.Document().handle_element_scroll_event(el), |
| None => self.Document().handle_viewport_scroll_event(), |
| }; |
| } |
| } |
| |
| pub(crate) fn device_pixel_ratio(&self) -> Scale<f32, CSSPixel, DevicePixel> { |
| self.viewport_details.get().hidpi_scale_factor |
| } |
| |
| fn client_window(&self) -> DeviceIndependentIntRect { |
| let (sender, receiver) = generic_channel::channel().expect("Failed to create IPC channel!"); |
| |
| self.send_to_embedder(EmbedderMsg::GetWindowRect(self.webview_id(), sender)); |
| |
| receiver.recv().unwrap_or_default() |
| } |
| |
| /// Prepares to tick animations and then does a reflow which also advances the |
| /// layout animation clock. |
| #[allow(unsafe_code)] |
| pub(crate) fn advance_animation_clock(&self, delta_ms: i32) { |
| self.Document() |
| .advance_animation_timeline_for_testing(delta_ms as f64 / 1000.); |
| ScriptThread::handle_tick_all_animations_for_testing(self.pipeline_id()); |
| } |
| |
| /// Reflows the page unconditionally if possible and not suppressed. This method will wait for |
| /// the layout to complete. If there is no window size yet, the page is presumed invisible and |
| /// no reflow is performed. If reflow is suppressed, no reflow will be performed for ForDisplay |
| /// goals. |
| /// |
| /// NOTE: This method should almost never be called directly! Layout and rendering updates should |
| /// happen as part of the HTML event loop via *update the rendering*. |
| pub(crate) fn reflow(&self, reflow_goal: ReflowGoal) -> ReflowPhasesRun { |
| let document = self.Document(); |
| |
| // Never reflow inactive Documents. |
| if !document.is_fully_active() { |
| return ReflowPhasesRun::empty(); |
| } |
| |
| self.Document().ensure_safe_to_run_script_or_layout(); |
| |
| // If layouts are blocked, we block all layouts that are for display only. Other |
| // layouts (for queries and scrolling) are not blocked, as they do not display |
| // anything and script expects the layout to be up-to-date after they run. |
| let pipeline_id = self.pipeline_id(); |
| if reflow_goal == ReflowGoal::UpdateTheRendering && |
| self.layout_blocker.get().layout_blocked() |
| { |
| debug!("Suppressing pre-load-event reflow pipeline {pipeline_id}"); |
| return ReflowPhasesRun::empty(); |
| } |
| |
| debug!("script: performing reflow for goal {reflow_goal:?}"); |
| let marker = if self.need_emit_timeline_marker(TimelineMarkerType::Reflow) { |
| Some(TimelineMarker::start("Reflow".to_owned())) |
| } else { |
| None |
| }; |
| |
| let restyle_reason = document.restyle_reason(); |
| document.clear_restyle_reasons(); |
| let restyle = if restyle_reason.needs_restyle() { |
| debug!("Invalidating layout cache due to reflow condition {restyle_reason:?}",); |
| // Invalidate any existing cached layout values. |
| self.layout_marker.borrow().set(false); |
| // Create a new layout caching token. |
| *self.layout_marker.borrow_mut() = Rc::new(Cell::new(true)); |
| |
| let stylesheets_changed = document.flush_stylesheets_for_reflow(); |
| let pending_restyles = document.drain_pending_restyles(); |
| let dirty_root = document |
| .take_dirty_root() |
| .filter(|_| !stylesheets_changed) |
| .or_else(|| document.GetDocumentElement()) |
| .map(|root| root.upcast::<Node>().to_trusted_node_address()); |
| |
| Some(ReflowRequestRestyle { |
| reason: restyle_reason, |
| dirty_root, |
| stylesheets_changed, |
| pending_restyles, |
| }) |
| } else { |
| None |
| }; |
| |
| let reflow = ReflowRequest { |
| document: document.upcast::<Node>().to_trusted_node_address(), |
| restyle, |
| viewport_details: self.viewport_details.get(), |
| origin: self.origin().immutable().clone(), |
| reflow_goal, |
| dom_count: document.dom_count(), |
| animation_timeline_value: document.current_animation_timeline_value(), |
| animations: document.animations().sets.clone(), |
| node_to_animating_image_map: document.image_animation_manager().node_to_image_map(), |
| theme: self.theme.get(), |
| highlighted_dom_node: document.highlighted_dom_node().map(|node| node.to_opaque()), |
| }; |
| |
| let Some(reflow_result) = self.layout.borrow_mut().reflow(reflow) else { |
| return ReflowPhasesRun::empty(); |
| }; |
| |
| debug!("script: layout complete"); |
| if let Some(marker) = marker { |
| self.emit_timeline_marker(marker.end()); |
| } |
| |
| self.handle_pending_images_post_reflow( |
| reflow_result.pending_images, |
| reflow_result.pending_rasterization_images, |
| reflow_result.pending_svg_elements_for_serialization, |
| ); |
| |
| if let Some(iframe_sizes) = reflow_result.iframe_sizes { |
| document |
| .iframes_mut() |
| .handle_new_iframe_sizes_after_layout(self, iframe_sizes); |
| } |
| |
| document.update_animations_post_reflow(); |
| self.update_constellation_epoch(); |
| |
| reflow_result.reflow_phases_run |
| } |
| |
| pub(crate) fn maybe_send_idle_document_state_to_constellation(&self) { |
| if !opts::get().wait_for_stable_image { |
| return; |
| } |
| |
| if self.has_sent_idle_message.get() { |
| return; |
| } |
| |
| let document = self.Document(); |
| if document.ReadyState() != DocumentReadyState::Complete { |
| return; |
| } |
| |
| // Checks if the html element has reftest-wait attribute present. |
| // See http://testthewebforward.org/docs/reftests.html |
| // and https://web-platform-tests.org/writing-tests/crashtest.html |
| if document.GetDocumentElement().is_some_and(|elem| { |
| elem.has_class(&atom!("reftest-wait"), CaseSensitivity::CaseSensitive) || |
| elem.has_class(&Atom::from("test-wait"), CaseSensitivity::CaseSensitive) |
| }) { |
| return; |
| } |
| |
| if self.font_context().web_fonts_still_loading() != 0 { |
| return; |
| } |
| |
| if !self.pending_layout_images.borrow().is_empty() || |
| !self.pending_images_for_rasterization.borrow().is_empty() |
| { |
| return; |
| } |
| |
| if self.Document().needs_rendering_update() { |
| return; |
| } |
| |
| // When all these conditions are met, notify the constellation |
| // that this pipeline is ready to write the image (from the script thread |
| // perspective at least). |
| debug!( |
| "{:?}: Sending DocumentState::Idle to Constellation", |
| self.pipeline_id() |
| ); |
| self.send_to_constellation(ScriptToConstellationMessage::SetDocumentState( |
| DocumentState::Idle, |
| )); |
| self.has_sent_idle_message.set(true); |
| } |
| |
| /// If parsing has taken a long time and reflows are still waiting for the `load` event, |
| /// start allowing them. See <https://github.com/servo/servo/pull/6028>. |
| pub(crate) fn reflow_if_reflow_timer_expired(&self) { |
| // Only trigger a long parsing time reflow if we are in the first parse of `<body>` |
| // and it started more than `INITIAL_REFLOW_DELAY` ago. |
| if !matches!( |
| self.layout_blocker.get(), |
| LayoutBlocker::Parsing(instant) if instant + INITIAL_REFLOW_DELAY < Instant::now() |
| ) { |
| return; |
| } |
| self.allow_layout_if_necessary(); |
| } |
| |
| /// Block layout for this `Window` until parsing is done. If parsing takes a long time, |
| /// we want to layout anyway, so schedule a moment in the future for when layouts are |
| /// allowed even though parsing isn't finished and we havne't sent a load event. |
| pub(crate) fn prevent_layout_until_load_event(&self) { |
| // If we have already started parsing or have already fired a load event, then |
| // don't delay the first layout any longer. |
| if !matches!(self.layout_blocker.get(), LayoutBlocker::WaitingForParse) { |
| return; |
| } |
| |
| self.layout_blocker |
| .set(LayoutBlocker::Parsing(Instant::now())); |
| } |
| |
| /// Inform the [`Window`] that layout is allowed either because `load` has happened |
| /// or because parsing the `<body>` took so long that we cannot wait any longer. |
| pub(crate) fn allow_layout_if_necessary(&self) { |
| if matches!( |
| self.layout_blocker.get(), |
| LayoutBlocker::FiredLoadEventOrParsingTimerExpired |
| ) { |
| return; |
| } |
| |
| self.layout_blocker |
| .set(LayoutBlocker::FiredLoadEventOrParsingTimerExpired); |
| |
| // We do this immediately instead of scheduling a future task, because this can |
| // happen if parsing is taking a very long time, which means that the |
| // `ScriptThread` is busy doing the parsing and not doing layouts. |
| // |
| // TOOD(mrobinson): It's expected that this is necessary when in the process of |
| // parsing, as we need to interrupt it to update contents, but why is this |
| // necessary when parsing finishes? Not doing the synchronous update in that case |
| // causes iframe tests to become flaky. It seems there's an issue with the timing of |
| // iframe size updates. |
| // |
| // See <https://github.com/servo/servo/issues/14719> |
| if self.Document().update_the_rendering().needs_frame() { |
| self.compositor_api().generate_frame(); |
| } |
| } |
| |
| pub(crate) fn layout_blocked(&self) -> bool { |
| self.layout_blocker.get().layout_blocked() |
| } |
| |
| /// If writing a screenshot, synchronously update the layout epoch that it set |
| /// in the constellation. |
| pub(crate) fn update_constellation_epoch(&self) { |
| if !opts::get().wait_for_stable_image { |
| return; |
| } |
| |
| let epoch = self.layout.borrow().current_epoch(); |
| debug!( |
| "{:?}: Updating constellation epoch: {epoch:?}", |
| self.pipeline_id() |
| ); |
| let (sender, receiver) = ipc::channel().expect("Failed to create IPC channel!"); |
| let event = ScriptToConstellationMessage::SetLayoutEpoch(epoch, sender); |
| self.send_to_constellation(event); |
| let _ = receiver.recv(); |
| } |
| |
| /// Trigger a reflow that is required by a certain queries. |
| pub(crate) fn layout_reflow(&self, query_msg: QueryMsg) { |
| self.reflow(ReflowGoal::LayoutQuery(query_msg)); |
| } |
| |
| pub(crate) fn resolved_font_style_query( |
| &self, |
| node: &Node, |
| value: String, |
| ) -> Option<ServoArc<Font>> { |
| self.layout_reflow(QueryMsg::ResolvedFontStyleQuery); |
| |
| let document = self.Document(); |
| let animations = document.animations().sets.clone(); |
| self.layout.borrow().query_resolved_font_style( |
| node.to_trusted_node_address(), |
| &value, |
| animations, |
| document.current_animation_timeline_value(), |
| ) |
| } |
| |
| /// Do the same kind of query as `Self::box_area_query`, but do not force a reflow. |
| /// This is used for things like `IntersectionObserver` which should observe the value |
| /// from the most recent reflow, but do not need it to reflect the current state of |
| /// the DOM / style. |
| pub(crate) fn box_area_query_without_reflow( |
| &self, |
| node: &Node, |
| area: BoxAreaType, |
| ) -> Option<UntypedRect<Au>> { |
| let layout = self.layout.borrow(); |
| layout.ensure_stacking_context_tree(self.viewport_details.get()); |
| layout.query_box_area(node.to_trusted_node_address(), area) |
| } |
| |
| pub(crate) fn box_area_query(&self, node: &Node, area: BoxAreaType) -> Option<UntypedRect<Au>> { |
| self.layout_reflow(QueryMsg::BoxArea); |
| self.box_area_query_without_reflow(node, area) |
| } |
| |
| pub(crate) fn box_areas_query(&self, node: &Node, area: BoxAreaType) -> Vec<UntypedRect<Au>> { |
| self.layout_reflow(QueryMsg::BoxAreas); |
| self.layout |
| .borrow() |
| .query_box_areas(node.to_trusted_node_address(), area) |
| } |
| |
| pub(crate) fn client_rect_query(&self, node: &Node) -> UntypedRect<i32> { |
| self.layout_reflow(QueryMsg::ClientRectQuery); |
| self.layout |
| .borrow() |
| .query_client_rect(node.to_trusted_node_address()) |
| } |
| |
| /// Find the scroll area of the given node, if it is not None. If the node |
| /// is None, find the scroll area of the viewport. |
| pub(crate) fn scrolling_area_query(&self, node: Option<&Node>) -> UntypedRect<i32> { |
| self.layout_reflow(QueryMsg::ScrollingAreaOrOffsetQuery); |
| self.layout |
| .borrow() |
| .query_scrolling_area(node.map(Node::to_trusted_node_address)) |
| } |
| |
| pub(crate) fn scroll_offset_query(&self, node: &Node) -> Vector2D<f32, LayoutPixel> { |
| let external_scroll_id = ExternalScrollId( |
| combine_id_with_fragment_type(node.to_opaque().id(), FragmentType::FragmentBody), |
| self.pipeline_id().into(), |
| ); |
| self.scroll_offset_query_with_external_scroll_id(external_scroll_id) |
| } |
| |
| fn scroll_offset_query_with_external_scroll_id( |
| &self, |
| external_scroll_id: ExternalScrollId, |
| ) -> Vector2D<f32, LayoutPixel> { |
| self.layout_reflow(QueryMsg::ScrollingAreaOrOffsetQuery); |
| self.scroll_offset_query_with_external_scroll_id_no_reflow(external_scroll_id) |
| } |
| |
| fn scroll_offset_query_with_external_scroll_id_no_reflow( |
| &self, |
| external_scroll_id: ExternalScrollId, |
| ) -> Vector2D<f32, LayoutPixel> { |
| self.layout |
| .borrow() |
| .scroll_offset(external_scroll_id) |
| .unwrap_or_default() |
| } |
| |
| /// <https://drafts.csswg.org/cssom-view/#scroll-an-element> |
| // TODO(stevennovaryo): Need to update the scroll API to follow the spec since it is quite outdated. |
| pub(crate) fn scroll_an_element( |
| &self, |
| element: &Element, |
| x: f32, |
| y: f32, |
| behavior: ScrollBehavior, |
| ) { |
| let scroll_id = ExternalScrollId( |
| combine_id_with_fragment_type( |
| element.upcast::<Node>().to_opaque().id(), |
| FragmentType::FragmentBody, |
| ), |
| self.pipeline_id().into(), |
| ); |
| |
| // Step 6. |
| // > Perform a scroll of box to position, element as the associated element and behavior as |
| // > the scroll behavior. |
| self.perform_a_scroll(x, y, scroll_id, behavior, Some(element)); |
| } |
| |
| pub(crate) fn resolved_style_query( |
| &self, |
| element: TrustedNodeAddress, |
| pseudo: Option<PseudoElement>, |
| property: PropertyId, |
| ) -> DOMString { |
| self.layout_reflow(QueryMsg::ResolvedStyleQuery); |
| |
| let document = self.Document(); |
| let animations = document.animations().sets.clone(); |
| DOMString::from(self.layout.borrow().query_resolved_style( |
| element, |
| pseudo, |
| property, |
| animations, |
| document.current_animation_timeline_value(), |
| )) |
| } |
| |
| /// If the given |browsing_context_id| refers to an `<iframe>` that is an element |
| /// in this [`Window`] and that `<iframe>` has been laid out, return its size. |
| /// Otherwise, return `None`. |
| pub(crate) fn get_iframe_viewport_details_if_known( |
| &self, |
| browsing_context_id: BrowsingContextId, |
| ) -> Option<ViewportDetails> { |
| // Reflow might fail, but do a best effort to return the right size. |
| self.layout_reflow(QueryMsg::InnerWindowDimensionsQuery); |
| self.Document() |
| .iframes() |
| .get(browsing_context_id) |
| .and_then(|iframe| iframe.size) |
| } |
| |
| #[allow(unsafe_code)] |
| pub(crate) fn offset_parent_query( |
| &self, |
| node: &Node, |
| ) -> (Option<DomRoot<Element>>, UntypedRect<Au>) { |
| self.layout_reflow(QueryMsg::OffsetParentQuery); |
| let response = self |
| .layout |
| .borrow() |
| .query_offset_parent(node.to_trusted_node_address()); |
| let element = response.node_address.and_then(|parent_node_address| { |
| let node = unsafe { from_untrusted_node_address(parent_node_address) }; |
| DomRoot::downcast(node) |
| }); |
| (element, response.rect) |
| } |
| |
| pub(crate) fn scroll_container_query( |
| &self, |
| node: &Node, |
| query_type: ScrollContainerQueryType, |
| ) -> Option<ScrollContainerResponse> { |
| self.layout_reflow(QueryMsg::ScrollParentQuery); |
| self.layout |
| .borrow() |
| .query_scroll_container(node.to_trusted_node_address(), query_type) |
| } |
| |
| pub(crate) fn text_index_query( |
| &self, |
| node: &Node, |
| point_in_node: UntypedPoint2D<f32>, |
| ) -> Option<usize> { |
| self.layout_reflow(QueryMsg::TextIndexQuery); |
| self.layout |
| .borrow() |
| .query_text_indext(node.to_opaque(), point_in_node) |
| } |
| |
| pub(crate) fn elements_from_point_query( |
| &self, |
| point: LayoutPoint, |
| flags: ElementsFromPointFlags, |
| ) -> Vec<ElementsFromPointResult> { |
| self.layout_reflow(QueryMsg::ElementsFromPoint); |
| self.layout().query_elements_from_point(point, flags) |
| } |
| |
| pub(crate) fn hit_test_from_input_event( |
| &self, |
| input_event: &ConstellationInputEvent, |
| ) -> Option<HitTestResult> { |
| self.hit_test_from_point_in_viewport( |
| input_event.hit_test_result.as_ref()?.point_in_viewport, |
| ) |
| } |
| |
| #[allow(unsafe_code)] |
| pub(crate) fn hit_test_from_point_in_viewport( |
| &self, |
| point_in_frame: Point2D<f32, CSSPixel>, |
| ) -> Option<HitTestResult> { |
| let result = self |
| .elements_from_point_query(point_in_frame.cast_unit(), ElementsFromPointFlags::empty()) |
| .into_iter() |
| .nth(0)?; |
| |
| let point_relative_to_initial_containing_block = |
| point_in_frame + self.scroll_offset().cast_unit(); |
| |
| // SAFETY: This is safe because `Window::query_elements_from_point` has ensured that |
| // layout has run and any OpaqueNodes that no longer refer to real nodes are gone. |
| let address = UntrustedNodeAddress(result.node.0 as *const c_void); |
| Some(HitTestResult { |
| node: unsafe { from_untrusted_node_address(address) }, |
| cursor: result.cursor, |
| point_in_node: result.point_in_target, |
| point_in_frame, |
| point_relative_to_initial_containing_block, |
| }) |
| } |
| |
| #[allow(unsafe_code)] |
| pub(crate) fn init_window_proxy(&self, window_proxy: &WindowProxy) { |
| assert!(self.window_proxy.get().is_none()); |
| self.window_proxy.set(Some(window_proxy)); |
| } |
| |
| #[allow(unsafe_code)] |
| pub(crate) fn init_document(&self, document: &Document) { |
| assert!(self.document.get().is_none()); |
| assert!(document.window() == self); |
| self.document.set(Some(document)); |
| |
| if self.unminify_css { |
| *self.unminified_css_dir.borrow_mut() = Some(unminified_path("unminified-css")); |
| } |
| } |
| |
| /// Commence a new URL load which will either replace this window or scroll to a fragment. |
| /// |
| /// <https://html.spec.whatwg.org/multipage/#navigating-across-documents> |
| pub(crate) fn load_url( |
| &self, |
| history_handling: NavigationHistoryBehavior, |
| force_reload: bool, |
| load_data: LoadData, |
| can_gc: CanGc, |
| ) { |
| let doc = self.Document(); |
| |
| // Step 3. Let initiatorOriginSnapshot be sourceDocument's origin. |
| let initiator_origin_snapshot = &load_data.load_origin; |
| |
| // TODO: Important re security. See https://github.com/servo/servo/issues/23373 |
| // Step 5. check that the source browsing-context is "allowed to navigate" this window. |
| if !force_reload && |
| load_data.url.as_url()[..Position::AfterQuery] == |
| doc.url().as_url()[..Position::AfterQuery] |
| { |
| // Step 6 |
| // TODO: Fragment handling appears to have moved to step 13 |
| if let Some(fragment) = load_data.url.fragment() { |
| let webdriver_sender = self.webdriver_load_status_sender.borrow().clone(); |
| if let Some(ref sender) = webdriver_sender { |
| let _ = sender.send(WebDriverLoadStatus::NavigationStart); |
| } |
| |
| self.send_to_constellation(ScriptToConstellationMessage::NavigatedToFragment( |
| load_data.url.clone(), |
| history_handling, |
| )); |
| doc.check_and_scroll_fragment(fragment); |
| let this = Trusted::new(self); |
| let old_url = doc.url().into_string(); |
| let new_url = load_data.url.clone().into_string(); |
| let task = task!(hashchange_event: move || { |
| let this = this.root(); |
| let event = HashChangeEvent::new( |
| &this, |
| atom!("hashchange"), |
| false, |
| false, |
| old_url, |
| new_url, |
| CanGc::note()); |
| event.upcast::<Event>().fire(this.upcast::<EventTarget>(), CanGc::note()); |
| if let Some(sender) = webdriver_sender { |
| let _ = sender.send(WebDriverLoadStatus::NavigationStop); |
| } |
| }); |
| self.as_global_scope() |
| .task_manager() |
| .dom_manipulation_task_source() |
| .queue(task); |
| doc.set_url(load_data.url.clone()); |
| return; |
| } |
| } |
| |
| // Step 4 and 5 |
| let pipeline_id = self.pipeline_id(); |
| let window_proxy = self.window_proxy(); |
| if let Some(active) = window_proxy.currently_active() { |
| if pipeline_id == active && doc.is_prompting_or_unloading() { |
| return; |
| } |
| } |
| |
| // Step 8 |
| if doc.prompt_to_unload(false, can_gc) { |
| let window_proxy = self.window_proxy(); |
| if window_proxy.parent().is_some() { |
| // Step 10 |
| // If browsingContext is a nested browsing context, |
| // then put it in the delaying load events mode. |
| window_proxy.start_delaying_load_events_mode(); |
| } |
| |
| // Step 11. If historyHandling is "auto", then: |
| let resolved_history_handling = if history_handling == NavigationHistoryBehavior::Auto { |
| // Step 11.1. If url equals navigable's active document's URL, and |
| // initiatorOriginSnapshot is same origin with targetNavigable's active document's |
| // origin, then set historyHandling to "replace". |
| // Note: `targetNavigable` is not actually defined in the spec, "active document" is |
| // assumed to be the correct reference based on WPT results |
| if let LoadOrigin::Script(initiator_origin) = initiator_origin_snapshot { |
| if load_data.url == doc.url() && initiator_origin.same_origin(doc.origin()) { |
| NavigationHistoryBehavior::Replace |
| } else { |
| NavigationHistoryBehavior::Push |
| } |
| } else { |
| // Step 11.2. Otherwise, set historyHandling to "push". |
| NavigationHistoryBehavior::Push |
| } |
| // Step 12. If the navigation must be a replace given url and navigable's active |
| // document, then set historyHandling to "replace". |
| } else if load_data.url.scheme() == "javascript" || doc.is_initial_about_blank() { |
| NavigationHistoryBehavior::Replace |
| } else { |
| NavigationHistoryBehavior::Push |
| }; |
| |
| if let Some(sender) = self.webdriver_load_status_sender.borrow().as_ref() { |
| let _ = sender.send(WebDriverLoadStatus::NavigationStart); |
| } |
| |
| // Step 13 |
| ScriptThread::navigate(pipeline_id, load_data, resolved_history_handling); |
| }; |
| } |
| |
| pub(crate) fn set_viewport_details(&self, size: ViewportDetails) { |
| self.viewport_details.set(size); |
| } |
| |
| pub(crate) fn viewport_details(&self) -> ViewportDetails { |
| self.viewport_details.get() |
| } |
| |
| /// Get the theme of this [`Window`]. |
| pub(crate) fn theme(&self) -> Theme { |
| self.theme.get() |
| } |
| |
| /// Handle a theme change request, triggering a reflow is any actual change occured. |
| pub(crate) fn handle_theme_change(&self, new_theme: Theme) { |
| if self.theme.get() == new_theme { |
| return; |
| } |
| self.theme.set(new_theme); |
| self.Document() |
| .add_restyle_reason(RestyleReason::ThemeChanged); |
| } |
| |
| pub(crate) fn get_url(&self) -> ServoUrl { |
| self.Document().url() |
| } |
| |
| pub(crate) fn windowproxy_handler(&self) -> &'static WindowProxyHandler { |
| self.dom_static.windowproxy_handler |
| } |
| |
| pub(crate) fn add_resize_event(&self, event: ViewportDetails, event_type: WindowSizeType) { |
| // Whenever we receive a new resize event we forget about all the ones that came before |
| // it, to avoid unnecessary relayouts |
| *self.unhandled_resize_event.borrow_mut() = Some((event, event_type)) |
| } |
| |
| pub(crate) fn take_unhandled_resize_event(&self) -> Option<(ViewportDetails, WindowSizeType)> { |
| self.unhandled_resize_event.borrow_mut().take() |
| } |
| |
| /// Whether or not this [`Window`] has any resize events that have not been processed. |
| pub(crate) fn has_unhandled_resize_event(&self) -> bool { |
| self.unhandled_resize_event.borrow().is_some() |
| } |
| |
| pub(crate) fn set_viewport_size(&self, new_viewport_size: UntypedSize2D<f32>) { |
| let new_viewport_size = Size2D::new( |
| Au::from_f32_px(new_viewport_size.width), |
| Au::from_f32_px(new_viewport_size.height), |
| ); |
| if new_viewport_size == self.current_viewport_size.get() { |
| return; |
| } |
| |
| self.current_viewport_size.set(new_viewport_size); |
| |
| // The document needs to be repainted, because the initial containing block |
| // is now a different size. |
| self.Document() |
| .add_restyle_reason(RestyleReason::ViewportSizeChanged); |
| |
| // If viewport units were used, all nodes need to be restyled, because |
| // we currently do not track which ones rely on viewport units. |
| if self.layout().device().used_viewport_units() { |
| self.Document().dirty_all_nodes(); |
| } |
| } |
| |
| pub(crate) fn suspend(&self, can_gc: CanGc) { |
| // Suspend timer events. |
| self.as_global_scope().suspend(); |
| |
| // Set the window proxy to be a cross-origin window. |
| if self.window_proxy().currently_active() == Some(self.global().pipeline_id()) { |
| self.window_proxy().unset_currently_active(can_gc); |
| } |
| |
| // A hint to the JS runtime that now would be a good time to |
| // GC any unreachable objects generated by user script, |
| // or unattached DOM nodes. Attached DOM nodes can't be GCd yet, |
| // as the document might be reactivated later. |
| self.Gc(); |
| } |
| |
| pub(crate) fn resume(&self, can_gc: CanGc) { |
| // Resume timer events. |
| self.as_global_scope().resume(); |
| |
| // Set the window proxy to be this object. |
| self.window_proxy().set_currently_active(self, can_gc); |
| |
| // Push the document title to the compositor since we are |
| // activating this document due to a navigation. |
| self.Document().title_changed(); |
| } |
| |
| pub(crate) fn need_emit_timeline_marker(&self, timeline_type: TimelineMarkerType) -> bool { |
| let markers = self.devtools_markers.borrow(); |
| markers.contains(&timeline_type) |
| } |
| |
| pub(crate) fn emit_timeline_marker(&self, marker: TimelineMarker) { |
| let sender = self.devtools_marker_sender.borrow(); |
| let sender = sender.as_ref().expect("There is no marker sender"); |
| sender.send(Some(marker)).unwrap(); |
| } |
| |
| pub(crate) fn set_devtools_timeline_markers( |
| &self, |
| markers: Vec<TimelineMarkerType>, |
| reply: IpcSender<Option<TimelineMarker>>, |
| ) { |
| *self.devtools_marker_sender.borrow_mut() = Some(reply); |
| self.devtools_markers.borrow_mut().extend(markers); |
| } |
| |
| pub(crate) fn drop_devtools_timeline_markers(&self, markers: Vec<TimelineMarkerType>) { |
| let mut devtools_markers = self.devtools_markers.borrow_mut(); |
| for marker in markers { |
| devtools_markers.remove(&marker); |
| } |
| if devtools_markers.is_empty() { |
| *self.devtools_marker_sender.borrow_mut() = None; |
| } |
| } |
| |
| pub(crate) fn set_webdriver_script_chan(&self, chan: Option<IpcSender<WebDriverJSResult>>) { |
| *self.webdriver_script_chan.borrow_mut() = chan; |
| } |
| |
| pub(crate) fn set_webdriver_load_status_sender( |
| &self, |
| sender: Option<GenericSender<WebDriverLoadStatus>>, |
| ) { |
| *self.webdriver_load_status_sender.borrow_mut() = sender; |
| } |
| |
| pub(crate) fn is_alive(&self) -> bool { |
| self.current_state.get() == WindowState::Alive |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#top-level-browsing-context |
| pub(crate) fn is_top_level(&self) -> bool { |
| self.parent_info.is_none() |
| } |
| |
| /// An implementation of: |
| /// <https://drafts.csswg.org/cssom-view/#document-run-the-resize-steps> |
| /// |
| /// Returns true if there were any pending resize events. |
| pub(crate) fn run_the_resize_steps(&self, can_gc: CanGc) -> bool { |
| let Some((new_size, size_type)) = self.take_unhandled_resize_event() else { |
| return false; |
| }; |
| |
| if self.viewport_details() == new_size { |
| return false; |
| } |
| |
| let _realm = enter_realm(self); |
| debug!( |
| "Resizing Window for pipeline {:?} from {:?} to {new_size:?}", |
| self.pipeline_id(), |
| self.viewport_details(), |
| ); |
| self.set_viewport_details(new_size); |
| |
| // The document needs to be repainted, because the initial containing |
| // block is now a different size. This should be triggered before the |
| // event is fired below so that any script queries trigger a restyle. |
| self.Document() |
| .add_restyle_reason(RestyleReason::ViewportSizeChanged); |
| |
| // If viewport units were used, all nodes need to be restyled, because |
| // we currently do not track which ones rely on viewport units. |
| if self.layout().device().used_viewport_units() { |
| self.Document().dirty_all_nodes(); |
| } |
| |
| // http://dev.w3.org/csswg/cssom-view/#resizing-viewports |
| if size_type == WindowSizeType::Resize { |
| let uievent = UIEvent::new( |
| self, |
| DOMString::from("resize"), |
| EventBubbles::DoesNotBubble, |
| EventCancelable::NotCancelable, |
| Some(self), |
| 0i32, |
| can_gc, |
| ); |
| uievent.upcast::<Event>().fire(self.upcast(), can_gc); |
| } |
| |
| true |
| } |
| |
| /// Evaluate media query lists and report changes |
| /// <https://drafts.csswg.org/cssom-view/#evaluate-media-queries-and-report-changes> |
| pub(crate) fn evaluate_media_queries_and_report_changes(&self, can_gc: CanGc) { |
| let _realm = enter_realm(self); |
| |
| rooted_vec!(let mut mql_list); |
| self.media_query_lists.for_each(|mql| { |
| if let MediaQueryListMatchState::Changed = mql.evaluate_changes() { |
| // Recording list of changed Media Queries |
| mql_list.push(Dom::from_ref(&*mql)); |
| } |
| }); |
| // Sending change events for all changed Media Queries |
| for mql in mql_list.iter() { |
| let event = MediaQueryListEvent::new( |
| &mql.global(), |
| atom!("change"), |
| false, |
| false, |
| mql.Media(), |
| mql.Matches(), |
| can_gc, |
| ); |
| event |
| .upcast::<Event>() |
| .fire(mql.upcast::<EventTarget>(), can_gc); |
| } |
| } |
| |
| /// Set whether to use less resources by running timers at a heavily limited rate. |
| pub(crate) fn set_throttled(&self, throttled: bool) { |
| self.throttled.set(throttled); |
| if throttled { |
| self.as_global_scope().slow_down_timers(); |
| } else { |
| self.as_global_scope().speed_up_timers(); |
| } |
| } |
| |
| pub(crate) fn throttled(&self) -> bool { |
| self.throttled.get() |
| } |
| |
| pub(crate) fn unminified_css_dir(&self) -> Option<String> { |
| self.unminified_css_dir.borrow().clone() |
| } |
| |
| pub(crate) fn local_script_source(&self) -> &Option<String> { |
| &self.local_script_source |
| } |
| |
| pub(crate) fn set_navigation_start(&self) { |
| self.navigation_start.set(CrossProcessInstant::now()); |
| } |
| |
| pub(crate) fn send_to_embedder(&self, msg: EmbedderMsg) { |
| self.as_global_scope() |
| .script_to_embedder_chan() |
| .send(msg) |
| .unwrap(); |
| } |
| |
| pub(crate) fn send_to_constellation(&self, msg: ScriptToConstellationMessage) { |
| self.as_global_scope() |
| .script_to_constellation_chan() |
| .send(msg) |
| .unwrap(); |
| } |
| |
| #[cfg(feature = "webxr")] |
| pub(crate) fn in_immersive_xr_session(&self) -> bool { |
| self.navigator |
| .get() |
| .as_ref() |
| .and_then(|nav| nav.xr()) |
| .is_some_and(|xr| xr.pending_or_active_session()) |
| } |
| |
| #[cfg(not(feature = "webxr"))] |
| pub(crate) fn in_immersive_xr_session(&self) -> bool { |
| false |
| } |
| |
| #[allow(unsafe_code)] |
| fn handle_pending_images_post_reflow( |
| &self, |
| pending_images: Vec<PendingImage>, |
| pending_rasterization_images: Vec<PendingRasterizationImage>, |
| pending_svg_element_for_serialization: Vec<UntrustedNodeAddress>, |
| ) { |
| let pipeline_id = self.pipeline_id(); |
| for image in pending_images { |
| let id = image.id; |
| let node = unsafe { from_untrusted_node_address(image.node) }; |
| |
| if let PendingImageState::Unrequested(ref url) = image.state { |
| fetch_image_for_layout(url.clone(), &node, id, self.image_cache.clone()); |
| } |
| |
| let mut images = self.pending_layout_images.borrow_mut(); |
| if !images.contains_key(&id) { |
| let trusted_node = Trusted::new(&*node); |
| let sender = self.register_image_cache_listener(id, move |response| { |
| trusted_node |
| .root() |
| .owner_window() |
| .pending_layout_image_notification(response); |
| }); |
| |
| self.image_cache |
| .add_listener(ImageLoadListener::new(sender, pipeline_id, id)); |
| } |
| |
| let nodes = images.entry(id).or_default(); |
| if !nodes.iter().any(|n| std::ptr::eq(&**n, &*node)) { |
| nodes.push(Dom::from_ref(&*node)); |
| } |
| } |
| |
| for image in pending_rasterization_images { |
| let node = unsafe { from_untrusted_node_address(image.node) }; |
| |
| let mut images = self.pending_images_for_rasterization.borrow_mut(); |
| if !images.contains_key(&(image.id, image.size)) { |
| self.image_cache.add_rasterization_complete_listener( |
| pipeline_id, |
| image.id, |
| image.size, |
| self.image_cache_sender.clone(), |
| ); |
| } |
| |
| let nodes = images.entry((image.id, image.size)).or_default(); |
| if !nodes.iter().any(|n| std::ptr::eq(&**n, &*node)) { |
| nodes.push(Dom::from_ref(&*node)); |
| } |
| } |
| |
| for node in pending_svg_element_for_serialization.into_iter() { |
| let node = unsafe { from_untrusted_node_address(node) }; |
| let svg = node.downcast::<SVGSVGElement>().unwrap(); |
| svg.serialize_and_cache_subtree(); |
| node.dirty(NodeDamage::Other); |
| } |
| } |
| } |
| |
| impl Window { |
| #[allow(unsafe_code)] |
| #[allow(clippy::too_many_arguments)] |
| pub(crate) fn new( |
| webview_id: WebViewId, |
| runtime: Rc<Runtime>, |
| script_chan: Sender<MainThreadScriptMsg>, |
| layout: Box<dyn Layout>, |
| font_context: Arc<FontContext>, |
| image_cache_sender: IpcSender<ImageCacheResponseMessage>, |
| image_cache: Arc<dyn ImageCache>, |
| resource_threads: ResourceThreads, |
| #[cfg(feature = "bluetooth")] bluetooth_thread: IpcSender<BluetoothRequest>, |
| mem_profiler_chan: MemProfilerChan, |
| time_profiler_chan: TimeProfilerChan, |
| devtools_chan: Option<IpcSender<ScriptToDevtoolsControlMsg>>, |
| constellation_chan: ScriptToConstellationChan, |
| embedder_chan: ScriptToEmbedderChan, |
| control_chan: GenericSender<ScriptThreadMessage>, |
| pipeline_id: PipelineId, |
| parent_info: Option<PipelineId>, |
| viewport_details: ViewportDetails, |
| origin: MutableOrigin, |
| creation_url: ServoUrl, |
| top_level_creation_url: ServoUrl, |
| navigation_start: CrossProcessInstant, |
| webgl_chan: Option<WebGLChan>, |
| #[cfg(feature = "webxr")] webxr_registry: Option<webxr_api::Registry>, |
| microtask_queue: Rc<MicrotaskQueue>, |
| compositor_api: CrossProcessCompositorApi, |
| unminify_js: bool, |
| unminify_css: bool, |
| local_script_source: Option<String>, |
| user_content_manager: UserContentManager, |
| player_context: WindowGLContext, |
| #[cfg(feature = "webgpu")] gpu_id_hub: Arc<IdentityHub>, |
| inherited_secure_context: Option<bool>, |
| theme: Theme, |
| ) -> DomRoot<Self> { |
| let error_reporter = CSSErrorReporter { |
| pipelineid: pipeline_id, |
| script_chan: control_chan, |
| }; |
| |
| let initial_viewport = f32_rect_to_au_rect(UntypedRect::new( |
| Point2D::zero(), |
| viewport_details.size.to_untyped(), |
| )); |
| |
| let win = Box::new(Self { |
| webview_id, |
| globalscope: GlobalScope::new_inherited( |
| pipeline_id, |
| devtools_chan, |
| mem_profiler_chan, |
| time_profiler_chan, |
| constellation_chan, |
| embedder_chan, |
| resource_threads, |
| origin, |
| creation_url, |
| Some(top_level_creation_url), |
| microtask_queue, |
| #[cfg(feature = "webgpu")] |
| gpu_id_hub, |
| inherited_secure_context, |
| unminify_js, |
| Some(font_context.clone()), |
| ), |
| ongoing_navigation: Default::default(), |
| script_chan, |
| layout: RefCell::new(layout), |
| image_cache_sender, |
| image_cache, |
| navigator: Default::default(), |
| location: Default::default(), |
| history: Default::default(), |
| indexeddb: Default::default(), |
| custom_element_registry: Default::default(), |
| window_proxy: Default::default(), |
| document: Default::default(), |
| performance: Default::default(), |
| navigation_start: Cell::new(navigation_start), |
| screen: Default::default(), |
| session_storage: Default::default(), |
| local_storage: Default::default(), |
| status: DomRefCell::new(DOMString::new()), |
| parent_info, |
| dom_static: GlobalStaticData::new(), |
| js_runtime: DomRefCell::new(Some(runtime.clone())), |
| #[cfg(feature = "bluetooth")] |
| bluetooth_thread, |
| #[cfg(feature = "bluetooth")] |
| bluetooth_extra_permission_data: BluetoothExtraPermissionData::new(), |
| unhandled_resize_event: Default::default(), |
| viewport_details: Cell::new(viewport_details), |
| current_viewport_size: Cell::new(initial_viewport.to_untyped().size), |
| layout_blocker: Cell::new(LayoutBlocker::WaitingForParse), |
| current_state: Cell::new(WindowState::Alive), |
| devtools_marker_sender: Default::default(), |
| devtools_markers: Default::default(), |
| webdriver_script_chan: Default::default(), |
| webdriver_load_status_sender: Default::default(), |
| error_reporter, |
| media_query_lists: DOMTracker::new(), |
| #[cfg(feature = "bluetooth")] |
| test_runner: Default::default(), |
| webgl_chan, |
| #[cfg(feature = "webxr")] |
| webxr_registry, |
| pending_image_callbacks: Default::default(), |
| pending_layout_images: Default::default(), |
| pending_images_for_rasterization: Default::default(), |
| unminified_css_dir: Default::default(), |
| local_script_source, |
| test_worklet: Default::default(), |
| paint_worklet: Default::default(), |
| exists_mut_observer: Cell::new(false), |
| compositor_api, |
| has_sent_idle_message: Cell::new(false), |
| unminify_css, |
| user_content_manager, |
| player_context, |
| throttled: Cell::new(false), |
| layout_marker: DomRefCell::new(Rc::new(Cell::new(true))), |
| current_event: DomRefCell::new(None), |
| theme: Cell::new(theme), |
| trusted_types: Default::default(), |
| reporting_observer_list: Default::default(), |
| report_list: Default::default(), |
| endpoints_list: Default::default(), |
| }); |
| |
| WindowBinding::Wrap::<crate::DomTypeHolder>(GlobalScope::get_cx(), win) |
| } |
| |
| pub(crate) fn pipeline_id(&self) -> PipelineId { |
| self.as_global_scope().pipeline_id() |
| } |
| |
| /// Create a new cached instance of the given value. |
| pub(crate) fn cache_layout_value<T>(&self, value: T) -> LayoutValue<T> |
| where |
| T: Copy + MallocSizeOf, |
| { |
| LayoutValue::new(self.layout_marker.borrow().clone(), value) |
| } |
| } |
| |
| /// An instance of a value associated with a particular snapshot of layout. This stored |
| /// value can only be read as long as the associated layout marker that is considered |
| /// valid. It will automatically become unavailable when the next layout operation is |
| /// performed. |
| #[derive(MallocSizeOf)] |
| pub(crate) struct LayoutValue<T: MallocSizeOf> { |
| #[ignore_malloc_size_of = "Rc is hard"] |
| is_valid: Rc<Cell<bool>>, |
| value: T, |
| } |
| |
| #[allow(unsafe_code)] |
| unsafe impl<T: JSTraceable + MallocSizeOf> JSTraceable for LayoutValue<T> { |
| unsafe fn trace(&self, trc: *mut js::jsapi::JSTracer) { |
| unsafe { self.value.trace(trc) }; |
| } |
| } |
| |
| impl<T: Copy + MallocSizeOf> LayoutValue<T> { |
| fn new(marker: Rc<Cell<bool>>, value: T) -> Self { |
| LayoutValue { |
| is_valid: marker, |
| value, |
| } |
| } |
| |
| /// Retrieve the stored value if it is still valid. |
| pub(crate) fn get(&self) -> Result<T, ()> { |
| if self.is_valid.get() { |
| return Ok(self.value); |
| } |
| Err(()) |
| } |
| } |
| |
| fn should_move_clip_rect(clip_rect: UntypedRect<Au>, new_viewport: UntypedRect<f32>) -> bool { |
| let clip_rect = UntypedRect::new( |
| Point2D::new( |
| clip_rect.origin.x.to_f32_px(), |
| clip_rect.origin.y.to_f32_px(), |
| ), |
| Size2D::new( |
| clip_rect.size.width.to_f32_px(), |
| clip_rect.size.height.to_f32_px(), |
| ), |
| ); |
| |
| // We only need to move the clip rect if the viewport is getting near the edge of |
| // our preexisting clip rect. We use half of the size of the viewport as a heuristic |
| // for "close." |
| static VIEWPORT_SCROLL_MARGIN_SIZE: f32 = 0.5; |
| let viewport_scroll_margin = new_viewport.size * VIEWPORT_SCROLL_MARGIN_SIZE; |
| |
| (clip_rect.origin.x - new_viewport.origin.x).abs() <= viewport_scroll_margin.width || |
| (clip_rect.max_x() - new_viewport.max_x()).abs() <= viewport_scroll_margin.width || |
| (clip_rect.origin.y - new_viewport.origin.y).abs() <= viewport_scroll_margin.height || |
| (clip_rect.max_y() - new_viewport.max_y()).abs() <= viewport_scroll_margin.height |
| } |
| |
| impl Window { |
| // https://html.spec.whatwg.org/multipage/#dom-window-postmessage step 7. |
| pub(crate) fn post_message( |
| &self, |
| target_origin: Option<ImmutableOrigin>, |
| source_origin: ImmutableOrigin, |
| source: &WindowProxy, |
| data: StructuredSerializedData, |
| ) { |
| let this = Trusted::new(self); |
| let source = Trusted::new(source); |
| let task = task!(post_serialised_message: move || { |
| let this = this.root(); |
| let source = source.root(); |
| let document = this.Document(); |
| |
| // Step 7.1. |
| if let Some(ref target_origin) = target_origin { |
| if !target_origin.same_origin(document.origin()) { |
| return; |
| } |
| } |
| |
| // Steps 7.2.-7.5. |
| let cx = this.get_cx(); |
| let obj = this.reflector().get_jsobject(); |
| let _ac = JSAutoRealm::new(*cx, obj.get()); |
| rooted!(in(*cx) let mut message_clone = UndefinedValue()); |
| if let Ok(ports) = structuredclone::read(this.upcast(), data, message_clone.handle_mut()) { |
| // Step 7.6, 7.7 |
| MessageEvent::dispatch_jsval( |
| this.upcast(), |
| this.upcast(), |
| message_clone.handle(), |
| Some(&source_origin.ascii_serialization()), |
| Some(&*source), |
| ports, |
| CanGc::note() |
| ); |
| } else { |
| // Step 4, fire messageerror. |
| MessageEvent::dispatch_error( |
| this.upcast(), |
| this.upcast(), |
| CanGc::note() |
| ); |
| } |
| }); |
| // TODO(#12718): Use the "posted message task source". |
| self.as_global_scope() |
| .task_manager() |
| .dom_manipulation_task_source() |
| .queue(task); |
| } |
| } |
| |
| #[derive(MallocSizeOf)] |
| pub(crate) struct CSSErrorReporter { |
| pub(crate) pipelineid: PipelineId, |
| pub(crate) script_chan: GenericSender<ScriptThreadMessage>, |
| } |
| unsafe_no_jsmanaged_fields!(CSSErrorReporter); |
| |
| impl ParseErrorReporter for CSSErrorReporter { |
| fn report_error( |
| &self, |
| url: &UrlExtraData, |
| location: SourceLocation, |
| error: ContextualParseError, |
| ) { |
| if log_enabled!(log::Level::Info) { |
| info!( |
| "Url:\t{}\n{}:{} {}", |
| url.0.as_str(), |
| location.line, |
| location.column, |
| error |
| ) |
| } |
| |
| // TODO: report a real filename |
| let _ = self.script_chan.send(ScriptThreadMessage::ReportCSSError( |
| self.pipelineid, |
| url.0.to_string(), |
| location.line, |
| location.column, |
| error.to_string(), |
| )); |
| } |
| } |
| |
| fn is_named_element_with_name_attribute(elem: &Element) -> bool { |
| let type_ = match elem.upcast::<Node>().type_id() { |
| NodeTypeId::Element(ElementTypeId::HTMLElement(type_)) => type_, |
| _ => return false, |
| }; |
| matches!( |
| type_, |
| HTMLElementTypeId::HTMLEmbedElement | |
| HTMLElementTypeId::HTMLFormElement | |
| HTMLElementTypeId::HTMLImageElement | |
| HTMLElementTypeId::HTMLObjectElement |
| ) |
| } |
| |
| fn is_named_element_with_id_attribute(elem: &Element) -> bool { |
| elem.is_html_element() |
| } |
| |
| #[allow(unsafe_code)] |
| #[unsafe(no_mangle)] |
| /// Helper for interactive debugging sessions in lldb/gdb. |
| unsafe extern "C" fn dump_js_stack(cx: *mut RawJSContext) { |
| unsafe { |
| DumpJSStack(cx, true, false, false); |
| } |
| } |
| |
| impl WindowHelpers for Window { |
| fn create_named_properties_object( |
| cx: JSContext, |
| proto: HandleObject, |
| object: MutableHandleObject, |
| ) { |
| Self::create_named_properties_object(cx, proto, object) |
| } |
| } |