/* 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::Cow; use std::cell::{Cell, RefCell}; use std::cmp::Ordering; use std::collections::hash_map::Entry::{Occupied, Vacant}; use std::collections::{HashMap, HashSet, VecDeque}; use std::default::Default; use std::f64::consts::PI; use std::mem; use std::rc::Rc; use std::slice::from_ref; use std::sync::{LazyLock, Mutex}; use std::time::{Duration, Instant}; use base::cross_process_instant::CrossProcessInstant; use base::id::WebViewId; use canvas_traits::canvas::CanvasId; use canvas_traits::webgl::{self, WebGLContextId, WebGLMsg}; use chrono::Local; use constellation_traits::{AnimationTickType, ScriptToConstellationMessage}; use content_security_policy::{self as csp, CspList, PolicyDisposition}; use cookie::Cookie; use cssparser::match_ignore_ascii_case; use devtools_traits::ScriptToDevtoolsControlMsg; use dom_struct::dom_struct; use embedder_traits::{ AllowOrDeny, AnimationState, CompositorHitTestResult, ContextMenuResult, EditingActionEvent, EmbedderMsg, ImeEvent, InputEvent, LoadStatus, MouseButton, MouseButtonAction, MouseButtonEvent, TouchEvent, TouchEventType, TouchId, WheelEvent, }; use encoding_rs::{Encoding, UTF_8}; use euclid::default::{Point2D, Rect, Size2D}; use html5ever::{LocalName, Namespace, QualName, local_name, namespace_url, ns}; use hyper_serde::Serde; use ipc_channel::ipc; use js::rust::{HandleObject, HandleValue}; use keyboard_types::{Code, Key, KeyState, Modifiers}; use metrics::{InteractiveFlag, InteractiveWindow, ProgressiveWebMetrics}; use mime::{self, Mime}; use net_traits::CookieSource::NonHTTP; use net_traits::CoreResourceMsg::{GetCookiesForUrl, SetCookiesForUrl}; use net_traits::policy_container::PolicyContainer; use net_traits::pub_domains::is_pub_domain; use net_traits::request::{InsecureRequestsPolicy, RequestBuilder}; use net_traits::response::HttpsState; use net_traits::{FetchResponseListener, IpcSend, ReferrerPolicy}; use num_traits::ToPrimitive; use percent_encoding::percent_decode; use profile_traits::ipc as profile_ipc; use profile_traits::time::TimerMetadataFrameType; use script_bindings::interfaces::DocumentHelpers; use script_layout_interface::{PendingRestyle, TrustedNodeAddress}; use script_traits::{ConstellationInputEvent, DocumentActivity, ProgressiveWebMetricType}; use servo_arc::Arc; use servo_config::pref; use servo_media::{ClientContextId, ServoMedia}; use servo_url::{ImmutableOrigin, MutableOrigin, ServoUrl}; use style::attr::AttrValue; use style::context::QuirksMode; use style::invalidation::element::restyle_hints::RestyleHint; use style::selector_parser::Snapshot; use style::shared_lock::SharedRwLock as StyleSharedRwLock; use style::str::{split_html_space_chars, str_join}; use style::stylesheet_set::DocumentStylesheetSet; use style::stylesheets::{Origin, OriginSet, Stylesheet}; use stylo_atoms::Atom; use url::Host; use uuid::Uuid; #[cfg(feature = "webgpu")] use webgpu_traits::WebGPUContextId; use webrender_api::units::DeviceIntRect; use crate::animation_timeline::AnimationTimeline; use crate::animations::Animations; use crate::canvas_context::CanvasContext as _; use crate::document_loader::{DocumentLoader, LoadType}; use crate::dom::attr::Attr; use crate::dom::beforeunloadevent::BeforeUnloadEvent; use crate::dom::bindings::callback::ExceptionHandling; use crate::dom::bindings::cell::{DomRefCell, Ref, RefMut}; use crate::dom::bindings::codegen::Bindings::BeforeUnloadEventBinding::BeforeUnloadEvent_Binding::BeforeUnloadEventMethods; use crate::dom::bindings::codegen::Bindings::DocumentBinding::{ DocumentMethods, DocumentReadyState, DocumentVisibilityState, NamedPropertyValue, }; use crate::dom::bindings::codegen::Bindings::EventBinding::Event_Binding::EventMethods; use crate::dom::bindings::codegen::Bindings::HTMLIFrameElementBinding::HTMLIFrameElement_Binding::HTMLIFrameElementMethods; use crate::dom::bindings::codegen::Bindings::HTMLInputElementBinding::HTMLInputElementMethods; use crate::dom::bindings::codegen::Bindings::HTMLTextAreaElementBinding::HTMLTextAreaElementMethods; use crate::dom::bindings::codegen::Bindings::NavigatorBinding::Navigator_Binding::NavigatorMethods; use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods; use crate::dom::bindings::codegen::Bindings::NodeFilterBinding::NodeFilter; use crate::dom::bindings::codegen::Bindings::PerformanceBinding::PerformanceMethods; use crate::dom::bindings::codegen::Bindings::PermissionStatusBinding::PermissionName; use crate::dom::bindings::codegen::Bindings::ShadowRootBinding::ShadowRootMethods; use crate::dom::bindings::codegen::Bindings::TouchBinding::TouchMethods; use crate::dom::bindings::codegen::Bindings::WindowBinding::{ FrameRequestCallback, ScrollBehavior, WindowMethods, }; use crate::dom::bindings::codegen::Bindings::XPathEvaluatorBinding::XPathEvaluatorMethods; use crate::dom::bindings::codegen::Bindings::XPathNSResolverBinding::XPathNSResolver; use crate::dom::bindings::codegen::UnionTypes::{NodeOrString, StringOrElementCreationOptions}; use crate::dom::bindings::error::{Error, ErrorInfo, ErrorResult, Fallible}; use crate::dom::bindings::inheritance::{Castable, ElementTypeId, HTMLElementTypeId, NodeTypeId}; use crate::dom::bindings::num::Finite; use crate::dom::bindings::refcounted::{Trusted, TrustedPromise}; use crate::dom::bindings::reflector::{DomGlobal, reflect_dom_object_with_proto}; use crate::dom::bindings::root::{Dom, DomRoot, DomSlice, LayoutDom, MutNullableDom, ToLayout}; use crate::dom::bindings::str::{DOMString, USVString}; use crate::dom::bindings::trace::{HashMapTracedValues, NoTrace}; #[cfg(feature = "webgpu")] use crate::dom::bindings::weakref::WeakRef; use crate::dom::bindings::xmlname::{ matches_name_production, namespace_from_domstring, validate_and_extract, }; use crate::dom::canvasrenderingcontext2d::CanvasRenderingContext2D; use crate::dom::cdatasection::CDATASection; use crate::dom::clipboardevent::{ClipboardEvent, ClipboardEventType}; use crate::dom::comment::Comment; use crate::dom::compositionevent::CompositionEvent; use crate::dom::cssstylesheet::CSSStyleSheet; use crate::dom::customelementregistry::CustomElementDefinition; use crate::dom::customevent::CustomEvent; use crate::dom::datatransfer::DataTransfer; use crate::dom::documentfragment::DocumentFragment; use crate::dom::documentorshadowroot::{DocumentOrShadowRoot, StyleSheetInDocument}; use crate::dom::documenttype::DocumentType; use crate::dom::domimplementation::DOMImplementation; use crate::dom::element::{ CustomElementCreationMode, Element, ElementCreator, ElementPerformFullscreenEnter, ElementPerformFullscreenExit, }; use crate::dom::event::{Event, EventBubbles, EventCancelable, EventDefault, EventStatus}; use crate::dom::eventtarget::EventTarget; use crate::dom::focusevent::FocusEvent; use crate::dom::fontfaceset::FontFaceSet; use crate::dom::globalscope::GlobalScope; use crate::dom::hashchangeevent::HashChangeEvent; use crate::dom::htmlanchorelement::HTMLAnchorElement; use crate::dom::htmlareaelement::HTMLAreaElement; use crate::dom::htmlbaseelement::HTMLBaseElement; use crate::dom::htmlbodyelement::HTMLBodyElement; use crate::dom::htmlcollection::{CollectionFilter, HTMLCollection}; use crate::dom::htmlelement::HTMLElement; use crate::dom::htmlembedelement::HTMLEmbedElement; use crate::dom::htmlformelement::{FormControl, FormControlElementHelpers, HTMLFormElement}; use crate::dom::htmlheadelement::HTMLHeadElement; use crate::dom::htmlhtmlelement::HTMLHtmlElement; use crate::dom::htmliframeelement::HTMLIFrameElement; use crate::dom::htmlimageelement::HTMLImageElement; use crate::dom::htmlinputelement::HTMLInputElement; use crate::dom::htmlmetaelement::RefreshRedirectDue; use crate::dom::htmlscriptelement::{HTMLScriptElement, ScriptResult}; use crate::dom::htmltextareaelement::HTMLTextAreaElement; use crate::dom::htmltitleelement::HTMLTitleElement; use crate::dom::intersectionobserver::IntersectionObserver; use crate::dom::keyboardevent::KeyboardEvent; use crate::dom::location::Location; use crate::dom::messageevent::MessageEvent; use crate::dom::mouseevent::MouseEvent; use crate::dom::node::{ self, CloneChildrenFlag, Node, NodeDamage, NodeFlags, NodeTraits, ShadowIncluding, }; use crate::dom::nodeiterator::NodeIterator; use crate::dom::nodelist::NodeList; use crate::dom::pagetransitionevent::PageTransitionEvent; use crate::dom::performanceentry::PerformanceEntry; use crate::dom::performancepainttiming::PerformancePaintTiming; use crate::dom::pointerevent::{PointerEvent, PointerId}; use crate::dom::processinginstruction::ProcessingInstruction; use crate::dom::promise::Promise; use crate::dom::range::Range; use crate::dom::resizeobserver::{ResizeObservationDepth, ResizeObserver}; use crate::dom::selection::Selection; use crate::dom::servoparser::ServoParser; use crate::dom::shadowroot::ShadowRoot; use crate::dom::storageevent::StorageEvent; use crate::dom::stylesheetlist::{StyleSheetList, StyleSheetListOwner}; use crate::dom::text::Text; use crate::dom::touch::Touch; use crate::dom::touchevent::TouchEvent as DomTouchEvent; use crate::dom::touchlist::TouchList; use crate::dom::treewalker::TreeWalker; use crate::dom::types::VisibilityStateEntry; use crate::dom::uievent::UIEvent; use crate::dom::virtualmethods::vtable_for; use crate::dom::webglrenderingcontext::WebGLRenderingContext; #[cfg(feature = "webgpu")] use crate::dom::webgpu::gpucanvascontext::GPUCanvasContext; use crate::dom::wheelevent::WheelEvent as DomWheelEvent; use crate::dom::window::Window; use crate::dom::windowproxy::WindowProxy; use crate::dom::xpathevaluator::XPathEvaluator; use crate::drag_data_store::{DragDataStore, Kind, Mode}; use crate::fetch::FetchCanceller; use crate::iframe_collection::IFrameCollection; use crate::image_animation::ImageAnimationManager; use crate::messaging::{CommonScriptMsg, MainThreadScriptMsg}; use crate::network_listener::{NetworkListener, PreInvoke}; use crate::realms::{AlreadyInRealm, InRealm, enter_realm}; use crate::script_runtime::{CanGc, ScriptThreadEventCategory}; use crate::script_thread::{ScriptThread, with_script_thread}; use crate::stylesheet_set::StylesheetSetRef; use crate::task::TaskBox; use crate::task_source::TaskSourceName; use crate::timers::OneshotTimerCallback; /// The number of times we are allowed to see spurious `requestAnimationFrame()` calls before /// falling back to fake ones. /// /// A spurious `requestAnimationFrame()` call is defined as one that does not change the DOM. const SPURIOUS_ANIMATION_FRAME_THRESHOLD: u8 = 5; /// The amount of time between fake `requestAnimationFrame()`s. const FAKE_REQUEST_ANIMATION_FRAME_DELAY: u64 = 16; pub(crate) enum TouchEventResult { Processed(bool), Forwarded, } #[derive(Clone, Copy, PartialEq)] pub(crate) enum FireMouseEventType { Move, Over, Out, Enter, Leave, } impl FireMouseEventType { pub(crate) fn as_str(&self) -> &str { match *self { FireMouseEventType::Move => "mousemove", FireMouseEventType::Over => "mouseover", FireMouseEventType::Out => "mouseout", FireMouseEventType::Enter => "mouseenter", FireMouseEventType::Leave => "mouseleave", } } } #[derive(Clone, Copy, Debug, JSTraceable, MallocSizeOf, PartialEq)] pub(crate) enum IsHTMLDocument { HTMLDocument, NonHTMLDocument, } #[derive(JSTraceable, MallocSizeOf)] #[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)] enum FocusTransaction { /// No focus operation is in effect. NotInTransaction, /// A focus operation is in effect. /// Contains the element that has most recently requested focus for itself. InTransaction(Option>), } /// Information about a declarative refresh #[derive(JSTraceable, MallocSizeOf)] pub(crate) enum DeclarativeRefresh { PendingLoad { #[no_trace] url: ServoUrl, time: u64, }, CreatedAfterLoad, } #[cfg(feature = "webgpu")] pub(crate) type WebGPUContextsMap = Rc>>>; /// #[dom_struct] pub(crate) struct Document { node: Node, document_or_shadow_root: DocumentOrShadowRoot, window: Dom, implementation: MutNullableDom, #[ignore_malloc_size_of = "type from external crate"] #[no_trace] content_type: Mime, last_modified: Option, #[no_trace] encoding: Cell<&'static Encoding>, has_browsing_context: bool, is_html_document: bool, #[no_trace] activity: Cell, #[no_trace] url: DomRefCell, #[ignore_malloc_size_of = "defined in selectors"] #[no_trace] quirks_mode: Cell, /// Caches for the getElement methods id_map: DomRefCell>>>, name_map: DomRefCell>>>, tag_map: DomRefCell>>, tagns_map: DomRefCell>>, classes_map: DomRefCell, Dom>>, images: MutNullableDom, embeds: MutNullableDom, links: MutNullableDom, forms: MutNullableDom, scripts: MutNullableDom, anchors: MutNullableDom, applets: MutNullableDom, /// Information about the `` in this [`Document`]. iframes: RefCell, /// Lock use for style attributes and author-origin stylesheet objects in this document. /// Can be acquired once for accessing many objects. #[no_trace] style_shared_lock: StyleSharedRwLock, /// List of stylesheets associated with nodes in this document. |None| if the list needs to be refreshed. #[custom_trace] stylesheets: DomRefCell>, stylesheet_list: MutNullableDom, ready_state: Cell, /// Whether the DOMContentLoaded event has already been dispatched. domcontentloaded_dispatched: Cell, /// The state of this document's focus transaction. focus_transaction: DomRefCell, /// The element that currently has the document focus context. focused: MutNullableDom, /// The script element that is currently executing. current_script: MutNullableDom, /// pending_parsing_blocking_script: DomRefCell>, /// Number of stylesheets that block executing the next parser-inserted script script_blocking_stylesheets_count: Cell, /// deferred_scripts: PendingInOrderScriptVec, /// asap_in_order_scripts_list: PendingInOrderScriptVec, /// asap_scripts_set: DomRefCell>>, /// /// True if scripting is enabled for all scripts in this document scripting_enabled: bool, /// /// Current identifier of animation frame callback animation_frame_ident: Cell, /// /// List of animation frame callbacks animation_frame_list: DomRefCell)>>, /// Whether we're in the process of running animation callbacks. /// /// Tracking this is not necessary for correctness. Instead, it is an optimization to avoid /// sending needless `ChangeRunningAnimationsState` messages to the compositor. running_animation_callbacks: Cell, /// Tracks all outstanding loads related to this document. loader: DomRefCell, /// The current active HTML parser, to allow resuming after interruptions. current_parser: MutNullableDom, /// The cached first `base` element with an `href` attribute. base_element: MutNullableDom, /// This field is set to the document itself for inert documents. /// appropriate_template_contents_owner_document: MutNullableDom, /// Information on elements needing restyle to ship over to layout when the /// time comes. pending_restyles: DomRefCell, NoTrace>>, /// This flag will be true if the `Document` needs to be painted again /// during the next full layout attempt due to some external change such as /// the web view changing size, or because the previous layout was only for /// layout queries (which do not trigger display). needs_paint: Cell, /// active_touch_points: DomRefCell>>, /// Navigation Timing properties: /// #[no_trace] dom_interactive: Cell>, #[no_trace] dom_content_loaded_event_start: Cell>, #[no_trace] dom_content_loaded_event_end: Cell>, #[no_trace] dom_complete: Cell>, #[no_trace] top_level_dom_complete: Cell>, #[no_trace] load_event_start: Cell>, #[no_trace] load_event_end: Cell>, #[no_trace] unload_event_start: Cell>, #[no_trace] unload_event_end: Cell>, /// #[no_trace] https_state: Cell, /// The document's origin. #[no_trace] origin: MutableOrigin, /// referrer: Option, /// target_element: MutNullableDom, /// #[no_trace] policy_container: DomRefCell, /// #[ignore_malloc_size_of = "Defined in std"] #[no_trace] last_click_info: DomRefCell)>>, /// ignore_destructive_writes_counter: Cell, /// ignore_opens_during_unload_counter: Cell, /// The number of spurious `requestAnimationFrame()` requests we've received. /// /// A rAF request is considered spurious if nothing was actually reflowed. spurious_animation_frames: Cell, /// Track the total number of elements in this DOM's tree. /// This is sent to layout every time a reflow is done; /// layout uses this to determine if the gains from parallel layout will be worth the overhead. /// /// See also: dom_count: Cell, /// Entry node for fullscreen. fullscreen_element: MutNullableDom, /// Map from ID to set of form control elements that have that ID as /// their 'form' content attribute. Used to reset form controls /// whenever any element with the same ID as the form attribute /// is inserted or removed from the document. /// See form_id_listener_map: DomRefCell>>>, #[no_trace] interactive_time: DomRefCell, #[no_trace] tti_window: DomRefCell, /// RAII canceller for Fetch canceller: FetchCanceller, /// throw_on_dynamic_markup_insertion_counter: Cell, /// page_showing: Cell, /// Whether the document is salvageable. salvageable: Cell, /// Whether the document was aborted with an active parser active_parser_was_aborted: Cell, /// Whether the unload event has already been fired. fired_unload: Cell, /// List of responsive images responsive_images: DomRefCell>>, /// Number of redirects for the document load redirect_count: Cell, /// Number of outstanding requests to prevent JS or layout from running. script_and_layout_blockers: Cell, /// List of tasks to execute as soon as last script/layout blocker is removed. #[ignore_malloc_size_of = "Measuring trait objects is hard"] delayed_tasks: DomRefCell>>, /// completely_loaded: Cell, /// Set of shadow roots connected to the document tree. shadow_roots: DomRefCell>>, /// Whether any of the shadow roots need the stylesheets flushed. shadow_roots_styles_changed: Cell, /// List of registered media controls. /// We need to keep this list to allow the media controls to /// access the "privileged" document.servoGetMediaControls(id) API, /// where `id` needs to match any of the registered ShadowRoots /// hosting the media controls UI. media_controls: DomRefCell>>, /// List of all context 2d IDs that need flushing. dirty_2d_contexts: DomRefCell>>, /// List of all WebGL context IDs that need flushing. dirty_webgl_contexts: DomRefCell>>, /// List of all WebGPU contexts. #[cfg(feature = "webgpu")] #[ignore_malloc_size_of = "Rc are hard"] webgpu_contexts: WebGPUContextsMap, /// selection: MutNullableDom, /// A timeline for animations which is used for synchronizing animations. /// animation_timeline: DomRefCell, /// Animations for this Document animations: DomRefCell, /// Image Animation Manager for this Document image_animation_manager: DomRefCell, /// The nearest inclusive ancestors to all the nodes that require a restyle. dirty_root: MutNullableDom, /// declarative_refresh: DomRefCell>, /// Pending input events, to be handled at the next rendering opportunity. #[no_trace] #[ignore_malloc_size_of = "CompositorEvent contains data from outside crates"] pending_input_events: DomRefCell>, /// The index of the last mouse move event in the pending compositor events queue. mouse_move_event_index: DomRefCell>, /// Pending animation ticks, to be handled at the next rendering opportunity. #[no_trace] #[ignore_malloc_size_of = "AnimationTickType contains data from an outside crate"] pending_animation_ticks: DomRefCell, /// /// /// Note: we are storing, but never removing, resize observers. /// The lifetime of resize observers is specified at /// . /// But implementing it comes with known problems: /// - /// - resize_observers: DomRefCell>>, /// The set of all fonts loaded by this document. /// fonts: MutNullableDom, /// visibility_state: Cell, /// status_code: Option, /// is_initial_about_blank: Cell, /// allow_declarative_shadow_roots: Cell, /// #[no_trace] inherited_insecure_requests_policy: Cell>, //// has_trustworthy_ancestor_origin: Cell, /// intersection_observer_task_queued: Cell, /// Active intersection observers that should be processed by this document in /// the update intersection observation steps. /// /// > Let observer list be a list of all IntersectionObservers whose root is in the DOM tree of document. /// > For the top-level browsing context, this includes implicit root observers. /// /// Details of which document that should process an observers is discussed further at /// . /// /// The lifetime of an intersection observer is specified at /// . intersection_observers: DomRefCell>>, /// The active keyboard modifiers for the WebView. This is updated when receiving any input event. #[no_trace] active_keyboard_modifiers: Cell, } #[allow(non_snake_case)] impl Document { pub(crate) fn note_node_with_dirty_descendants(&self, node: &Node) { debug_assert!(*node.owner_doc() == *self); if !node.is_connected() { return; } let parent = match node.parent_in_flat_tree() { Some(parent) => parent, None => { // There is no parent so this is the Document node, so we // behave as if we were called with the document element. let document_element = match self.GetDocumentElement() { Some(element) => element, None => return, }; if let Some(dirty_root) = self.dirty_root.get() { // There was an existing dirty root so we mark its // ancestors as dirty until the document element. for ancestor in dirty_root .upcast::() .inclusive_ancestors_in_flat_tree() { if ancestor.is::() { ancestor.set_flag(NodeFlags::HAS_DIRTY_DESCENDANTS, true); } } } self.dirty_root.set(Some(&document_element)); return; }, }; if parent.is::() { if !parent.is_styled() { return; } if parent.is_display_none() { return; } } let element_parent: DomRoot; let element = match node.downcast::() { Some(element) => element, None => { // Current node is not an element, it's probably a text node, // we try to get its element parent. match DomRoot::downcast::(parent) { Some(parent) => { element_parent = parent; &element_parent }, None => { // Parent is not an element so it must be a document, // and this is not an element either, so there is // nothing to do. return; }, } }, }; let dirty_root = match self.dirty_root.get() { None => { element .upcast::() .set_flag(NodeFlags::HAS_DIRTY_DESCENDANTS, true); self.dirty_root.set(Some(element)); return; }, Some(root) => root, }; for ancestor in element.upcast::().inclusive_ancestors_in_flat_tree() { if ancestor.get_flag(NodeFlags::HAS_DIRTY_DESCENDANTS) { return; } if ancestor.is::() { ancestor.set_flag(NodeFlags::HAS_DIRTY_DESCENDANTS, true); } } let new_dirty_root = element .upcast::() .common_ancestor_in_flat_tree(dirty_root.upcast()) .expect("Couldn't find common ancestor"); let mut has_dirty_descendants = true; for ancestor in dirty_root .upcast::() .inclusive_ancestors_in_flat_tree() { ancestor.set_flag(NodeFlags::HAS_DIRTY_DESCENDANTS, has_dirty_descendants); has_dirty_descendants &= *ancestor != *new_dirty_root; } let maybe_shadow_host = new_dirty_root .downcast::() .map(ShadowRootMethods::Host); let new_dirty_root_element = new_dirty_root .downcast::() .or(maybe_shadow_host.as_deref()); self.dirty_root.set(new_dirty_root_element); } pub(crate) fn take_dirty_root(&self) -> Option> { self.dirty_root.take() } #[inline] pub(crate) fn loader(&self) -> Ref { self.loader.borrow() } #[inline] pub(crate) fn loader_mut(&self) -> RefMut { self.loader.borrow_mut() } #[inline] pub(crate) fn has_browsing_context(&self) -> bool { self.has_browsing_context } /// #[inline] pub(crate) fn browsing_context(&self) -> Option> { if self.has_browsing_context { self.window.undiscarded_window_proxy() } else { None } } pub(crate) fn webview_id(&self) -> WebViewId { self.window.webview_id() } #[inline] pub(crate) fn window(&self) -> &Window { &self.window } #[inline] pub(crate) fn is_html_document(&self) -> bool { self.is_html_document } pub(crate) fn is_xhtml_document(&self) -> bool { self.content_type.type_() == mime::APPLICATION && self.content_type.subtype().as_str() == "xhtml" && self.content_type.suffix() == Some(mime::XML) } pub(crate) fn set_https_state(&self, https_state: HttpsState) { self.https_state.set(https_state); } pub(crate) fn is_fully_active(&self) -> bool { self.activity.get() == DocumentActivity::FullyActive } pub(crate) fn is_active(&self) -> bool { self.activity.get() != DocumentActivity::Inactive } pub(crate) fn set_activity(&self, activity: DocumentActivity, can_gc: CanGc) { // This function should only be called on documents with a browsing context assert!(self.has_browsing_context); if activity == self.activity.get() { return; } // Set the document's activity level, reflow if necessary, and suspend or resume timers. self.activity.set(activity); let media = ServoMedia::get(); let pipeline_id = self.window().pipeline_id(); let client_context_id = ClientContextId::build(pipeline_id.namespace_id.0, pipeline_id.index.0.get()); if activity != DocumentActivity::FullyActive { self.window().suspend(can_gc); media.suspend(&client_context_id); return; } self.title_changed(); self.dirty_all_nodes(); self.window().resume(can_gc); media.resume(&client_context_id); if self.ready_state.get() != DocumentReadyState::Complete { return; } // This step used to be Step 4.6 in html.spec.whatwg.org/multipage/#history-traversal // But it's now Step 4 in https://html.spec.whatwg.org/multipage/#reactivate-a-document // TODO: See #32687 for more information. let document = Trusted::new(self); self.owner_global() .task_manager() .dom_manipulation_task_source() .queue(task!(fire_pageshow_event: move || { let document = document.root(); let window = document.window(); // Step 4.6.1 if document.page_showing.get() { return; } // Step 4.6.2 Set document's page showing flag to true. document.page_showing.set(true); // Step 4.6.3 Update the visibility state of document to "visible". document.update_visibility_state(DocumentVisibilityState::Visible, CanGc::note()); // Step 4.6.4 Fire a page transition event named pageshow at document's relevant // global object with true. let event = PageTransitionEvent::new( window, atom!("pageshow"), false, // bubbles false, // cancelable true, // persisted CanGc::note(), ); let event = event.upcast::(); event.set_trusted(true); window.dispatch_event_with_target_override(event, CanGc::note()); })) } pub(crate) fn origin(&self) -> &MutableOrigin { &self.origin } /// pub(crate) fn url(&self) -> ServoUrl { self.url.borrow().clone() } pub(crate) fn set_url(&self, url: ServoUrl) { *self.url.borrow_mut() = url; } /// pub(crate) fn fallback_base_url(&self) -> ServoUrl { let document_url = self.url(); if let Some(browsing_context) = self.browsing_context() { // Step 1: If document is an iframe srcdoc document, then return the // document base URL of document's browsing context's container document. let container_base_url = browsing_context .parent() .and_then(|parent| parent.document()) .map(|document| document.base_url()); if document_url.as_str() == "about:srcdoc" { if let Some(base_url) = container_base_url { return base_url; } } // Step 2: If document's URL is about:blank, and document's browsing // context's creator base URL is non-null, then return that creator base URL. if document_url.as_str() == "about:blank" && browsing_context.has_creator_base_url() { return browsing_context.creator_base_url().unwrap(); } } // Step 3: Return document's URL. document_url } /// pub(crate) fn base_url(&self) -> ServoUrl { match self.base_element() { // Step 1. None => self.fallback_base_url(), // Step 2. Some(base) => base.frozen_base_url(), } } pub(crate) fn set_needs_paint(&self, value: bool) { self.needs_paint.set(value) } pub(crate) fn needs_reflow(&self) -> Option { // FIXME: This should check the dirty bit on the document, // not the document element. Needs some layout changes to make // that workable. if self.stylesheets.borrow().has_changed() { return Some(ReflowTriggerCondition::StylesheetsChanged); } let root = self.GetDocumentElement()?; if root.upcast::().has_dirty_descendants() { return Some(ReflowTriggerCondition::DirtyDescendants); } if !self.pending_restyles.borrow().is_empty() { return Some(ReflowTriggerCondition::PendingRestyles); } if self.needs_paint.get() { return Some(ReflowTriggerCondition::PaintPostponed); } None } /// Returns the first `base` element in the DOM that has an `href` attribute. pub(crate) fn base_element(&self) -> Option> { self.base_element.get() } /// Refresh the cached first base element in the DOM. /// pub(crate) fn refresh_base_element(&self) { let base = self .upcast::() .traverse_preorder(ShadowIncluding::No) .filter_map(DomRoot::downcast::) .find(|element| { element .upcast::() .has_attribute(&local_name!("href")) }); self.base_element.set(base.as_deref()); } pub(crate) fn dom_count(&self) -> u32 { self.dom_count.get() } /// This is called by `bind_to_tree` when a node is added to the DOM. /// The internal count is used by layout to determine whether to be sequential or parallel. /// (it's sequential for small DOMs) pub(crate) fn increment_dom_count(&self) { self.dom_count.set(self.dom_count.get() + 1); } /// This is called by `unbind_from_tree` when a node is removed from the DOM. pub(crate) fn decrement_dom_count(&self) { self.dom_count.set(self.dom_count.get() - 1); } pub(crate) fn quirks_mode(&self) -> QuirksMode { self.quirks_mode.get() } pub(crate) fn set_quirks_mode(&self, new_mode: QuirksMode) { let old_mode = self.quirks_mode.replace(new_mode); if old_mode != new_mode { self.window.layout_mut().set_quirks_mode(new_mode); } } pub(crate) fn encoding(&self) -> &'static Encoding { self.encoding.get() } pub(crate) fn set_encoding(&self, encoding: &'static Encoding) { self.encoding.set(encoding); } pub(crate) fn content_and_heritage_changed(&self, node: &Node) { if node.is_connected() { node.note_dirty_descendants(); } // FIXME(emilio): This is very inefficient, ideally the flag above would // be enough and incremental layout could figure out from there. node.dirty(NodeDamage::OtherNodeDamage); } /// Remove any existing association between the provided id and any elements in this document. pub(crate) fn unregister_element_id(&self, to_unregister: &Element, id: Atom, can_gc: CanGc) { self.document_or_shadow_root .unregister_named_element(&self.id_map, to_unregister, &id); self.reset_form_owner_for_listeners(&id, can_gc); } /// Associate an element present in this document with the provided id. pub(crate) fn register_element_id(&self, element: &Element, id: Atom, can_gc: CanGc) { let root = self.GetDocumentElement().expect( "The element is in the document, so there must be a document \ element.", ); self.document_or_shadow_root.register_named_element( &self.id_map, element, &id, DomRoot::from_ref(root.upcast::()), ); self.reset_form_owner_for_listeners(&id, can_gc); } /// Remove any existing association between the provided name and any elements in this document. pub(crate) fn unregister_element_name(&self, to_unregister: &Element, name: Atom) { self.document_or_shadow_root .unregister_named_element(&self.name_map, to_unregister, &name); } /// Associate an element present in this document with the provided name. pub(crate) fn register_element_name(&self, element: &Element, name: Atom) { let root = self.GetDocumentElement().expect( "The element is in the document, so there must be a document \ element.", ); self.document_or_shadow_root.register_named_element( &self.name_map, element, &name, DomRoot::from_ref(root.upcast::()), ); } pub(crate) fn register_form_id_listener( &self, id: DOMString, listener: &T, ) { let mut map = self.form_id_listener_map.borrow_mut(); let listener = listener.to_element(); let set = map.entry(Atom::from(id)).or_default(); set.insert(Dom::from_ref(listener)); } pub(crate) fn unregister_form_id_listener( &self, id: DOMString, listener: &T, ) { let mut map = self.form_id_listener_map.borrow_mut(); if let Occupied(mut entry) = map.entry(Atom::from(id)) { entry .get_mut() .remove(&Dom::from_ref(listener.to_element())); if entry.get().is_empty() { entry.remove(); } } } /// Attempt to find a named element in this page's document. /// pub(crate) fn find_fragment_node(&self, fragid: &str) -> Option> { // Step 1 is not handled here; the fragid is already obtained by the calling function // Step 2: Simply use None to indicate the top of the document. // Step 3 & 4 percent_decode(fragid.as_bytes()) .decode_utf8() .ok() // Step 5 .and_then(|decoded_fragid| self.get_element_by_id(&Atom::from(decoded_fragid))) // Step 6 .or_else(|| self.get_anchor_by_name(fragid)) // Step 7 & 8 } /// Scroll to the target element, and when we do not find a target /// and the fragment is empty or "top", scroll to the top. /// pub(crate) fn check_and_scroll_fragment(&self, fragment: &str, can_gc: CanGc) { let target = self.find_fragment_node(fragment); // Step 1 self.set_target_element(target.as_deref()); let point = target .as_ref() .map(|element| { // TODO: This strategy is completely wrong if the element we are scrolling to in // inside other scrollable containers. Ideally this should use an implementation of // `scrollIntoView` when that is available: // See https://github.com/servo/servo/issues/24059. let rect = element .upcast::() .bounding_content_box_or_zero(can_gc); // In order to align with element edges, we snap to unscaled pixel boundaries, since // the paint thread currently does the same for drawing elements. This is important // for pages that require pixel perfect scroll positioning for proper display // (like Acid2). let device_pixel_ratio = self.window.device_pixel_ratio().get(); ( rect.origin.x.to_nearest_pixel(device_pixel_ratio), rect.origin.y.to_nearest_pixel(device_pixel_ratio), ) }) .or_else(|| { if fragment.is_empty() || fragment.eq_ignore_ascii_case("top") { // FIXME(stshine): this should be the origin of the stacking context space, // which may differ under the influence of writing mode. Some((0.0, 0.0)) } else { None } }); if let Some((x, y)) = point { self.window .scroll(x as f64, y as f64, ScrollBehavior::Instant, can_gc) } } fn get_anchor_by_name(&self, name: &str) -> Option> { let name = Atom::from(name); self.name_map.borrow().get(&name).and_then(|elements| { elements .iter() .find(|e| e.is::()) .map(|e| DomRoot::from_ref(&**e)) }) } // https://html.spec.whatwg.org/multipage/#current-document-readiness pub(crate) fn set_ready_state(&self, state: DocumentReadyState, can_gc: CanGc) { match state { DocumentReadyState::Loading => { if self.window().is_top_level() { self.send_to_embedder(EmbedderMsg::NotifyLoadStatusChanged( self.webview_id(), LoadStatus::Started, )); self.send_to_embedder(EmbedderMsg::Status(self.webview_id(), None)); } }, DocumentReadyState::Complete => { if self.window().is_top_level() { self.send_to_embedder(EmbedderMsg::NotifyLoadStatusChanged( self.webview_id(), LoadStatus::Complete, )); } update_with_current_instant(&self.dom_complete); }, DocumentReadyState::Interactive => update_with_current_instant(&self.dom_interactive), }; self.ready_state.set(state); self.upcast::() .fire_event(atom!("readystatechange"), can_gc); } /// Return whether scripting is enabled or not pub(crate) fn is_scripting_enabled(&self) -> bool { self.scripting_enabled } /// Return the element that currently has focus. // https://w3c.github.io/uievents/#events-focusevent-doc-focus pub(crate) fn get_focused_element(&self) -> Option> { self.focused.get() } /// Initiate a new round of checking for elements requesting focus. The last element to call /// `request_focus` before `commit_focus_transaction` is called will receive focus. fn begin_focus_transaction(&self) { *self.focus_transaction.borrow_mut() = FocusTransaction::InTransaction(Default::default()); } /// pub(crate) fn perform_focus_fixup_rule(&self, not_focusable: &Element, can_gc: CanGc) { if Some(not_focusable) != self.focused.get().as_deref() { return; } self.request_focus( self.GetBody().as_ref().map(|e| e.upcast()), FocusType::Element, can_gc, ) } /// Request that the given element receive focus once the current transaction is complete. /// If None is passed, then whatever element is currently focused will no longer be focused /// once the transaction is complete. pub(crate) fn request_focus( &self, elem: Option<&Element>, focus_type: FocusType, can_gc: CanGc, ) { let implicit_transaction = matches!( *self.focus_transaction.borrow(), FocusTransaction::NotInTransaction ); if implicit_transaction { self.begin_focus_transaction(); } if elem.is_none_or(|e| e.is_focusable_area()) { *self.focus_transaction.borrow_mut() = FocusTransaction::InTransaction(elem.map(Dom::from_ref)); } if implicit_transaction { self.commit_focus_transaction(focus_type, can_gc); } } /// Reassign the focus context to the element that last requested focus during this /// transaction, or none if no elements requested it. fn commit_focus_transaction(&self, focus_type: FocusType, can_gc: CanGc) { let possibly_focused = match *self.focus_transaction.borrow() { FocusTransaction::NotInTransaction => unreachable!(), FocusTransaction::InTransaction(ref elem) => { elem.as_ref().map(|e| DomRoot::from_ref(&**e)) }, }; *self.focus_transaction.borrow_mut() = FocusTransaction::NotInTransaction; if self.focused == possibly_focused.as_deref() { return; } if let Some(ref elem) = self.focused.get() { let node = elem.upcast::(); elem.set_focus_state(false); // FIXME: pass appropriate relatedTarget self.fire_focus_event(FocusEventType::Blur, node, None, can_gc); // Notify the embedder to hide the input method. if elem.input_method_type().is_some() { self.send_to_embedder(EmbedderMsg::HideIME(self.webview_id())); } } self.focused.set(possibly_focused.as_deref()); if let Some(ref elem) = self.focused.get() { elem.set_focus_state(true); let node = elem.upcast::(); // FIXME: pass appropriate relatedTarget self.fire_focus_event(FocusEventType::Focus, node, None, can_gc); // Update the focus state for all elements in the focus chain. // https://html.spec.whatwg.org/multipage/#focus-chain if focus_type == FocusType::Element { self.window() .send_to_constellation(ScriptToConstellationMessage::Focus); } // Notify the embedder to display an input method. if let Some(kind) = elem.input_method_type() { let rect = elem.upcast::().bounding_content_box_or_zero(can_gc); let rect = Rect::new( Point2D::new(rect.origin.x.to_px(), rect.origin.y.to_px()), Size2D::new(rect.size.width.to_px(), rect.size.height.to_px()), ); let (text, multiline) = if let Some(input) = elem.downcast::() { ( Some(( input.Value().to_string(), input.GetSelectionEnd().unwrap_or(0) as i32, )), false, ) } else if let Some(textarea) = elem.downcast::() { ( Some(( textarea.Value().to_string(), textarea.GetSelectionEnd().unwrap_or(0) as i32, )), true, ) } else { (None, false) }; self.send_to_embedder(EmbedderMsg::ShowIME( self.webview_id(), kind, text, multiline, DeviceIntRect::from_untyped(&rect.to_box2d()), )); } } } /// Handles any updates when the document's title has changed. pub(crate) fn title_changed(&self) { if self.browsing_context().is_some() { self.send_title_to_embedder(); let title = String::from(self.Title()); self.window .send_to_constellation(ScriptToConstellationMessage::TitleChanged( self.window.pipeline_id(), title.clone(), )); if let Some(chan) = self.window.as_global_scope().devtools_chan() { let _ = chan.send(ScriptToDevtoolsControlMsg::TitleChanged( self.window.pipeline_id(), title, )); } } } /// Determine the title of the [`Document`] according to the specification at: /// . The difference /// here is that when the title isn't specified `None` is returned. fn title(&self) -> Option { let title = self.GetDocumentElement().and_then(|root| { if root.namespace() == &ns!(svg) && root.local_name() == &local_name!("svg") { // Step 1. root.upcast::() .child_elements() .find(|node| { node.namespace() == &ns!(svg) && node.local_name() == &local_name!("title") }) .map(DomRoot::upcast::) } else { // Step 2. root.upcast::() .traverse_preorder(ShadowIncluding::No) .find(|node| node.is::()) } }); title.map(|title| { // Steps 3-4. let value = title.child_text_content(); DOMString::from(str_join(split_html_space_chars(&value), " ")) }) } /// Sends this document's title to the constellation. pub(crate) fn send_title_to_embedder(&self) { let window = self.window(); if window.is_top_level() { let title = self.title().map(String::from); self.send_to_embedder(EmbedderMsg::ChangePageTitle(self.webview_id(), title)); } } pub(crate) fn send_to_embedder(&self, msg: EmbedderMsg) { let window = self.window(); window.send_to_embedder(msg); } pub(crate) fn dirty_all_nodes(&self) { let root = match self.GetDocumentElement() { Some(root) => root, None => return, }; for node in root .upcast::() .traverse_preorder(ShadowIncluding::Yes) { node.dirty(NodeDamage::OtherNodeDamage) } } #[allow(unsafe_code)] pub(crate) fn handle_mouse_button_event( &self, event: MouseButtonEvent, hit_test_result: Option, pressed_mouse_buttons: u16, can_gc: CanGc, ) { // Ignore all incoming events without a hit test. let Some(hit_test_result) = hit_test_result else { return; }; debug!( "{:?}: at {:?}", event.action, hit_test_result.point_in_viewport ); let node = unsafe { node::from_untrusted_node_address(hit_test_result.node) }; let Some(el) = node .inclusive_ancestors(ShadowIncluding::Yes) .filter_map(DomRoot::downcast::) .next() else { return; }; let node = el.upcast::(); debug!("{:?} on {:?}", event.action, node.debug_str()); // Prevent click event if form control element is disabled. if let MouseButtonAction::Click = event.action { // The click event is filtered by the disabled state. if el.is_actually_disabled() { return; } self.begin_focus_transaction(); self.request_focus(Some(&*el), FocusType::Element, can_gc); } let dom_event = DomRoot::upcast::(MouseEvent::for_platform_mouse_event( event, pressed_mouse_buttons, &self.window, &hit_test_result, can_gc, )); // https://html.spec.whatwg.org/multipage/#run-authentic-click-activation-steps let activatable = el.as_maybe_activatable(); match event.action { MouseButtonAction::Click => { el.set_click_in_progress(true); dom_event.fire(node.upcast(), can_gc); el.set_click_in_progress(false); }, MouseButtonAction::Down => { if let Some(a) = activatable { a.enter_formal_activation_state(); } let target = node.upcast(); dom_event.fire(target, can_gc); }, MouseButtonAction::Up => { if let Some(a) = activatable { a.exit_formal_activation_state(); } let target = node.upcast(); dom_event.fire(target, can_gc); }, } if let MouseButtonAction::Click = event.action { self.commit_focus_transaction(FocusType::Element, can_gc); self.maybe_fire_dblclick( hit_test_result.point_in_viewport, node, pressed_mouse_buttons, can_gc, ); } // When the contextmenu event is triggered by right mouse button // the contextmenu event MUST be dispatched after the mousedown event. if let (MouseButtonAction::Down, MouseButton::Right) = (event.action, event.button) { self.maybe_show_context_menu( node.upcast(), pressed_mouse_buttons, hit_test_result.point_in_viewport, can_gc, ); } } /// fn maybe_show_context_menu( &self, target: &EventTarget, pressed_mouse_buttons: u16, client_point: Point2D, can_gc: CanGc, ) { let client_x = client_point.x.to_i32().unwrap_or(0); let client_y = client_point.y.to_i32().unwrap_or(0); // let menu_event = PointerEvent::new( &self.window, // window DOMString::from("contextmenu"), // type EventBubbles::Bubbles, // can_bubble EventCancelable::Cancelable, // cancelable Some(&self.window), // view 0, // detail client_x, // screen_x client_y, // screen_y client_x, // client_x client_y, // client_y false, // ctrl_key false, // alt_key false, // shift_key false, // meta_key 2i16, // button, right mouse button pressed_mouse_buttons, // buttons None, // related_target None, // point_in_target PointerId::Mouse as i32, // pointer_id 1, // width 1, // height 0.5, // pressure 0.0, // tangential_pressure 0, // tilt_x 0, // tilt_y 0, // twist PI / 2.0, // altitude_angle 0.0, // azimuth_angle DOMString::from("mouse"), // pointer_type true, // is_primary vec![], // coalesced_events vec![], // predicted_events can_gc, ); let event = menu_event.upcast::(); event.fire(target, can_gc); // if the event was not canceled, notify the embedder to show the context menu if event.status() == EventStatus::NotCanceled { let (sender, receiver) = ipc::channel::().expect("Failed to create IPC channel."); self.send_to_embedder(EmbedderMsg::ShowContextMenu( self.webview_id(), sender, None, vec![], )); let _ = receiver.recv().unwrap(); }; } fn maybe_fire_dblclick( &self, click_pos: Point2D, target: &Node, pressed_mouse_buttons: u16, can_gc: CanGc, ) { // https://w3c.github.io/uievents/#event-type-dblclick let now = Instant::now(); let opt = self.last_click_info.borrow_mut().take(); if let Some((last_time, last_pos)) = opt { let DBL_CLICK_TIMEOUT = Duration::from_millis(pref!(dom_document_dblclick_timeout) as u64); let DBL_CLICK_DIST_THRESHOLD = pref!(dom_document_dblclick_dist) as u64; // Calculate distance between this click and the previous click. let line = click_pos - last_pos; let dist = (line.dot(line) as f64).sqrt(); if now.duration_since(last_time) < DBL_CLICK_TIMEOUT && dist < DBL_CLICK_DIST_THRESHOLD as f64 { // A double click has occurred if this click is within a certain time and dist. of previous click. let click_count = 2; let client_x = click_pos.x as i32; let client_y = click_pos.y as i32; let event = MouseEvent::new( &self.window, DOMString::from("dblclick"), EventBubbles::Bubbles, EventCancelable::Cancelable, Some(&self.window), click_count, client_x, client_y, client_x, client_y, false, false, false, false, 0i16, pressed_mouse_buttons, None, None, can_gc, ); event.upcast::().fire(target.upcast(), can_gc); // When a double click occurs, self.last_click_info is left as None so that a // third sequential click will not cause another double click. return; } } // Update last_click_info with the time and position of the click. *self.last_click_info.borrow_mut() = Some((now, click_pos)); } #[allow(clippy::too_many_arguments)] pub(crate) fn fire_mouse_event( &self, client_point: Point2D, target: &EventTarget, event_name: FireMouseEventType, can_bubble: EventBubbles, cancelable: EventCancelable, pressed_mouse_buttons: u16, can_gc: CanGc, ) { let client_x = client_point.x.to_i32().unwrap_or(0); let client_y = client_point.y.to_i32().unwrap_or(0); MouseEvent::new( &self.window, DOMString::from(event_name.as_str()), can_bubble, cancelable, Some(&self.window), 0i32, client_x, client_y, client_x, client_y, false, false, false, false, 0i16, pressed_mouse_buttons, None, None, can_gc, ) .upcast::() .fire(target, can_gc); } pub(crate) fn handle_editing_action(&self, action: EditingActionEvent, can_gc: CanGc) -> bool { let clipboard_event = match action { EditingActionEvent::Copy => ClipboardEventType::Copy, EditingActionEvent::Cut => ClipboardEventType::Cut, EditingActionEvent::Paste => ClipboardEventType::Paste, }; self.handle_clipboard_action(clipboard_event, can_gc) } /// fn handle_clipboard_action(&self, action: ClipboardEventType, can_gc: CanGc) -> bool { // The script_triggered flag is set if the action runs because of a script, e.g. document.execCommand() let script_triggered = false; // The script_may_access_clipboard flag is set // if action is paste and the script thread is allowed to read from clipboard or // if action is copy or cut and the script thread is allowed to modify the clipboard let script_may_access_clipboard = false; // Step 1 If the script-triggered flag is set and the script-may-access-clipboard flag is unset if script_triggered && !script_may_access_clipboard { return false; } // Step 2 Fire a clipboard event let event = ClipboardEvent::new( &self.window, None, DOMString::from(action.as_str()), EventBubbles::Bubbles, EventCancelable::Cancelable, None, can_gc, ); self.fire_clipboard_event(&event, action, can_gc); // Step 3 If a script doesn't call preventDefault() // the event will be handled inside target's VirtualMethods::handle_event let e = event.upcast::(); if !e.IsTrusted() { return false; } // Step 4 If the event was canceled, then if e.DefaultPrevented() { match e.Type().str() { "copy" => { // Step 4.1 Call the write content to the clipboard algorithm, // passing on the DataTransferItemList items, a clear-was-called flag and a types-to-clear list. if let Some(clipboard_data) = event.get_clipboard_data() { let drag_data_store = clipboard_data.data_store().expect("This shouldn't fail"); self.write_content_to_the_clipboard(&drag_data_store); } }, "cut" => { // Step 4.1 Call the write content to the clipboard algorithm, // passing on the DataTransferItemList items, a clear-was-called flag and a types-to-clear list. if let Some(clipboard_data) = event.get_clipboard_data() { let drag_data_store = clipboard_data.data_store().expect("This shouldn't fail"); self.write_content_to_the_clipboard(&drag_data_store); } // Step 4.2 Fire a clipboard event named clipboardchange self.fire_clipboardchange_event(can_gc); }, "paste" => return false, _ => (), } } //Step 5 true } /// fn fire_clipboard_event( &self, event: &ClipboardEvent, action: ClipboardEventType, can_gc: CanGc, ) { // Step 1 Let clear_was_called be false // Step 2 Let types_to_clear an empty list let mut drag_data_store = DragDataStore::new(); // Step 4 let clipboard-entry be the sequence number of clipboard content, null if the OS doesn't support it. // Step 5 let trusted be true if the event is generated by the user agent, false otherwise let trusted = true; // Step 6 if the context is editable: let focused = self.get_focused_element(); let body = self.GetBody(); let target = match (&focused, &body) { (Some(focused), _) => focused.upcast(), (&None, Some(body)) => body.upcast(), (&None, &None) => self.window.upcast(), }; // Step 6.2 else TODO require Selection see https://github.com/w3c/clipboard-apis/issues/70 // Step 7 match action { ClipboardEventType::Copy | ClipboardEventType::Cut => { // Step 7.2.1 drag_data_store.set_mode(Mode::ReadWrite); }, ClipboardEventType::Paste => { let (sender, receiver) = ipc::channel().unwrap(); self.window .send_to_constellation(ScriptToConstellationMessage::ForwardToEmbedder( EmbedderMsg::GetClipboardText(self.window.webview_id(), sender), )); let text_contents = receiver .recv() .map(Result::unwrap_or_default) .unwrap_or_default(); // Step 7.1.1 drag_data_store.set_mode(Mode::ReadOnly); // Step 7.1.2 If trusted or the implementation gives script-generated events access to the clipboard if trusted { // Step 7.1.2.1 For each clipboard-part on the OS clipboard: // Step 7.1.2.1.1 If clipboard-part contains plain text, then let data = DOMString::from(text_contents.to_string()); let type_ = DOMString::from("text/plain"); let _ = drag_data_store.add(Kind::Text { data, type_ }); // Step 7.1.2.1.2 TODO If clipboard-part represents file references, then for each file reference // Step 7.1.2.1.3 TODO If clipboard-part contains HTML- or XHTML-formatted text then // Step 7.1.3 Update clipboard-event-data’s files to match clipboard-event-data’s items // Step 7.1.4 Update clipboard-event-data’s types to match clipboard-event-data’s items } }, ClipboardEventType::Change => (), } // Step 3 let clipboard_event_data = DataTransfer::new( &self.window, Rc::new(RefCell::new(Some(drag_data_store))), can_gc, ); // Step 8 event.set_clipboard_data(Some(&clipboard_event_data)); let event = event.upcast::(); // Step 9 event.set_trusted(trusted); // Step 10 Set event’s composed to true. event.set_composed(true); // Step 11 event.dispatch(target, false, can_gc); } pub(crate) fn fire_clipboardchange_event(&self, can_gc: CanGc) { let clipboardchange_event = ClipboardEvent::new( &self.window, None, DOMString::from("clipboardchange"), EventBubbles::Bubbles, EventCancelable::Cancelable, None, can_gc, ); self.fire_clipboard_event(&clipboardchange_event, ClipboardEventType::Change, can_gc); } /// fn write_content_to_the_clipboard(&self, drag_data_store: &DragDataStore) { // Step 1 if drag_data_store.list_len() > 0 { // Step 1.1 Clear the clipboard. self.send_to_embedder(EmbedderMsg::ClearClipboard(self.webview_id())); // Step 1.2 for item in drag_data_store.iter_item_list() { match item { Kind::Text { data, .. } => { // Step 1.2.1.1 Ensure encoding is correct per OS and locale conventions // Step 1.2.1.2 Normalize line endings according to platform conventions // Step 1.2.1.3 self.send_to_embedder(EmbedderMsg::SetClipboardText( self.webview_id(), data.to_string(), )); }, Kind::File { .. } => { // Step 1.2.2 If data is of a type listed in the mandatory data types list, then // Step 1.2.2.1 Place part on clipboard with the appropriate OS clipboard format description // Step 1.2.3 Else this is left to the implementation }, } } } else { // Step 2.1 if drag_data_store.clear_was_called { // Step 2.1.1 If types-to-clear list is empty, clear the clipboard self.send_to_embedder(EmbedderMsg::ClearClipboard(self.webview_id())); // Step 2.1.2 Else remove the types in the list from the clipboard // As of now this can't be done with Arboard, and it's possible that will be removed from the spec } } } #[allow(unsafe_code)] pub(crate) unsafe fn handle_mouse_move_event( &self, hit_test_result: Option, pressed_mouse_buttons: u16, prev_mouse_over_target: &MutNullableDom, can_gc: CanGc, ) { // Ignore all incoming events without a hit test. let Some(hit_test_result) = hit_test_result else { return; }; let node = unsafe { node::from_untrusted_node_address(hit_test_result.node) }; let Some(new_target) = node .inclusive_ancestors(ShadowIncluding::No) .filter_map(DomRoot::downcast::) .next() else { return; }; let target_has_changed = prev_mouse_over_target .get() .as_ref() .is_none_or(|old_target| old_target != &new_target); // Here we know the target has changed, so we must update the state, // dispatch mouseout to the previous one, mouseover to the new one. if target_has_changed { // Dispatch mouseout and mouseleave to previous target. if let Some(old_target) = prev_mouse_over_target.get() { let old_target_is_ancestor_of_new_target = old_target .upcast::() .is_ancestor_of(new_target.upcast::()); // If the old target is an ancestor of the new target, this can be skipped // completely, since the node's hover state will be reset below. if !old_target_is_ancestor_of_new_target { for element in old_target .upcast::() .inclusive_ancestors(ShadowIncluding::No) .filter_map(DomRoot::downcast::) { element.set_hover_state(false); element.set_active_state(false); } } self.fire_mouse_event( hit_test_result.point_in_viewport, old_target.upcast(), FireMouseEventType::Out, EventBubbles::Bubbles, EventCancelable::Cancelable, pressed_mouse_buttons, can_gc, ); if !old_target_is_ancestor_of_new_target { let event_target = DomRoot::from_ref(old_target.upcast::()); let moving_into = Some(DomRoot::from_ref(new_target.upcast::())); self.handle_mouse_enter_leave_event( hit_test_result.point_in_viewport, FireMouseEventType::Leave, moving_into, event_target, pressed_mouse_buttons, can_gc, ); } } // Dispatch mouseover and mouseenter to new target. for element in new_target .upcast::() .inclusive_ancestors(ShadowIncluding::No) .filter_map(DomRoot::downcast::) { if element.hover_state() { break; } element.set_hover_state(true); } self.fire_mouse_event( hit_test_result.point_in_viewport, new_target.upcast(), FireMouseEventType::Over, EventBubbles::Bubbles, EventCancelable::Cancelable, pressed_mouse_buttons, can_gc, ); let moving_from = prev_mouse_over_target .get() .map(|old_target| DomRoot::from_ref(old_target.upcast::())); let event_target = DomRoot::from_ref(new_target.upcast::()); self.handle_mouse_enter_leave_event( hit_test_result.point_in_viewport, FireMouseEventType::Enter, moving_from, event_target, pressed_mouse_buttons, can_gc, ); } // Send mousemove event to topmost target, unless it's an iframe, in which case the // compositor should have also sent an event to the inner document. self.fire_mouse_event( hit_test_result.point_in_viewport, new_target.upcast(), FireMouseEventType::Move, EventBubbles::Bubbles, EventCancelable::Cancelable, pressed_mouse_buttons, can_gc, ); // If the target has changed then store the current mouse over target for next frame. if target_has_changed { prev_mouse_over_target.set(Some(&new_target)); } } fn handle_mouse_enter_leave_event( &self, client_point: Point2D, event_type: FireMouseEventType, related_target: Option>, event_target: DomRoot, pressed_mouse_buttons: u16, can_gc: CanGc, ) { assert!(matches!( event_type, FireMouseEventType::Enter | FireMouseEventType::Leave )); let common_ancestor = match related_target.as_ref() { Some(related_target) => event_target .common_ancestor(related_target, ShadowIncluding::No) .unwrap_or_else(|| DomRoot::from_ref(&*event_target)), None => DomRoot::from_ref(&*event_target), }; // We need to create a target chain in case the event target shares // its boundaries with its ancestors. let mut targets = vec![]; let mut current = Some(event_target); while let Some(node) = current { if node == common_ancestor { break; } current = node.GetParentNode(); targets.push(node); } // The order for dispatching mouseenter events starts from the topmost // common ancestor of the event target and the related target. if event_type == FireMouseEventType::Enter { targets = targets.into_iter().rev().collect(); } for target in targets { self.fire_mouse_event( client_point, target.upcast(), event_type, EventBubbles::DoesNotBubble, EventCancelable::NotCancelable, pressed_mouse_buttons, can_gc, ); } } #[allow(unsafe_code)] pub(crate) fn handle_wheel_event( &self, event: WheelEvent, hit_test_result: Option, can_gc: CanGc, ) { // Ignore all incoming events without a hit test. let Some(hit_test_result) = hit_test_result else { return; }; let node = unsafe { node::from_untrusted_node_address(hit_test_result.node) }; let Some(el) = node .inclusive_ancestors(ShadowIncluding::No) .filter_map(DomRoot::downcast::) .next() else { return; }; let node = el.upcast::(); let wheel_event_type_string = "wheel".to_owned(); debug!( "{}: on {:?} at {:?}", wheel_event_type_string, node.debug_str(), hit_test_result.point_in_viewport ); // https://w3c.github.io/uievents/#event-wheelevents let dom_event = DomWheelEvent::new( &self.window, DOMString::from(wheel_event_type_string), EventBubbles::Bubbles, EventCancelable::Cancelable, Some(&self.window), 0i32, Finite::wrap(event.delta.x), Finite::wrap(event.delta.y), Finite::wrap(event.delta.z), event.delta.mode as u32, can_gc, ); let dom_event = dom_event.upcast::(); dom_event.set_trusted(true); let target = node.upcast(); dom_event.fire(target, can_gc); } #[allow(unsafe_code)] pub(crate) fn handle_touch_event( &self, event: TouchEvent, hit_test_result: Option, can_gc: CanGc, ) -> TouchEventResult { // Ignore all incoming events without a hit test. let Some(hit_test_result) = hit_test_result else { return TouchEventResult::Forwarded; }; let TouchId(identifier) = event.id; let event_name = match event.event_type { TouchEventType::Down => "touchstart", TouchEventType::Move => "touchmove", TouchEventType::Up => "touchend", TouchEventType::Cancel => "touchcancel", }; let node = unsafe { node::from_untrusted_node_address(hit_test_result.node) }; let Some(el) = node .inclusive_ancestors(ShadowIncluding::No) .filter_map(DomRoot::downcast::) .next() else { return TouchEventResult::Forwarded; }; let target = DomRoot::upcast::(el); let window = &*self.window; let client_x = Finite::wrap(hit_test_result.point_in_viewport.x as f64); let client_y = Finite::wrap(hit_test_result.point_in_viewport.y as f64); let page_x = Finite::wrap(hit_test_result.point_in_viewport.x as f64 + window.PageXOffset() as f64); let page_y = Finite::wrap(hit_test_result.point_in_viewport.y as f64 + window.PageYOffset() as f64); let touch = Touch::new( window, identifier, &target, client_x, client_y, // TODO: Get real screen coordinates? client_x, client_y, page_x, page_y, can_gc, ); match event.event_type { TouchEventType::Down => { // Add a new touch point self.active_touch_points .borrow_mut() .push(Dom::from_ref(&*touch)); }, TouchEventType::Move => { // Replace an existing touch point let mut active_touch_points = self.active_touch_points.borrow_mut(); match active_touch_points .iter_mut() .find(|t| t.Identifier() == identifier) { Some(t) => *t = Dom::from_ref(&*touch), None => warn!("Got a touchmove event for a non-active touch point"), } }, TouchEventType::Up | TouchEventType::Cancel => { // Remove an existing touch point let mut active_touch_points = self.active_touch_points.borrow_mut(); match active_touch_points .iter() .position(|t| t.Identifier() == identifier) { Some(i) => { active_touch_points.swap_remove(i); }, None => warn!("Got a touchend event for a non-active touch point"), } }, } rooted_vec!(let mut target_touches); let touches = { let touches = self.active_touch_points.borrow(); target_touches.extend(touches.iter().filter(|t| t.Target() == target).cloned()); TouchList::new(window, touches.r(), can_gc) }; let event = DomTouchEvent::new( window, DOMString::from(event_name), EventBubbles::Bubbles, EventCancelable::from(event.is_cancelable()), Some(window), 0i32, &touches, &TouchList::new(window, from_ref(&&*touch), can_gc), &TouchList::new(window, target_touches.r(), can_gc), // FIXME: modifier keys false, false, false, false, can_gc, ); let event = event.upcast::(); let result = event.fire(&target, can_gc); match result { EventStatus::Canceled => TouchEventResult::Processed(false), EventStatus::NotCanceled => TouchEventResult::Processed(true), } } /// The entry point for all key processing for web content pub(crate) fn dispatch_key_event( &self, keyboard_event: ::keyboard_types::KeyboardEvent, can_gc: CanGc, ) { let focused = self.get_focused_element(); let body = self.GetBody(); let target = match (&focused, &body) { (Some(focused), _) => focused.upcast(), (&None, Some(body)) => body.upcast(), (&None, &None) => self.window.upcast(), }; let keyevent = KeyboardEvent::new( &self.window, DOMString::from(keyboard_event.state.to_string()), true, true, Some(&self.window), 0, keyboard_event.key.clone(), DOMString::from(keyboard_event.code.to_string()), keyboard_event.location as u32, keyboard_event.repeat, keyboard_event.is_composing, keyboard_event.modifiers, 0, keyboard_event.key.legacy_keycode(), can_gc, ); let event = keyevent.upcast::(); event.fire(target, can_gc); let mut cancel_state = event.get_cancel_state(); // https://w3c.github.io/uievents/#keys-cancelable-keys if keyboard_event.state == KeyState::Down && is_character_value_key(&(keyboard_event.key)) && !keyboard_event.is_composing && cancel_state != EventDefault::Prevented { // https://w3c.github.io/uievents/#keypress-event-order let event = KeyboardEvent::new( &self.window, DOMString::from("keypress"), true, true, Some(&self.window), 0, keyboard_event.key.clone(), DOMString::from(keyboard_event.code.to_string()), keyboard_event.location as u32, keyboard_event.repeat, keyboard_event.is_composing, keyboard_event.modifiers, keyboard_event.key.legacy_charcode(), 0, can_gc, ); let ev = event.upcast::(); ev.fire(target, can_gc); cancel_state = ev.get_cancel_state(); } if cancel_state == EventDefault::Allowed { let msg = EmbedderMsg::Keyboard(self.webview_id(), keyboard_event.clone()); self.send_to_embedder(msg); // This behavior is unspecced // We are supposed to dispatch synthetic click activation for Space and/or Return, // however *when* we do it is up to us. // Here, we're dispatching it after the key event so the script has a chance to cancel it // https://www.w3.org/Bugs/Public/show_bug.cgi?id=27337 if (keyboard_event.key == Key::Enter || keyboard_event.code == Code::Space) && keyboard_event.state == KeyState::Up { if let Some(elem) = target.downcast::() { elem.upcast::() .fire_synthetic_pointer_event_not_trusted(DOMString::from("click"), can_gc); } } } } pub(crate) fn dispatch_ime_event(&self, event: ImeEvent, can_gc: CanGc) { let composition_event = match event { ImeEvent::Dismissed => { self.request_focus( self.GetBody().as_ref().map(|e| e.upcast()), FocusType::Element, can_gc, ); return; }, ImeEvent::Composition(composition_event) => composition_event, }; // spec: https://w3c.github.io/uievents/#compositionstart // spec: https://w3c.github.io/uievents/#compositionupdate // spec: https://w3c.github.io/uievents/#compositionend // > Event.target : focused element processing the composition let focused = self.get_focused_element(); let target = if let Some(elem) = &focused { elem.upcast() } else { // Event is only dispatched if there is a focused element. return; }; let cancelable = composition_event.state == keyboard_types::CompositionState::Start; let compositionevent = CompositionEvent::new( &self.window, DOMString::from(composition_event.state.to_string()), true, cancelable, Some(&self.window), 0, DOMString::from(composition_event.data), can_gc, ); let event = compositionevent.upcast::(); event.fire(target, can_gc); } // https://dom.spec.whatwg.org/#converting-nodes-into-a-node pub(crate) fn node_from_nodes_and_strings( &self, mut nodes: Vec, can_gc: CanGc, ) -> Fallible> { if nodes.len() == 1 { Ok(match nodes.pop().unwrap() { NodeOrString::Node(node) => node, NodeOrString::String(string) => { DomRoot::upcast(self.CreateTextNode(string, can_gc)) }, }) } else { let fragment = DomRoot::upcast::(self.CreateDocumentFragment(can_gc)); for node in nodes { match node { NodeOrString::Node(node) => { fragment.AppendChild(&node, can_gc)?; }, NodeOrString::String(string) => { let node = DomRoot::upcast::(self.CreateTextNode(string, can_gc)); // No try!() here because appending a text node // should not fail. fragment.AppendChild(&node, can_gc).unwrap(); }, } } Ok(fragment) } } pub(crate) fn get_body_attribute(&self, local_name: &LocalName) -> DOMString { match self .GetBody() .and_then(DomRoot::downcast::) { Some(ref body) => body.upcast::().get_string_attribute(local_name), None => DOMString::new(), } } pub(crate) fn set_body_attribute( &self, local_name: &LocalName, value: DOMString, can_gc: CanGc, ) { if let Some(ref body) = self .GetBody() .and_then(DomRoot::downcast::) { let body = body.upcast::(); let value = body.parse_attribute(&ns!(), local_name, value); body.set_attribute(local_name, value, can_gc); } } pub(crate) fn set_current_script(&self, script: Option<&HTMLScriptElement>) { self.current_script.set(script); } pub(crate) fn get_script_blocking_stylesheets_count(&self) -> u32 { self.script_blocking_stylesheets_count.get() } pub(crate) fn increment_script_blocking_stylesheet_count(&self) { let count_cell = &self.script_blocking_stylesheets_count; count_cell.set(count_cell.get() + 1); } pub(crate) fn decrement_script_blocking_stylesheet_count(&self) { let count_cell = &self.script_blocking_stylesheets_count; assert!(count_cell.get() > 0); count_cell.set(count_cell.get() - 1); } pub(crate) fn invalidate_stylesheets(&self) { self.stylesheets.borrow_mut().force_dirty(OriginSet::all()); // Mark the document element dirty so a reflow will be performed. // // FIXME(emilio): Use the DocumentStylesheetSet invalidation stuff. if let Some(element) = self.GetDocumentElement() { element.upcast::().dirty(NodeDamage::NodeStyleDamaged); } } /// Whether or not this `Document` has any active requestAnimationFrame callbacks /// registered. pub(crate) fn has_active_request_animation_frame_callbacks(&self) -> bool { !self.animation_frame_list.borrow().is_empty() } /// pub(crate) fn request_animation_frame(&self, callback: AnimationFrameCallback) -> u32 { let ident = self.animation_frame_ident.get() + 1; self.animation_frame_ident.set(ident); self.animation_frame_list .borrow_mut() .push_back((ident, Some(callback))); // If we are running 'fake' animation frames, we unconditionally // set up a one-shot timer for script to execute the rAF callbacks. if self.is_faking_animation_frames() && !self.window().throttled() { self.schedule_fake_animation_frame(); } else if !self.running_animation_callbacks.get() { // No need to send a `ChangeRunningAnimationsState` if we're running animation callbacks: // we're guaranteed to already be in the "animation callbacks present" state. // // This reduces CPU usage by avoiding needless thread wakeups in the common case of // repeated rAF. let event = ScriptToConstellationMessage::ChangeRunningAnimationsState( AnimationState::AnimationCallbacksPresent, ); self.window().send_to_constellation(event); } ident } /// pub(crate) fn cancel_animation_frame(&self, ident: u32) { let mut list = self.animation_frame_list.borrow_mut(); if let Some(pair) = list.iter_mut().find(|pair| pair.0 == ident) { pair.1 = None; } } fn schedule_fake_animation_frame(&self) { warn!("Scheduling fake animation frame. Animation frames tick too fast."); let callback = FakeRequestAnimationFrameCallback { document: Trusted::new(self), }; self.global().schedule_callback( OneshotTimerCallback::FakeRequestAnimationFrame(callback), Duration::from_millis(FAKE_REQUEST_ANIMATION_FRAME_DELAY), ); } /// pub(crate) fn run_the_animation_frame_callbacks(&self, can_gc: CanGc) { let _realm = enter_realm(self); self.pending_animation_ticks .borrow_mut() .remove(AnimationTickType::REQUEST_ANIMATION_FRAME); self.running_animation_callbacks.set(true); let was_faking_animation_frames = self.is_faking_animation_frames(); let timing = self.global().performance().Now(); let num_callbacks = self.animation_frame_list.borrow().len(); for _ in 0..num_callbacks { let (_, maybe_callback) = self.animation_frame_list.borrow_mut().pop_front().unwrap(); if let Some(callback) = maybe_callback { callback.call(self, *timing, can_gc); } } self.running_animation_callbacks.set(false); let callbacks_did_not_trigger_reflow = self.needs_reflow().is_none(); let is_empty = self.animation_frame_list.borrow().is_empty(); if !is_empty && callbacks_did_not_trigger_reflow && !was_faking_animation_frames { // If the rAF callbacks did not mutate the DOM, then the impending // reflow call as part of *update the rendering* will not do anything // and therefore no new frame will be sent to the compositor. // If this happens, the compositor will not tick the animation // and the next rAF will never be called! When this happens // for several frames, then the spurious rAF detection below // will kick in and use a timer to tick the callbacks. However, // for the interim frames where we are deciding whether this rAF // is considered spurious, we need to ensure that the layout // and compositor *do* tick the animation. self.set_needs_paint(true); } // Update the counter of spurious animation frames. let spurious_frames = self.spurious_animation_frames.get(); if callbacks_did_not_trigger_reflow { if spurious_frames < SPURIOUS_ANIMATION_FRAME_THRESHOLD { self.spurious_animation_frames.set(spurious_frames + 1); } } else { self.spurious_animation_frames.set(0); } // Only send the animation change state message after running any callbacks. // This means that if the animation callback adds a new callback for // the next frame (which is the common case), we won't send a NoAnimationCallbacksPresent // message quickly followed by an AnimationCallbacksPresent message. // // If this frame was spurious and we've seen too many spurious frames in a row, tell the // constellation to stop giving us video refresh callbacks, to save energy. (A spurious // animation frame is one in which the callback did not mutate the DOM—that is, an // animation frame that wasn't actually used for animation.) let just_crossed_spurious_animation_threshold = !was_faking_animation_frames && self.is_faking_animation_frames(); if is_empty || just_crossed_spurious_animation_threshold { if !is_empty { // We just realized that we need to stop requesting compositor's animation ticks // due to spurious animation frames, but we still have rAF callbacks queued. Since // `is_faking_animation_frames` would not have been true at the point where these // new callbacks were registered, the one-shot timer will not have been setup in // `request_animation_frame()`. Since we stop the compositor ticks below, we need // to expliclty trigger a OneshotTimerCallback for these queued callbacks. self.schedule_fake_animation_frame(); } let event = ScriptToConstellationMessage::ChangeRunningAnimationsState( AnimationState::NoAnimationCallbacksPresent, ); self.window().send_to_constellation(event); } // If we were previously faking animation frames, we need to re-enable video refresh // callbacks when we stop seeing spurious animation frames. if was_faking_animation_frames && !self.is_faking_animation_frames() && !is_empty { self.window().send_to_constellation( ScriptToConstellationMessage::ChangeRunningAnimationsState( AnimationState::AnimationCallbacksPresent, ), ); } } pub(crate) fn policy_container(&self) -> Ref { self.policy_container.borrow() } /// Add the policy container and HTTPS state to a given request. /// /// TODO: Can this hapen for all requests that go through the document? pub(crate) fn prepare_request(&self, request: RequestBuilder) -> RequestBuilder { request .policy_container(self.policy_container().to_owned()) .https_state(self.https_state.get()) } pub(crate) fn fetch( &self, load: LoadType, mut request: RequestBuilder, listener: Listener, ) { request = request .insecure_requests_policy(self.insecure_requests_policy()) .has_trustworthy_ancestor_origin(self.has_trustworthy_ancestor_or_current_origin()); let callback = NetworkListener { context: std::sync::Arc::new(Mutex::new(listener)), task_source: self .owner_global() .task_manager() .networking_task_source() .into(), } .into_callback(); self.loader_mut() .fetch_async_with_callback(load, request, callback); } pub(crate) fn fetch_background( &self, mut request: RequestBuilder, listener: Listener, ) { request = request .insecure_requests_policy(self.insecure_requests_policy()) .has_trustworthy_ancestor_origin(self.has_trustworthy_ancestor_or_current_origin()); let callback = NetworkListener { context: std::sync::Arc::new(Mutex::new(listener)), task_source: self .owner_global() .task_manager() .networking_task_source() .into(), } .into_callback(); self.loader_mut().fetch_async_background(request, callback); } // https://html.spec.whatwg.org/multipage/#the-end // https://html.spec.whatwg.org/multipage/#delay-the-load-event pub(crate) fn finish_load(&self, load: LoadType, can_gc: CanGc) { // This does not delay the load event anymore. debug!("Document got finish_load: {:?}", load); self.loader.borrow_mut().finish_load(&load); match load { LoadType::Stylesheet(_) => { // A stylesheet finishing to load may unblock any pending // parsing-blocking script or deferred script. self.process_pending_parsing_blocking_script(can_gc); // Step 3. self.process_deferred_scripts(can_gc); }, LoadType::PageSource(_) => { // We finished loading the page, so if the `Window` is still waiting for // the first layout, allow it. if self.has_browsing_context && self.is_fully_active() { self.window().allow_layout_if_necessary(can_gc); } // Deferred scripts have to wait for page to finish loading, // this is the first opportunity to process them. // Step 3. self.process_deferred_scripts(can_gc); }, _ => {}, } // Step 4 is in another castle, namely at the end of // process_deferred_scripts. // Step 5 can be found in asap_script_loaded and // asap_in_order_script_loaded. let loader = self.loader.borrow(); // Servo measures when the top-level content (not iframes) is loaded. if self.top_level_dom_complete.get().is_none() && loader.is_only_blocked_by_iframes() { update_with_current_instant(&self.top_level_dom_complete); } if loader.is_blocked() || loader.events_inhibited() { // Step 6. return; } ScriptThread::mark_document_with_no_blocked_loads(self); } // https://html.spec.whatwg.org/multipage/#prompt-to-unload-a-document pub(crate) fn prompt_to_unload(&self, recursive_flag: bool, can_gc: CanGc) -> bool { // TODO: Step 1, increase the event loop's termination nesting level by 1. // Step 2 self.incr_ignore_opens_during_unload_counter(); //Step 3-5. let beforeunload_event = BeforeUnloadEvent::new( &self.window, atom!("beforeunload"), EventBubbles::Bubbles, EventCancelable::Cancelable, can_gc, ); let event = beforeunload_event.upcast::(); event.set_trusted(true); let event_target = self.window.upcast::(); let has_listeners = event_target.has_listeners_for(&atom!("beforeunload")); self.window .dispatch_event_with_target_override(event, can_gc); // TODO: Step 6, decrease the event loop's termination nesting level by 1. // Step 7 if has_listeners { self.salvageable.set(false); } let mut can_unload = true; // TODO: Step 8, also check sandboxing modals flag. let default_prevented = event.DefaultPrevented(); let return_value_not_empty = !event .downcast::() .unwrap() .ReturnValue() .is_empty(); if default_prevented || return_value_not_empty { let (chan, port) = ipc::channel().expect("Failed to create IPC channel!"); let msg = EmbedderMsg::AllowUnload(self.webview_id(), chan); self.send_to_embedder(msg); can_unload = port.recv().unwrap() == AllowOrDeny::Allow; } // Step 9 if !recursive_flag { // `prompt_to_unload` might cause futher modifications to the DOM so collecting here prevents // a double borrow if the `IFrameCollection` needs to be validated again. let iframes: Vec<_> = self.iframes().iter().collect(); for iframe in &iframes { // TODO: handle the case of cross origin iframes. let document = iframe.owner_document(); can_unload = document.prompt_to_unload(true, can_gc); if !document.salvageable() { self.salvageable.set(false); } if !can_unload { break; } } } // Step 10 self.decr_ignore_opens_during_unload_counter(); can_unload } // https://html.spec.whatwg.org/multipage/#unload-a-document pub(crate) fn unload(&self, recursive_flag: bool, can_gc: CanGc) { // TODO: Step 1, increase the event loop's termination nesting level by 1. // Step 2 self.incr_ignore_opens_during_unload_counter(); // Step 3-6 If oldDocument's page showing is true: if self.page_showing.get() { // Set oldDocument's page showing to false. self.page_showing.set(false); // Fire a page transition event named pagehide at oldDocument's relevant global object with oldDocument's // salvageable state. let event = PageTransitionEvent::new( &self.window, atom!("pagehide"), false, // bubbles false, // cancelable self.salvageable.get(), // persisted can_gc, ); let event = event.upcast::(); event.set_trusted(true); let _ = self .window .dispatch_event_with_target_override(event, can_gc); // Step 6 Update the visibility state of oldDocument to "hidden". self.update_visibility_state(DocumentVisibilityState::Hidden, can_gc); } // Step 7 if !self.fired_unload.get() { let event = Event::new( self.window.upcast(), atom!("unload"), EventBubbles::Bubbles, EventCancelable::Cancelable, can_gc, ); event.set_trusted(true); let event_target = self.window.upcast::(); let has_listeners = event_target.has_listeners_for(&atom!("unload")); let _ = self .window .dispatch_event_with_target_override(&event, can_gc); self.fired_unload.set(true); // Step 9 if has_listeners { self.salvageable.set(false); } } // TODO: Step 8, decrease the event loop's termination nesting level by 1. // Step 13 if !recursive_flag { // `unload` might cause futher modifications to the DOM so collecting here prevents // a double borrow if the `IFrameCollection` needs to be validated again. let iframes: Vec<_> = self.iframes().iter().collect(); for iframe in &iframes { // TODO: handle the case of cross origin iframes. let document = iframe.owner_document(); document.unload(true, can_gc); if !document.salvageable() { self.salvageable.set(false); } } } let global_scope = self.window.as_global_scope(); // Step 10, 14 // https://html.spec.whatwg.org/multipage/#unloading-document-cleanup-steps if !self.salvageable.get() { // Step 1 of clean-up steps. global_scope.close_event_sources(); let msg = ScriptToConstellationMessage::DiscardDocument; let _ = global_scope.script_to_constellation_chan().send(msg); } // https://w3c.github.io/FileAPI/#lifeTime global_scope.clean_up_all_file_resources(); // Step 15, End self.decr_ignore_opens_during_unload_counter(); } // https://html.spec.whatwg.org/multipage/#the-end pub(crate) fn maybe_queue_document_completion(&self) { // https://html.spec.whatwg.org/multipage/#delaying-load-events-mode let is_in_delaying_load_events_mode = match self.window.undiscarded_window_proxy() { Some(window_proxy) => window_proxy.is_delaying_load_events_mode(), None => false, }; // Note: if the document is not fully active, layout will have exited already, // and this method will panic. // The underlying problem might actually be that layout exits while it should be kept alive. // See https://github.com/servo/servo/issues/22507 let not_ready_for_load = self.loader.borrow().is_blocked() || !self.is_fully_active() || is_in_delaying_load_events_mode; if not_ready_for_load { // Step 6. return; } assert!(!self.loader.borrow().events_inhibited()); self.loader.borrow_mut().inhibit_events(); // The rest will ever run only once per document. // Step 7. debug!("Document loads are complete."); let document = Trusted::new(self); self.owner_global() .task_manager() .dom_manipulation_task_source() .queue(task!(fire_load_event: move || { let document = document.root(); let window = document.window(); if !window.is_alive() { return; } // Step 7.1. document.set_ready_state(DocumentReadyState::Complete, CanGc::note()); // Step 7.2. if document.browsing_context().is_none() { return; } let event = Event::new( window.upcast(), atom!("load"), EventBubbles::DoesNotBubble, EventCancelable::NotCancelable, CanGc::note(), ); event.set_trusted(true); // http://w3c.github.io/navigation-timing/#widl-PerformanceNavigationTiming-loadEventStart update_with_current_instant(&document.load_event_start); debug!("About to dispatch load for {:?}", document.url()); window.dispatch_event_with_target_override(&event, CanGc::note()); // http://w3c.github.io/navigation-timing/#widl-PerformanceNavigationTiming-loadEventEnd update_with_current_instant(&document.load_event_end); if let Some(fragment) = document.url().fragment() { document.check_and_scroll_fragment(fragment, CanGc::note()); } })); // Step 8. let document = Trusted::new(self); if document.root().browsing_context().is_some() { self.owner_global() .task_manager() .dom_manipulation_task_source() .queue(task!(fire_pageshow_event: move || { let document = document.root(); let window = document.window(); if document.page_showing.get() || !window.is_alive() { return; } document.page_showing.set(true); let event = PageTransitionEvent::new( window, atom!("pageshow"), false, // bubbles false, // cancelable false, // persisted CanGc::note(), ); let event = event.upcast::(); event.set_trusted(true); window.dispatch_event_with_target_override(event, CanGc::note()); })); } // Step 9. // TODO: pending application cache download process tasks. // Step 10. // TODO: printing steps. // Step 11. // TODO: ready for post-load tasks. // The dom.webxr.sessionavailable pref allows webxr // content to immediately begin a session without waiting for a user gesture. // TODO: should this only happen on the first document loaded? // https://immersive-web.github.io/webxr/#user-intention // https://github.com/immersive-web/navigation/issues/10 #[cfg(feature = "webxr")] if pref!(dom_webxr_sessionavailable) && self.window.is_top_level() { self.window.Navigator().Xr().dispatch_sessionavailable(); } // Step 12: completely loaded. // https://html.spec.whatwg.org/multipage/#completely-loaded // TODO: fully implement "completely loaded". let document = Trusted::new(self); if document.root().browsing_context().is_some() { self.owner_global() .task_manager() .dom_manipulation_task_source() .queue(task!(completely_loaded: move || { let document = document.root(); document.completely_loaded.set(true); if let Some(DeclarativeRefresh::PendingLoad { url, time }) = &*document.declarative_refresh.borrow() { // https://html.spec.whatwg.org/multipage/#shared-declarative-refresh-steps document.window.as_global_scope().schedule_callback( OneshotTimerCallback::RefreshRedirectDue(RefreshRedirectDue { window: DomRoot::from_ref(document.window()), url: url.clone(), }), Duration::from_secs(*time), ); } // Note: this will, among others, result in the "iframe-load-event-steps" being run. // https://html.spec.whatwg.org/multipage/#iframe-load-event-steps document.notify_constellation_load(); })); } } pub(crate) fn completely_loaded(&self) -> bool { self.completely_loaded.get() } // https://html.spec.whatwg.org/multipage/#pending-parsing-blocking-script pub(crate) fn set_pending_parsing_blocking_script( &self, script: &HTMLScriptElement, load: Option, ) { assert!(!self.has_pending_parsing_blocking_script()); *self.pending_parsing_blocking_script.borrow_mut() = Some(PendingScript::new_with_load(script, load)); } // https://html.spec.whatwg.org/multipage/#pending-parsing-blocking-script pub(crate) fn has_pending_parsing_blocking_script(&self) -> bool { self.pending_parsing_blocking_script.borrow().is_some() } /// step 22.d. pub(crate) fn pending_parsing_blocking_script_loaded( &self, element: &HTMLScriptElement, result: ScriptResult, can_gc: CanGc, ) { { let mut blocking_script = self.pending_parsing_blocking_script.borrow_mut(); let entry = blocking_script.as_mut().unwrap(); assert!(&*entry.element == element); entry.loaded(result); } self.process_pending_parsing_blocking_script(can_gc); } fn process_pending_parsing_blocking_script(&self, can_gc: CanGc) { if self.script_blocking_stylesheets_count.get() > 0 { return; } let pair = self .pending_parsing_blocking_script .borrow_mut() .as_mut() .and_then(PendingScript::take_result); if let Some((element, result)) = pair { *self.pending_parsing_blocking_script.borrow_mut() = None; self.get_current_parser() .unwrap() .resume_with_pending_parsing_blocking_script(&element, result, can_gc); } } // https://html.spec.whatwg.org/multipage/#set-of-scripts-that-will-execute-as-soon-as-possible pub(crate) fn add_asap_script(&self, script: &HTMLScriptElement) { self.asap_scripts_set .borrow_mut() .push(Dom::from_ref(script)); } /// step 5. /// step 22.d. pub(crate) fn asap_script_loaded( &self, element: &HTMLScriptElement, result: ScriptResult, can_gc: CanGc, ) { { let mut scripts = self.asap_scripts_set.borrow_mut(); let idx = scripts .iter() .position(|entry| &**entry == element) .unwrap(); scripts.swap_remove(idx); } element.execute(result, can_gc); } // https://html.spec.whatwg.org/multipage/#list-of-scripts-that-will-execute-in-order-as-soon-as-possible pub(crate) fn push_asap_in_order_script(&self, script: &HTMLScriptElement) { self.asap_in_order_scripts_list.push(script); } /// step 5. /// step> 22.c. pub(crate) fn asap_in_order_script_loaded( &self, element: &HTMLScriptElement, result: ScriptResult, can_gc: CanGc, ) { self.asap_in_order_scripts_list.loaded(element, result); while let Some((element, result)) = self .asap_in_order_scripts_list .take_next_ready_to_be_executed() { element.execute(result, can_gc); } } // https://html.spec.whatwg.org/multipage/#list-of-scripts-that-will-execute-when-the-document-has-finished-parsing pub(crate) fn add_deferred_script(&self, script: &HTMLScriptElement) { self.deferred_scripts.push(script); } /// step 3. /// step 22.d. pub(crate) fn deferred_script_loaded(&self, element: &HTMLScriptElement, result: ScriptResult) { self.deferred_scripts.loaded(element, result); self.process_deferred_scripts(CanGc::note()); } /// step 3. fn process_deferred_scripts(&self, can_gc: CanGc) { if self.ready_state.get() != DocumentReadyState::Interactive { return; } // Part of substep 1. loop { if self.script_blocking_stylesheets_count.get() > 0 { return; } if let Some((element, result)) = self.deferred_scripts.take_next_ready_to_be_executed() { element.execute(result, can_gc); } else { break; } } if self.deferred_scripts.is_empty() { // https://html.spec.whatwg.org/multipage/#the-end step 4. self.maybe_dispatch_dom_content_loaded(); } } // https://html.spec.whatwg.org/multipage/#the-end step 4. pub(crate) fn maybe_dispatch_dom_content_loaded(&self) { if self.domcontentloaded_dispatched.get() { return; } self.domcontentloaded_dispatched.set(true); assert_ne!( self.ReadyState(), DocumentReadyState::Complete, "Complete before DOMContentLoaded?" ); update_with_current_instant(&self.dom_content_loaded_event_start); // Step 4.1. let document = Trusted::new(self); self.owner_global() .task_manager() .dom_manipulation_task_source() .queue( task!(fire_dom_content_loaded_event: move || { let document = document.root(); document.upcast::().fire_bubbling_event(atom!("DOMContentLoaded"), CanGc::note()); update_with_current_instant(&document.dom_content_loaded_event_end); }) ); // html parsing has finished - set dom content loaded self.interactive_time .borrow() .maybe_set_tti(InteractiveFlag::DOMContentLoaded); // Step 4.2. // TODO: client message queue. } // https://html.spec.whatwg.org/multipage/#abort-a-document pub(crate) fn abort(&self, can_gc: CanGc) { // We need to inhibit the loader before anything else. self.loader.borrow_mut().inhibit_events(); // Step 1. for iframe in self.iframes().iter() { if let Some(document) = iframe.GetContentDocument() { // TODO: abort the active documents of every child browsing context. document.abort(can_gc); // TODO: salvageable flag. } } // Step 2. self.script_blocking_stylesheets_count.set(0); *self.pending_parsing_blocking_script.borrow_mut() = None; *self.asap_scripts_set.borrow_mut() = vec![]; self.asap_in_order_scripts_list.clear(); self.deferred_scripts.clear(); let loads_cancelled = self.loader.borrow_mut().cancel_all_loads(); let event_sources_canceled = self.window.as_global_scope().close_event_sources(); if loads_cancelled || event_sources_canceled { // If any loads were canceled. self.salvageable.set(false); }; // Also Step 2. // Note: the spec says to discard any tasks queued for fetch. // This cancels all tasks on the networking task source, which might be too broad. // See https://github.com/whatwg/html/issues/3837 self.owner_global() .task_manager() .cancel_pending_tasks_for_source(TaskSourceName::Networking); // Step 3. if let Some(parser) = self.get_current_parser() { self.active_parser_was_aborted.set(true); parser.abort(can_gc); self.salvageable.set(false); } } pub(crate) fn notify_constellation_load(&self) { self.window() .send_to_constellation(ScriptToConstellationMessage::LoadComplete); } pub(crate) fn set_current_parser(&self, script: Option<&ServoParser>) { self.current_parser.set(script); } pub(crate) fn get_current_parser(&self) -> Option> { self.current_parser.get() } /// A reference to the [`IFrameCollection`] of this [`Document`], holding information about /// `