/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use std::sync::Arc; use base::id::PipelineId; use euclid::Size2D; use fnv::FnvHashMap; use fonts::FontContext; use fxhash::FxHashMap; use net_traits::image_cache::{ Image as CachedImage, ImageCache, ImageCacheResult, ImageOrMetadataAvailable, PendingImageId, UsePlaceholder, }; use parking_lot::{Mutex, RwLock}; use pixels::RasterImage; use script_layout_interface::{ IFrameSizes, ImageAnimationState, PendingImage, PendingImageState, PendingRasterizationImage, }; use servo_url::{ImmutableOrigin, ServoUrl}; use style::context::SharedStyleContext; use style::dom::OpaqueNode; use style::values::computed::image::{Gradient, Image}; use webrender_api::units::{DeviceIntSize, DeviceSize}; pub(crate) type CachedImageOrError = Result<CachedImage, ResolveImageError>; pub struct LayoutContext<'a> { pub id: PipelineId, pub use_rayon: bool, pub origin: ImmutableOrigin, /// Bits shared by the layout and style system. pub style_context: SharedStyleContext<'a>, /// A FontContext to be used during layout. pub font_context: Arc<FontContext>, /// Reference to the script thread image cache. pub image_cache: Arc<dyn ImageCache>, /// A list of in-progress image loads to be shared with the script thread. pub pending_images: Mutex<Vec<PendingImage>>, /// A list of fully loaded vector images that need to be rasterized to a specific /// size determined by layout. This will be shared with the script thread. pub pending_rasterization_images: Mutex<Vec<PendingRasterizationImage>>, /// A collection of `<iframe>` sizes to send back to script. pub iframe_sizes: Mutex<IFrameSizes>, // A cache that maps image resources used in CSS (e.g as the `url()` value // for `background-image` or `content` property) to the final resolved image data. pub resolved_images_cache: Arc<RwLock<FnvHashMap<(ServoUrl, UsePlaceholder), CachedImageOrError>>>, pub node_image_animation_map: Arc<RwLock<FxHashMap<OpaqueNode, ImageAnimationState>>>, /// The DOM node that is highlighted by the devtools inspector, if any pub highlighted_dom_node: Option<OpaqueNode>, } pub enum ResolvedImage<'a> { Gradient(&'a Gradient), // The size is tracked explicitly as image-set images can specify their // natural resolution which affects the final size for raster images. Image { image: CachedImage, size: DeviceSize, }, } impl Drop for LayoutContext<'_> { fn drop(&mut self) { if !std::thread::panicking() { assert!(self.pending_images.lock().is_empty()); assert!(self.pending_rasterization_images.lock().is_empty()); } } } #[derive(Clone, Copy, Debug)] pub enum ResolveImageError { LoadError, ImagePending, ImageRequested, OnlyMetadata, InvalidUrl, MissingNode, ImageMissingFromImageSet, FailedToResolveImageFromImageSet, NotImplementedYet(&'static str), None, } pub(crate) enum LayoutImageCacheResult { Pending, DataAvailable(ImageOrMetadataAvailable), LoadError, } impl LayoutContext<'_> { #[inline(always)] pub fn shared_context(&self) -> &SharedStyleContext { &self.style_context } pub(crate) fn get_or_request_image_or_meta( &self, node: OpaqueNode, url: ServoUrl, use_placeholder: UsePlaceholder, ) -> LayoutImageCacheResult { // Check for available image or start tracking. let cache_result = self.image_cache.get_cached_image_status( url.clone(), self.origin.clone(), None, use_placeholder, ); match cache_result { ImageCacheResult::Available(img_or_meta) => { LayoutImageCacheResult::DataAvailable(img_or_meta) }, // Image has been requested, is still pending. Return no image for this paint loop. // When the image loads it will trigger a reflow and/or repaint. ImageCacheResult::Pending(id) => { let image = PendingImage { state: PendingImageState::PendingResponse, node: node.into(), id, origin: self.origin.clone(), }; self.pending_images.lock().push(image); LayoutImageCacheResult::Pending }, // Not yet requested - request image or metadata from the cache ImageCacheResult::ReadyForRequest(id) => { let image = PendingImage { state: PendingImageState::Unrequested(url), node: node.into(), id, origin: self.origin.clone(), }; self.pending_images.lock().push(image); LayoutImageCacheResult::Pending }, // Image failed to load, so just return the same error. ImageCacheResult::LoadError => LayoutImageCacheResult::LoadError, } } pub fn handle_animated_image(&self, node: OpaqueNode, image: Arc<RasterImage>) { let mut store = self.node_image_animation_map.write(); // 1. first check whether node previously being track for animated image. if let Some(image_state) = store.get(&node) { // a. if the node is not containing the same image as before. if image_state.image_key() != image.id { if image.should_animate() { // i. Register/Replace tracking item in image_animation_manager. store.insert( node, ImageAnimationState::new( image, self.shared_context().current_time_for_animations, ), ); } else { // ii. Cancel Action if the node's image is no longer animated. store.remove(&node); } } } else if image.should_animate() { store.insert( node, ImageAnimationState::new(image, self.shared_context().current_time_for_animations), ); } } fn get_cached_image_for_url( &self, node: OpaqueNode, url: ServoUrl, use_placeholder: UsePlaceholder, ) -> Result<CachedImage, ResolveImageError> { if let Some(cached_image) = self .resolved_images_cache .read() .get(&(url.clone(), use_placeholder)) { return cached_image.clone(); } let result = self.get_or_request_image_or_meta(node, url.clone(), use_placeholder); match result { LayoutImageCacheResult::DataAvailable(img_or_meta) => match img_or_meta { ImageOrMetadataAvailable::ImageAvailable { image, .. } => { if let Some(image) = image.as_raster_image() { self.handle_animated_image(node, image.clone()); } let mut resolved_images_cache = self.resolved_images_cache.write(); resolved_images_cache.insert((url, use_placeholder), Ok(image.clone())); Ok(image) }, ImageOrMetadataAvailable::MetadataAvailable(..) => { Result::Err(ResolveImageError::OnlyMetadata) }, }, LayoutImageCacheResult::Pending => Result::Err(ResolveImageError::ImagePending), LayoutImageCacheResult::LoadError => { let error = Err(ResolveImageError::LoadError); self.resolved_images_cache .write() .insert((url, use_placeholder), error.clone()); error }, } } pub fn rasterize_vector_image( &self, image_id: PendingImageId, size: DeviceIntSize, node: OpaqueNode, ) -> Option<RasterImage> { let result = self.image_cache.rasterize_vector_image(image_id, size); if result.is_none() { self.pending_rasterization_images .lock() .push(PendingRasterizationImage { id: image_id, node: node.into(), size, }); } result } pub fn resolve_image<'a>( &self, node: Option<OpaqueNode>, image: &'a Image, ) -> Result<ResolvedImage<'a>, ResolveImageError> { match image { // TODO: Add support for PaintWorklet and CrossFade rendering. Image::None => Result::Err(ResolveImageError::None), Image::CrossFade(_) => Result::Err(ResolveImageError::NotImplementedYet("CrossFade")), Image::PaintWorklet(_) => { Result::Err(ResolveImageError::NotImplementedYet("PaintWorklet")) }, Image::Gradient(gradient) => Ok(ResolvedImage::Gradient(gradient)), Image::Url(image_url) => { // FIXME: images won’t always have in intrinsic width or // height when support for SVG is added, or a WebRender // `ImageKey`, for that matter. // // FIXME: It feels like this should take into account the pseudo // element and not just the node. let image_url = image_url.url().ok_or(ResolveImageError::InvalidUrl)?; let node = node.ok_or(ResolveImageError::MissingNode)?; let image = self.get_cached_image_for_url( node, image_url.clone().into(), UsePlaceholder::No, )?; let metadata = image.metadata(); let size = Size2D::new(metadata.width, metadata.height).to_f32(); Ok(ResolvedImage::Image { image, size }) }, Image::ImageSet(image_set) => { image_set .items .get(image_set.selected_index) .ok_or(ResolveImageError::ImageMissingFromImageSet) .and_then(|image| { self.resolve_image(node, &image.image) .map(|info| match info { ResolvedImage::Image { image: cached_image, .. } => { // From <https://drafts.csswg.org/css-images-4/#image-set-notation>: // > A <resolution> (optional). This is used to help the UA decide // > which <image-set-option> to choose. If the image reference is // > for a raster image, it also specifies the image’s natural // > resolution, overriding any other source of data that might // > supply a natural resolution. let image_metadata = cached_image.metadata(); let size = if cached_image.as_raster_image().is_some() { let scale_factor = image.resolution.dppx(); Size2D::new( image_metadata.width as f32 / scale_factor, image_metadata.height as f32 / scale_factor, ) } else { Size2D::new(image_metadata.width, image_metadata.height) .to_f32() }; ResolvedImage::Image { image: cached_image, size, } }, _ => info, }) }) }, } } }