diff --git a/components/compositing/compositor.rs b/components/compositing/compositor.rs index 16b788b1972..572ecfdb50d 100644 --- a/components/compositing/compositor.rs +++ b/components/compositing/compositor.rs @@ -13,32 +13,32 @@ use windowing::{MouseWindowEvent, WindowEvent, WindowMethods, WindowNavigateMsg} use geom::point::{Point2D, TypedPoint2D}; use geom::rect::{Rect, TypedRect}; -use geom::size::{Size2D, TypedSize2D}; use geom::scale_factor::ScaleFactor; +use geom::size::{Size2D, TypedSize2D}; use gfx::color; use gfx::paint_task::Msg as PaintMsg; use gfx::paint_task::PaintRequest; -use layers::geometry::{DevicePixel, LayerPixel}; -use layers::layers::{BufferRequest, Layer, LayerBuffer, LayerBufferSet}; -use layers::rendergl; -use layers::rendergl::RenderContext; -use layers::scene::Scene; -use png; use gleam::gl::types::{GLint, GLsizei}; use gleam::gl; -use script_traits::{ConstellationControlMsg, ScriptControlChan}; +use layers::geometry::{DevicePixel, LayerPixel}; +use layers::layers::{BufferRequest, Layer, LayerBuffer, LayerBufferSet}; +use layers::rendergl::RenderContext; +use layers::rendergl; +use layers::scene::Scene; use msg::compositor_msg::{Epoch, LayerId}; use msg::compositor_msg::{ReadyState, PaintState, ScrollPolicy}; -use msg::constellation_msg::{ConstellationChan, NavigationDirection}; use msg::constellation_msg::Msg as ConstellationMsg; +use msg::constellation_msg::{ConstellationChan, NavigationDirection}; use msg::constellation_msg::{Key, KeyModifiers, KeyState, LoadData}; use msg::constellation_msg::{PipelineId, WindowSizeData}; +use png; use profile::mem; use profile::time::{self, ProfilerCategory, profile}; +use script_traits::{ConstellationControlMsg, ScriptControlChan}; use std::cmp; use std::collections::HashMap; use std::collections::hash_map::Entry::{Occupied, Vacant}; -use std::mem::replace; +use std::mem as std_mem; use std::num::Float; use std::rc::Rc; use std::slice::bytes::copy_memory; @@ -164,6 +164,9 @@ struct PipelineDetails { /// The status of this pipeline's PaintTask. paint_state: PaintState, + + /// Whether animations are running. + animations_running: bool, } impl PipelineDetails { @@ -172,6 +175,7 @@ impl PipelineDetails { pipeline: None, ready_state: ReadyState::Blank, paint_state: PaintState::Painting, + animations_running: false, } } } @@ -272,6 +276,11 @@ impl IOCompositor { self.change_paint_state(pipeline_id, paint_state); } + (Msg::ChangeRunningAnimationsState(pipeline_id, running_animations), + ShutdownState::NotShuttingDown) => { + self.change_running_animations_state(pipeline_id, running_animations); + } + (Msg::ChangePageTitle(pipeline_id, title), ShutdownState::NotShuttingDown) => { self.change_page_title(pipeline_id, title); } @@ -311,7 +320,8 @@ impl IOCompositor { self.set_layer_rect(pipeline_id, layer_id, &rect); } - (Msg::AssignPaintedBuffers(pipeline_id, epoch, replies), ShutdownState::NotShuttingDown) => { + (Msg::AssignPaintedBuffers(pipeline_id, epoch, replies), + ShutdownState::NotShuttingDown) => { for (layer_id, new_layer_buffer_set) in replies.into_iter() { self.assign_painted_buffers(pipeline_id, layer_id, new_layer_buffer_set, epoch); } @@ -399,6 +409,18 @@ impl IOCompositor { self.window.set_paint_state(paint_state); } + /// Sets or unsets the animations-running flag for the given pipeline, and schedules a + /// recomposite if necessary. + fn change_running_animations_state(&mut self, + pipeline_id: PipelineId, + animations_running: bool) { + self.get_or_create_pipeline_details(pipeline_id).animations_running = animations_running; + + if animations_running { + self.composite_if_necessary(); + } + } + pub fn get_or_create_pipeline_details<'a>(&'a mut self, pipeline_id: PipelineId) -> &'a mut PipelineDetails { @@ -867,15 +889,13 @@ impl IOCompositor { fn process_pending_scroll_events(&mut self) { let had_scroll_events = self.pending_scroll_events.len() > 0; - for scroll_event in replace(&mut self.pending_scroll_events, Vec::new()).into_iter() { + for scroll_event in std_mem::replace(&mut self.pending_scroll_events, + Vec::new()).into_iter() { let delta = scroll_event.delta / self.scene.scale; let cursor = scroll_event.cursor.as_f32() / self.scene.scale; - match self.scene.root { - Some(ref mut layer) => { - layer.handle_scroll_event(delta, cursor); - } - None => {} + if let Some(ref mut layer) = self.scene.root { + layer.handle_scroll_event(delta, cursor); } self.start_scrolling_timer_if_necessary(); @@ -887,6 +907,16 @@ impl IOCompositor { } } + /// If there are any animations running, dispatches appropriate messages to the constellation. + fn process_animations(&mut self) { + for (pipeline_id, pipeline_details) in self.pipeline_details.iter() { + if !pipeline_details.animations_running { + continue + } + self.constellation_chan.0.send(ConstellationMsg::TickAnimation(*pipeline_id)).unwrap(); + } + } + fn device_pixels_per_screen_px(&self) -> ScaleFactor { match opts::get().device_pixels_per_px { Some(device_pixels_per_px) => device_pixels_per_px, @@ -1086,7 +1116,8 @@ impl IOCompositor { let mut framebuffer_ids = vec!(); let mut texture_ids = vec!(); - let (width, height) = (self.window_size.width.get() as usize, self.window_size.height.get() as usize); + let (width, height) = + (self.window_size.width.get() as usize, self.window_size.height.get() as usize); if output_image { framebuffer_ids = gl::gen_framebuffers(1); @@ -1172,6 +1203,7 @@ impl IOCompositor { self.composition_request = CompositionRequest::NoCompositingNecessary; self.process_pending_scroll_events(); + self.process_animations(); } fn composite_if_necessary(&mut self) { diff --git a/components/compositing/compositor_task.rs b/components/compositing/compositor_task.rs index f940ad820c5..c951f589965 100644 --- a/components/compositing/compositor_task.rs +++ b/components/compositing/compositor_task.rs @@ -201,6 +201,8 @@ pub enum Msg { ChangePageTitle(PipelineId, Option), /// Alerts the compositor that the current page has changed its URL. ChangePageUrl(PipelineId, Url), + /// Alerts the compositor that the given pipeline has changed whether it is running animations. + ChangeRunningAnimationsState(PipelineId, bool), /// Alerts the compositor that a `PaintMsg` has been discarded. PaintMsgDiscarded, /// Replaces the current frame tree, typically called during main frame navigation. @@ -231,6 +233,7 @@ impl Debug for Msg { Msg::AssignPaintedBuffers(..) => write!(f, "AssignPaintedBuffers"), Msg::ChangeReadyState(..) => write!(f, "ChangeReadyState"), Msg::ChangePaintState(..) => write!(f, "ChangePaintState"), + Msg::ChangeRunningAnimationsState(..) => write!(f, "ChangeRunningAnimationsState"), Msg::ChangePageTitle(..) => write!(f, "ChangePageTitle"), Msg::ChangePageUrl(..) => write!(f, "ChangePageUrl"), Msg::PaintMsgDiscarded(..) => write!(f, "PaintMsgDiscarded"), diff --git a/components/compositing/constellation.rs b/components/compositing/constellation.rs index ea091994f87..f5cf8d1a0c8 100644 --- a/components/compositing/constellation.rs +++ b/components/compositing/constellation.rs @@ -11,26 +11,22 @@ use geom::point::Point2D; use geom::rect::{Rect, TypedRect}; use geom::scale_factor::ScaleFactor; use gfx::font_cache_task::FontCacheTask; -use layout_traits::LayoutTaskFactory; +use layout_traits::{LayoutControlMsg, LayoutTaskFactory}; use libc; -use script_traits::{CompositorEvent, ConstellationControlMsg}; -use script_traits::{ScriptControlChan, ScriptTaskFactory}; use msg::compositor_msg::LayerId; -use msg::constellation_msg::{self, ConstellationChan, Failure}; -use msg::constellation_msg::{IFrameSandboxState, NavigationDirection}; -use msg::constellation_msg::{Key, KeyState, KeyModifiers, LoadData}; -use msg::constellation_msg::{FrameId, PipelineExitType, PipelineId}; -use msg::constellation_msg::{SubpageId, WindowSizeData, MozBrowserEvent}; use msg::constellation_msg::Msg as ConstellationMsg; +use msg::constellation_msg::{FrameId, PipelineExitType, PipelineId}; +use msg::constellation_msg::{IFrameSandboxState, MozBrowserEvent, NavigationDirection}; +use msg::constellation_msg::{Key, KeyState, KeyModifiers, LoadData}; +use msg::constellation_msg::{SubpageId, WindowSizeData}; +use msg::constellation_msg::{self, ConstellationChan, Failure}; use net::image_cache_task::{ImageCacheTask, ImageCacheTaskClient}; use net::resource_task::{self, ResourceTask}; use net::storage_task::{StorageTask, StorageTaskMsg}; use profile::mem; use profile::time; -use util::cursor::Cursor; -use util::geometry::PagePx; -use util::opts; -use util::task::spawn_named; +use script_traits::{CompositorEvent, ConstellationControlMsg}; +use script_traits::{ScriptControlChan, ScriptTaskFactory}; use std::borrow::ToOwned; use std::collections::HashMap; use std::io::{self, Write}; @@ -38,6 +34,10 @@ use std::marker::PhantomData; use std::mem::replace; use std::sync::mpsc::{Receiver, channel}; use url::Url; +use util::cursor::Cursor; +use util::geometry::PagePx; +use util::opts; +use util::task::spawn_named; /// Maintains the pipelines and navigation context and grants permission to composite. pub struct Constellation { @@ -201,8 +201,10 @@ impl Constellation { time_profiler_chan: time_profiler_chan, mem_profiler_chan: mem_profiler_chan, window_size: WindowSizeData { - visible_viewport: opts::get().initial_window_size.as_f32() * ScaleFactor::new(1.0), - initial_viewport: opts::get().initial_window_size.as_f32() * ScaleFactor::new(1.0), + visible_viewport: opts::get().initial_window_size.as_f32() * + ScaleFactor::new(1.0), + initial_viewport: opts::get().initial_window_size.as_f32() * + ScaleFactor::new(1.0), device_pixel_ratio: ScaleFactor::new(1.0), }, phantom: PhantomData, @@ -321,7 +323,10 @@ impl Constellation { new_subpage_id, old_subpage_id, sandbox) => { - debug!("constellation got iframe URL load message {:?} {:?} {:?}", source_pipeline_id, old_subpage_id, new_subpage_id); + debug!("constellation got iframe URL load message {:?} {:?} {:?}", + source_pipeline_id, + old_subpage_id, + new_subpage_id); self.handle_script_loaded_url_in_iframe_msg(url, source_pipeline_id, new_subpage_id, @@ -329,6 +334,12 @@ impl Constellation { sandbox); } ConstellationMsg::SetCursor(cursor) => self.handle_set_cursor_msg(cursor), + ConstellationMsg::ChangeRunningAnimationsState(pipeline_id, animations_running) => { + self.handle_change_running_animations_state(pipeline_id, animations_running) + } + ConstellationMsg::TickAnimation(pipeline_id) => { + self.handle_tick_animation(pipeline_id) + } // Load a new page, usually -- but not always -- from a mouse click or typed url // If there is already a pending page (self.pending_frames), it will not be overridden; // However, if the id is not encompassed by another change, it will be. @@ -420,15 +431,19 @@ impl Constellation { debug!("creating replacement pipeline for about:failure"); let window_rect = self.pipeline(pipeline_id).rect; - let new_pipeline_id = self.new_pipeline(parent_info, window_rect, None, - LoadData::new(Url::parse("about:failure").unwrap())); + let new_pipeline_id = + self.new_pipeline(parent_info, + window_rect, + None, + LoadData::new(Url::parse("about:failure").unwrap())); self.push_pending_frame(new_pipeline_id, Some(pipeline_id)); } fn handle_init_load(&mut self, url: Url) { let window_rect = Rect(Point2D::zero(), self.window_size.visible_viewport); - let root_pipeline_id = self.new_pipeline(None, Some(window_rect), None, LoadData::new(url)); + let root_pipeline_id = + self.new_pipeline(None, Some(window_rect), None, LoadData::new(url)); self.push_pending_frame(root_pipeline_id, None); } @@ -509,6 +524,21 @@ impl Constellation { self.compositor_proxy.send(CompositorMsg::SetCursor(cursor)) } + fn handle_change_running_animations_state(&mut self, + pipeline_id: PipelineId, + animations_running: bool) { + self.compositor_proxy.send(CompositorMsg::ChangeRunningAnimationsState(pipeline_id, + animations_running)) + } + + fn handle_tick_animation(&mut self, pipeline_id: PipelineId) { + self.pipeline(pipeline_id) + .layout_chan + .0 + .send(LayoutControlMsg::TickAnimationsMsg) + .unwrap(); + } + fn handle_load_url_msg(&mut self, source_id: PipelineId, load_data: LoadData) { // If this load targets an iframe, its framing element may exist // in a separate script task than the framed document that initiated @@ -907,7 +937,9 @@ impl Constellation { } fn find_subpage(&mut self, containing_pipeline_id: PipelineId, subpage_id: SubpageId) -> &mut Pipeline { - let pipeline_id = *self.subpage_map.get(&(containing_pipeline_id, subpage_id)).expect("no subpage pipeline_id"); + let pipeline_id = *self.subpage_map + .get(&(containing_pipeline_id, subpage_id)) + .expect("no subpage pipeline_id"); self.mut_pipeline(pipeline_id) } } diff --git a/components/compositing/headless.rs b/components/compositing/headless.rs index 27cac151f31..72a5b0c961e 100644 --- a/components/compositing/headless.rs +++ b/components/compositing/headless.rs @@ -98,6 +98,7 @@ impl CompositorEventListener for NullCompositor { Msg::AssignPaintedBuffers(..) | Msg::ChangeReadyState(..) | Msg::ChangePaintState(..) | + Msg::ChangeRunningAnimationsState(..) | Msg::ScrollFragmentPoint(..) | Msg::LoadComplete | Msg::PaintMsgDiscarded(..) | diff --git a/components/compositing/pipeline.rs b/components/compositing/pipeline.rs index 4f9b54eed36..21c5c9460fb 100644 --- a/components/compositing/pipeline.rs +++ b/components/compositing/pipeline.rs @@ -31,6 +31,7 @@ pub struct Pipeline { pub id: PipelineId, pub parent_info: Option<(PipelineId, SubpageId)>, pub script_chan: ScriptControlChan, + /// A channel to layout, for performing reflows and shutdown. pub layout_chan: LayoutControlChan, pub paint_chan: PaintChan, pub layout_shutdown_port: Receiver<()>, @@ -40,6 +41,9 @@ pub struct Pipeline { /// The title of the most recently-loaded page. pub title: Option, pub rect: Option>, + /// Whether this pipeline is currently running animations. Pipelines that are running + /// animations cause composites to be continually scheduled. + pub running_animations: bool, pub children: Vec, } @@ -113,12 +117,14 @@ impl Pipeline { ScriptControlChan(script_chan) } Some(script_chan) => { - let (containing_pipeline_id, subpage_id) = parent_info.expect("script_pipeline != None but subpage_id == None"); + let (containing_pipeline_id, subpage_id) = + parent_info.expect("script_pipeline != None but subpage_id == None"); let new_layout_info = NewLayoutInfo { containing_pipeline_id: containing_pipeline_id, new_pipeline_id: id, subpage_id: subpage_id, - layout_chan: ScriptTaskFactory::clone_layout_channel(None::<&mut STF>, &layout_pair), + layout_chan: ScriptTaskFactory::clone_layout_channel(None::<&mut STF>, + &layout_pair), load_data: load_data.clone(), }; @@ -186,6 +192,7 @@ impl Pipeline { title: None, children: vec!(), rect: rect, + running_animations: false, } } diff --git a/components/gfx/paint_task.rs b/components/gfx/paint_task.rs index 470bea78e7b..d4a95b6e619 100644 --- a/components/gfx/paint_task.rs +++ b/components/gfx/paint_task.rs @@ -330,13 +330,13 @@ impl PaintTask where C: PaintListener + Send + 'static { }) } - /// Paints one layer and sends the tiles back to the layer. + /// Paints one layer and places the painted tiles in `replies`. fn paint(&mut self, replies: &mut Vec<(LayerId, Box)>, mut tiles: Vec, scale: f32, layer_id: LayerId) { - profile(time::ProfilerCategory::Painting, None, self.time_profiler_chan.clone(), || { + time::profile(time::ProfilerCategory::Painting, None, self.time_profiler_chan.clone(), || { // Bail out if there is no appropriate stacking context. let stacking_context = if let Some(ref stacking_context) = self.root_stacking_context { match display_list::find_stacking_context_with_layer_id(stacking_context, @@ -559,8 +559,10 @@ impl WorkerThread { paint_context.clear(); // Draw the display list. - profile(time::ProfilerCategory::PaintingPerTile, None, - self.time_profiler_sender.clone(), || { + time::profile(time::ProfilerCategory::PaintingPerTile, + None, + self.time_profiler_sender.clone(), + || { stacking_context.optimize_and_draw_into_context(&mut paint_context, &tile_bounds, &matrix, diff --git a/components/layout/Cargo.toml b/components/layout/Cargo.toml index 2eee5eb3d4a..88f28089a3f 100644 --- a/components/layout/Cargo.toml +++ b/components/layout/Cargo.toml @@ -61,9 +61,13 @@ git = "https://github.com/servo/string-cache" [dependencies.png] git = "https://github.com/servo/rust-png" +[dependencies.clock_ticks] +git = "https://github.com/tomaka/clock_ticks" + [dependencies] encoding = "0.2" url = "0.2.16" bitflags = "*" rustc-serialize = "0.3" libc = "*" + diff --git a/components/layout/animation.rs b/components/layout/animation.rs new file mode 100644 index 00000000000..571228afccb --- /dev/null +++ b/components/layout/animation.rs @@ -0,0 +1,104 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +//! CSS transitions and animations. + +use flow::{self, Flow}; +use incremental::{self, RestyleDamage}; + +use clock_ticks; +use gfx::display_list::OpaqueNode; +use layout_task::{LayoutTask, LayoutTaskData}; +use msg::constellation_msg::{Msg, PipelineId}; +use script::layout_interface::Animation; +use std::mem; +use std::sync::mpsc::Sender; +use style::animation::{GetMod, PropertyAnimation}; +use style::properties::ComputedValues; + +/// Inserts transitions into the queue of running animations as applicable for the given style +/// difference. This is called from the layout worker threads. +pub fn start_transitions_if_applicable(new_animations_sender: &Sender, + node: OpaqueNode, + old_style: &ComputedValues, + new_style: &mut ComputedValues) { + for i in range(0, new_style.get_animation().transition_property.0.len()) { + // Create any property animations, if applicable. + let property_animations = PropertyAnimation::from_transition(i, old_style, new_style); + for property_animation in property_animations.into_iter() { + // Set the property to the initial value. + property_animation.update(new_style, 0.0); + + // Kick off the animation. + let now = clock_ticks::precise_time_s(); + let animation_style = new_style.get_animation(); + let start_time = now + animation_style.transition_delay.0.get_mod(i).seconds(); + new_animations_sender.send(Animation { + node: node.id(), + property_animation: property_animation, + start_time: start_time, + end_time: start_time + + animation_style.transition_duration.0.get_mod(i).seconds(), + }).unwrap() + } + } +} + +/// Processes any new animations that were discovered after style recalculation. +pub fn process_new_animations(rw_data: &mut LayoutTaskData, pipeline_id: PipelineId) { + while let Ok(animation) = rw_data.new_animations_receiver.try_recv() { + rw_data.running_animations.push(animation) + } + + let animations_are_running = !rw_data.running_animations.is_empty(); + rw_data.constellation_chan + .0 + .send(Msg::ChangeRunningAnimationsState(pipeline_id, animations_are_running)) + .unwrap(); +} + +/// Recalculates style for an animation. This does *not* run with the DOM lock held. +pub fn recalc_style_for_animation(flow: &mut Flow, animation: &Animation) { + let mut damage = RestyleDamage::empty(); + flow.mutate_fragments(&mut |fragment| { + if fragment.node.id() != animation.node { + return + } + + let now = clock_ticks::precise_time_s(); + let mut progress = (now - animation.start_time) / animation.duration(); + if progress > 1.0 { + progress = 1.0 + } + if progress <= 0.0 { + return + } + + let mut new_style = fragment.style.clone(); + animation.property_animation.update(&mut *new_style.make_unique(), progress); + damage.insert(incremental::compute_damage(&Some(fragment.style.clone()), &new_style)); + fragment.style = new_style + }); + + let base = flow::mut_base(flow); + base.restyle_damage.insert(damage); + for kid in base.children.iter_mut() { + recalc_style_for_animation(kid, animation) + } +} + +/// Handles animation updates. +pub fn tick_all_animations(layout_task: &LayoutTask, rw_data: &mut LayoutTaskData) { + let running_animations = mem::replace(&mut rw_data.running_animations, Vec::new()); + let now = clock_ticks::precise_time_s(); + for running_animation in running_animations.into_iter() { + layout_task.tick_animation(running_animation, rw_data); + + if now < running_animation.end_time { + // Keep running the animation if it hasn't expired. + rw_data.running_animations.push(running_animation) + } + } +} + diff --git a/components/layout/context.rs b/components/layout/context.rs index abb510df0d2..ae1c75e89e6 100644 --- a/components/layout/context.rs +++ b/components/layout/context.rs @@ -10,19 +10,20 @@ use css::matching::{ApplicableDeclarationsCache, StyleSharingCandidateCache}; use geom::{Rect, Size2D}; use gfx::display_list::OpaqueNode; -use gfx::font_context::FontContext; use gfx::font_cache_task::FontCacheTask; -use script::layout_interface::LayoutChan; -use script_traits::UntrustedNodeAddress; +use gfx::font_context::FontContext; use msg::constellation_msg::ConstellationChan; use net::local_image_cache::LocalImageCache; -use util::geometry::Au; +use script::layout_interface::{Animation, LayoutChan}; +use script_traits::UntrustedNodeAddress; use std::boxed; use std::cell::Cell; use std::ptr; +use std::sync::mpsc::Sender; use std::sync::{Arc, Mutex}; use style::selector_matching::Stylist; use url::Url; +use util::geometry::Au; struct LocalLayoutContext { font_context: FontContext, @@ -32,7 +33,8 @@ struct LocalLayoutContext { thread_local!(static LOCAL_CONTEXT_KEY: Cell<*mut LocalLayoutContext> = Cell::new(ptr::null_mut())); -fn create_or_get_local_context(shared_layout_context: &SharedLayoutContext) -> *mut LocalLayoutContext { +fn create_or_get_local_context(shared_layout_context: &SharedLayoutContext) + -> *mut LocalLayoutContext { LOCAL_CONTEXT_KEY.with(|ref r| { if r.get().is_null() { let context = box LocalLayoutContext { @@ -51,6 +53,7 @@ fn create_or_get_local_context(shared_layout_context: &SharedLayoutContext) -> * }) } +/// Layout information shared among all workers. This must be thread-safe. pub struct SharedLayoutContext { /// The local image cache. pub image_cache: Arc>>, @@ -76,7 +79,7 @@ pub struct SharedLayoutContext { pub stylist: *const Stylist, /// The root node at which we're starting the layout. - pub reflow_root: OpaqueNode, + pub reflow_root: Option, /// The URL. pub url: Url, @@ -87,9 +90,14 @@ pub struct SharedLayoutContext { /// Starts at zero, and increased by one every time a layout completes. /// This can be used to easily check for invalid stale data. pub generation: u32, + + /// A channel on which new animations that have been triggered by style recalculation can be + /// sent. + pub new_animations_sender: Sender, } pub struct SharedLayoutContextWrapper(pub *const SharedLayoutContext); + unsafe impl Send for SharedLayoutContextWrapper {} pub struct LayoutContext<'a> { diff --git a/components/layout/css/matching.rs b/components/layout/css/matching.rs index 400f8693231..1c977c8081b 100644 --- a/components/layout/css/matching.rs +++ b/components/layout/css/matching.rs @@ -6,29 +6,33 @@ #![allow(unsafe_code)] +use animation; use context::SharedLayoutContext; use css::node_style::StyledNode; -use incremental::{self, RestyleDamage}; use data::{LayoutDataAccess, LayoutDataWrapper}; +use incremental::{self, RestyleDamage}; +use opaque_node::OpaqueNodeMethods; use wrapper::{LayoutElement, LayoutNode, TLayoutNode}; use script::dom::node::NodeTypeId; +use script::layout_interface::Animation; use selectors::bloom::BloomFilter; -use util::cache::{LRUCache, SimpleHashCache}; -use util::smallvec::{SmallVec, SmallVec16}; -use util::arc_ptr_eq; -use std::borrow::ToOwned; -use std::mem; -use std::hash::{Hash, Hasher}; -use std::slice::Iter; -use string_cache::{Atom, Namespace}; -use selectors::parser::PseudoElement; -use style::selector_matching::{Stylist, DeclarationBlock}; -use style::node::{TElement, TNode}; -use style::properties::{ComputedValues, cascade}; use selectors::matching::{CommonStyleAffectingAttributeMode, CommonStyleAffectingAttributes}; use selectors::matching::{common_style_affecting_attributes, rare_style_affecting_attributes}; +use selectors::parser::PseudoElement; +use std::borrow::ToOwned; +use std::hash::{Hash, Hasher}; +use std::mem; +use std::slice::Iter; use std::sync::Arc; +use std::sync::mpsc::Sender; +use string_cache::{Atom, Namespace}; +use style::node::{TElement, TNode}; +use style::properties::{ComputedValues, cascade}; +use style::selector_matching::{Stylist, DeclarationBlock}; +use util::arc_ptr_eq; +use util::cache::{LRUCache, SimpleHashCache}; +use util::smallvec::{SmallVec, SmallVec16}; pub struct ApplicableDeclarations { pub normal: SmallVec16, @@ -399,7 +403,8 @@ pub trait MatchMethods { layout_context: &SharedLayoutContext, parent: Option, applicable_declarations: &ApplicableDeclarations, - applicable_declarations_cache: &mut ApplicableDeclarationsCache); + applicable_declarations_cache: &mut ApplicableDeclarationsCache, + new_animations_sender: &Sender); } trait PrivateMatchMethods { @@ -408,8 +413,9 @@ trait PrivateMatchMethods { parent_style: Option<&Arc>, applicable_declarations: &[DeclarationBlock], style: &mut Option>, - applicable_declarations_cache: &mut - ApplicableDeclarationsCache, + applicable_declarations_cache: + &mut ApplicableDeclarationsCache, + new_animations_sender: &Sender, shareable: bool) -> RestyleDamage; @@ -425,11 +431,12 @@ impl<'ln> PrivateMatchMethods for LayoutNode<'ln> { parent_style: Option<&Arc>, applicable_declarations: &[DeclarationBlock], style: &mut Option>, - applicable_declarations_cache: &mut - ApplicableDeclarationsCache, + applicable_declarations_cache: + &mut ApplicableDeclarationsCache, + new_animations_sender: &Sender, shareable: bool) -> RestyleDamage { - let this_style; + let mut this_style; let cacheable; match parent_style { Some(ref parent_style) => { @@ -444,7 +451,7 @@ impl<'ln> PrivateMatchMethods for LayoutNode<'ln> { Some(&***parent_style), cached_computed_values); cacheable = is_cacheable; - this_style = Arc::new(the_style); + this_style = the_style } None => { let (the_style, is_cacheable) = cascade(layout_context.screen_size, @@ -453,22 +460,39 @@ impl<'ln> PrivateMatchMethods for LayoutNode<'ln> { None, None); cacheable = is_cacheable; - this_style = Arc::new(the_style); + this_style = the_style } }; + // Trigger transitions if necessary. This will reset `this_style` back to its old value if + // it did trigger a transition. + match *style { + None => { + // This is a newly-created node; we've nothing to transition from! + } + Some(ref style) => { + let node = OpaqueNodeMethods::from_layout_node(self); + animation::start_transitions_if_applicable(new_animations_sender, + node, + &**style, + &mut this_style); + } + } + + // Calculate style difference. + let this_style = Arc::new(this_style); + let damage = incremental::compute_damage(style, &*this_style); + // Cache the resolved style if it was cacheable. if cacheable { applicable_declarations_cache.insert(applicable_declarations.to_vec(), this_style.clone()); } - // Calculate style difference and write. - let damage = incremental::compute_damage(style, &*this_style); + // Write in the final style and return the damage done to our caller. *style = Some(this_style); damage } - fn share_style_with_candidate_if_possible(&self, parent_node: Option, candidate: &StyleSharingCandidate) @@ -615,7 +639,8 @@ impl<'ln> MatchMethods for LayoutNode<'ln> { layout_context: &SharedLayoutContext, parent: Option, applicable_declarations: &ApplicableDeclarations, - applicable_declarations_cache: &mut ApplicableDeclarationsCache) { + applicable_declarations_cache: &mut ApplicableDeclarationsCache, + new_animations_sender: &Sender) { // Get our parent's style. This must be unsafe so that we don't touch the parent's // borrow flags. // @@ -625,8 +650,12 @@ impl<'ln> MatchMethods for LayoutNode<'ln> { None => None, Some(parent_node) => { let parent_layout_data_ref = parent_node.borrow_layout_data_unchecked(); - let parent_layout_data = (&*parent_layout_data_ref).as_ref().expect("no parent data!?"); - let parent_style = parent_layout_data.shared_data.style.as_ref().expect("parent hasn't been styled yet!"); + let parent_layout_data = (&*parent_layout_data_ref).as_ref() + .expect("no parent data!?"); + let parent_style = parent_layout_data.shared_data + .style + .as_ref() + .expect("parent hasn't been styled yet!"); Some(parent_style) } }; @@ -651,6 +680,7 @@ impl<'ln> MatchMethods for LayoutNode<'ln> { applicable_declarations.normal.as_slice(), &mut layout_data.shared_data.style, applicable_declarations_cache, + new_animations_sender, applicable_declarations.normal_shareable); if applicable_declarations.before.len() > 0 { damage = damage | self.cascade_node_pseudo_element( @@ -659,6 +689,7 @@ impl<'ln> MatchMethods for LayoutNode<'ln> { &*applicable_declarations.before, &mut layout_data.data.before_style, applicable_declarations_cache, + new_animations_sender, false); } if applicable_declarations.after.len() > 0 { @@ -668,6 +699,7 @@ impl<'ln> MatchMethods for LayoutNode<'ln> { &*applicable_declarations.after, &mut layout_data.data.after_style, applicable_declarations_cache, + new_animations_sender, false); } layout_data.data.restyle_damage = damage; diff --git a/components/layout/layout_task.rs b/components/layout/layout_task.rs index e535d6f9742..4016c3173f8 100644 --- a/components/layout/layout_task.rs +++ b/components/layout/layout_task.rs @@ -7,9 +7,9 @@ #![allow(unsafe_code)] +use animation; use construct::ConstructionResult; use context::{SharedLayoutContext, SharedLayoutContextWrapper}; -use css::node_style::StyledNode; use display_list_builder::ToGfxColor; use flow::{self, Flow, ImmutableFlowUtils, MutableFlowUtils, MutableOwnedFlowUtils}; use flow_ref::FlowRef; @@ -49,11 +49,10 @@ use profile::time::{self, ProfilerMetadata, profile}; use profile::time::{TimerMetadataFrameType, TimerMetadataReflowType}; use script::dom::bindings::js::LayoutJS; use script::dom::node::{LayoutData, Node}; -use script::layout_interface::ReflowQueryType; -use script::layout_interface::{ContentBoxResponse, ContentBoxesResponse}; +use script::layout_interface::{Animation, ContentBoxResponse, ContentBoxesResponse}; use script::layout_interface::{HitTestResponse, LayoutChan, LayoutRPC}; -use script::layout_interface::{MouseOverResponse, Msg}; -use script::layout_interface::{Reflow, ReflowGoal, ScriptLayoutChan, TrustedNodeAddress}; +use script::layout_interface::{MouseOverResponse, Msg, Reflow, ReflowGoal, ReflowQueryType}; +use script::layout_interface::{ScriptLayoutChan, ScriptReflow, TrustedNodeAddress}; use script_traits::{ConstellationControlMsg, CompositorEvent, OpaqueScriptLayoutChannel}; use script_traits::{ScriptControlChan, UntrustedNodeAddress}; use std::borrow::ToOwned; @@ -70,7 +69,7 @@ use style::selector_matching::Stylist; use style::stylesheets::{Origin, Stylesheet, iter_font_face_rules}; use url::Url; use util::cursor::Cursor; -use util::geometry::Au; +use util::geometry::{Au, MAX_RECT}; use util::logical_geometry::LogicalPoint; use util::mem::HeapSizeOf; use util::opts; @@ -83,6 +82,9 @@ use util::workqueue::WorkQueue; /// /// This needs to be protected by a mutex so we can do fast RPCs. pub struct LayoutTaskData { + /// The root of the flow tree. + pub root_flow: Option, + /// The local image cache. pub local_image_cache: Arc>>, @@ -113,6 +115,16 @@ pub struct LayoutTaskData { /// A queued response for the content boxes of a node. pub content_boxes_response: Vec>, + + /// The list of currently-running animations. + pub running_animations: Vec, + + /// Receives newly-discovered animations. + pub new_animations_receiver: Receiver, + + /// A channel on which new animations that have been triggered by style recalculation can be + /// sent. + pub new_animations_sender: Sender, } /// Information needed by the layout task. @@ -129,7 +141,7 @@ pub struct LayoutTask { /// The port on which we receive messages from the constellation pub pipeline_port: Receiver, - //// The channel to send messages to ourself. + /// The channel on which we or others can send messages to ourselves. pub chan: LayoutChan, /// The channel on which messages can be sent to the constellation. @@ -192,39 +204,37 @@ impl ImageResponder for LayoutImageResponder { impl LayoutTaskFactory for LayoutTask { /// Spawns a new layout task. fn create(_phantom: Option<&mut LayoutTask>, - id: PipelineId, - url: Url, - chan: OpaqueScriptLayoutChannel, - pipeline_port: Receiver, - constellation_chan: ConstellationChan, - failure_msg: Failure, - script_chan: ScriptControlChan, - paint_chan: PaintChan, - resource_task: ResourceTask, - img_cache_task: ImageCacheTask, - font_cache_task: FontCacheTask, - time_profiler_chan: time::ProfilerChan, - mem_profiler_chan: mem::ProfilerChan, - shutdown_chan: Sender<()>) { + id: PipelineId, + url: Url, + chan: OpaqueScriptLayoutChannel, + pipeline_port: Receiver, + constellation_chan: ConstellationChan, + failure_msg: Failure, + script_chan: ScriptControlChan, + paint_chan: PaintChan, + resource_task: ResourceTask, + img_cache_task: ImageCacheTask, + font_cache_task: FontCacheTask, + time_profiler_chan: time::ProfilerChan, + memory_profiler_chan: mem::ProfilerChan, + shutdown_chan: Sender<()>) { let ConstellationChan(con_chan) = constellation_chan.clone(); spawn_named_with_send_on_failure("LayoutTask", task_state::LAYOUT, move || { { // Ensures layout task is destroyed before we send shutdown message let sender = chan.sender(); - let layout = - LayoutTask::new( - id, - url, - chan.receiver(), - LayoutChan(sender), - pipeline_port, - constellation_chan, - script_chan, - paint_chan, - resource_task, - img_cache_task, - font_cache_task, - time_profiler_chan, - mem_profiler_chan); + let layout = LayoutTask::new(id, + url, + chan.receiver(), + LayoutChan(sender), + pipeline_port, + constellation_chan, + script_chan, + paint_chan, + resource_task, + img_cache_task, + font_cache_task, + time_profiler_chan, + memory_profiler_chan); layout.start(); } shutdown_chan.send(()).unwrap(); @@ -297,6 +307,10 @@ impl LayoutTask { let reporter_name = format!("layout-reporter-{}", id.0); mem_profiler_chan.send(mem::ProfilerMsg::RegisterReporter(reporter_name.clone(), reporter)); + + // Create the channel on which new animations can be sent. + let (new_animations_sender, new_animations_receiver) = channel(); + LayoutTask { id: id, url: url, @@ -315,6 +329,7 @@ impl LayoutTask { first_reflow: Cell::new(true), rw_data: Arc::new(Mutex::new( LayoutTaskData { + root_flow: None, local_image_cache: local_image_cache, constellation_chan: constellation_chan, screen_size: screen_size, @@ -325,6 +340,9 @@ impl LayoutTask { generation: 0, content_box_response: Rect::zero(), content_boxes_response: Vec::new(), + running_animations: Vec::new(), + new_animations_receiver: new_animations_receiver, + new_animations_sender: new_animations_sender, })), } } @@ -341,7 +359,7 @@ impl LayoutTask { fn build_shared_layout_context(&self, rw_data: &LayoutTaskData, screen_size_changed: bool, - reflow_root: &LayoutNode, + reflow_root: Option<&LayoutNode>, url: &Url) -> SharedLayoutContext { SharedLayoutContext { @@ -353,9 +371,10 @@ impl LayoutTask { font_cache_task: self.font_cache_task.clone(), stylist: &*rw_data.stylist, url: (*url).clone(), - reflow_root: OpaqueNodeMethods::from_layout_node(reflow_root), + reflow_root: reflow_root.map(|node| OpaqueNodeMethods::from_layout_node(node)), dirty: Rect::zero(), generation: rw_data.generation, + new_animations_sender: rw_data.new_animations_sender.clone(), } } @@ -389,8 +408,12 @@ impl LayoutTask { match port_to_read { PortToRead::Pipeline => { match self.pipeline_port.recv().unwrap() { + LayoutControlMsg::TickAnimationsMsg => { + self.handle_request_helper(Msg::TickAnimations, possibly_locked_rw_data) + } LayoutControlMsg::ExitNowMsg(exit_type) => { - self.handle_request_helper(Msg::ExitNow(exit_type), possibly_locked_rw_data) + self.handle_request_helper(Msg::ExitNow(exit_type), + possibly_locked_rw_data) } } }, @@ -434,8 +457,12 @@ impl LayoutTask { LayoutTaskData>>) -> bool { match request { - Msg::AddStylesheet(sheet, mq) => self.handle_add_stylesheet(sheet, mq, possibly_locked_rw_data), - Msg::LoadStylesheet(url, mq) => self.handle_load_stylesheet(url, mq, possibly_locked_rw_data), + Msg::AddStylesheet(sheet, mq) => { + self.handle_add_stylesheet(sheet, mq, possibly_locked_rw_data) + } + Msg::LoadStylesheet(url, mq) => { + self.handle_load_stylesheet(url, mq, possibly_locked_rw_data) + } Msg::SetQuirksMode => self.handle_set_quirks_mode(possibly_locked_rw_data), Msg::GetRPC(response_chan) => { response_chan.send(box LayoutRPCImpl(self.rw_data.clone()) as @@ -443,10 +470,11 @@ impl LayoutTask { }, Msg::Reflow(data) => { profile(time::ProfilerCategory::LayoutPerform, - self.profiler_metadata(&*data), + self.profiler_metadata(&data.reflow_info), self.time_profiler_chan.clone(), || self.handle_reflow(&*data, possibly_locked_rw_data)); }, + Msg::TickAnimations => self.tick_all_animations(possibly_locked_rw_data), Msg::ReapLayoutData(dead_layout_data) => { unsafe { self.handle_reap_layout_data(dead_layout_data) @@ -486,9 +514,9 @@ impl LayoutTask { reports_chan.send(reports); } - /// Enters a quiescent state in which no new messages except for `layout_interface::Msg::ReapLayoutData` will be - /// processed until an `ExitNowMsg` is received. A pong is immediately sent on the given - /// response channel. + /// Enters a quiescent state in which no new messages except for + /// `layout_interface::Msg::ReapLayoutData` will be processed until an `ExitNowMsg` is + /// received. A pong is immediately sent on the given response channel. fn prepare_to_exit<'a>(&'a self, response_chan: Sender<()>, possibly_locked_rw_data: &mut Option>) { @@ -586,7 +614,6 @@ impl LayoutTask { LayoutTask::return_rw_data(possibly_locked_rw_data, rw_data); } - /// Retrieves the flow tree root from the root node. fn try_get_layout_root(&self, node: LayoutNode) -> Option { let mut layout_data_ref = node.mutate_layout_data(); let layout_data = @@ -698,7 +725,7 @@ impl LayoutTask { data: &Reflow, layout_root: &mut FlowRef, shared_layout_context: &mut SharedLayoutContext, - rw_data: &mut RWGuard<'a>) { + rw_data: &mut LayoutTaskData) { let writing_mode = flow::base(&**layout_root).writing_mode; profile(time::ProfilerCategory::LayoutDispListBuild, self.profiler_metadata(data), @@ -714,7 +741,6 @@ impl LayoutTask { flow::mut_base(&mut **layout_root).clip = ClippingRegion::from_rect(&data.page_clip_rect); - let rw_data = &mut **rw_data; match rw_data.parallel_traversal { None => { sequential::build_display_list_for_subtree(layout_root, shared_layout_context); @@ -767,7 +793,7 @@ impl LayoutTask { /// The high-level routine that performs layout tasks. fn handle_reflow<'a>(&'a self, - data: &Reflow, + data: &ScriptReflow, possibly_locked_rw_data: &mut Option>) { // FIXME: Isolate this transmutation into a "bridge" module. // FIXME(rust#16366): The following line had to be moved because of a @@ -779,8 +805,7 @@ impl LayoutTask { transmute(&mut node) }; - debug!("layout: received layout request for: {}", data.url.serialize()); - debug!("layout: parsed Node tree"); + debug!("layout: received layout request for: {}", data.reflow_info.url.serialize()); if log_enabled!(log::DEBUG) { node.dump(); } @@ -796,7 +821,6 @@ impl LayoutTask { // TODO: Calculate the "actual viewport": // http://www.w3.org/TR/css-device-adapt/#actual-viewport let viewport_size = data.window_size.initial_viewport; - let old_screen_size = rw_data.screen_size; let current_screen_size = Size2D(Au::from_frac32_px(viewport_size.width.get()), Au::from_frac32_px(viewport_size.height.get())); @@ -804,23 +828,19 @@ impl LayoutTask { // Handle conditions where the entire flow tree is invalid. let screen_size_changed = current_screen_size != old_screen_size; - if screen_size_changed { let device = Device::new(MediaType::Screen, data.window_size.initial_viewport); rw_data.stylist.set_device(device); } - let needs_dirtying = rw_data.stylist.update(); - // If the entire flow tree is invalid, then it will be reflowed anyhow. + let needs_dirtying = rw_data.stylist.update(); let needs_reflow = screen_size_changed && !needs_dirtying; - unsafe { if needs_dirtying { LayoutTask::dirty_all_nodes(node); } } - if needs_reflow { match self.try_get_layout_root(*node) { None => {} @@ -833,13 +853,14 @@ impl LayoutTask { // Create a layout context for use throughout the following passes. let mut shared_layout_context = self.build_shared_layout_context(&*rw_data, screen_size_changed, - node, - &data.url); + Some(&node), + &data.reflow_info.url); - let mut layout_root = profile(time::ProfilerCategory::LayoutStyleRecalc, - self.profiler_metadata(data), - self.time_profiler_chan.clone(), - || { + // Recalculate CSS styles and rebuild flows and fragments. + profile(time::ProfilerCategory::LayoutStyleRecalc, + self.profiler_metadata(&data.reflow_info), + self.time_profiler_chan.clone(), + || { // Perform CSS selector matching and flow construction. let rw_data = &mut *rw_data; match rw_data.parallel_traversal { @@ -847,39 +868,105 @@ impl LayoutTask { sequential::traverse_dom_preorder(*node, &shared_layout_context); } Some(ref mut traversal) => { - parallel::traverse_dom_preorder(*node, &shared_layout_context, traversal) + parallel::traverse_dom_preorder(*node, &shared_layout_context, traversal); } } - - self.get_layout_root((*node).clone()) }); + // Retrieve the (possibly rebuilt) root flow. + rw_data.root_flow = Some(self.get_layout_root((*node).clone())); + + // Kick off animations if any were triggered. + animation::process_new_animations(&mut *rw_data, self.id); + + // Perform post-style recalculation layout passes. + self.perform_post_style_recalc_layout_passes(&data.reflow_info, + &mut rw_data, + &mut shared_layout_context); + + let mut root_flow = (*rw_data.root_flow.as_ref().unwrap()).clone(); + match data.query_type { + ReflowQueryType::ContentBoxQuery(node) => { + self.process_content_box_request(node, &mut root_flow, &mut rw_data) + } + ReflowQueryType::ContentBoxesQuery(node) => { + self.process_content_boxes_request(node, &mut root_flow, &mut rw_data) + } + ReflowQueryType::NoQuery => {} + } + + + // Tell script that we're done. + // + // FIXME(pcwalton): This should probably be *one* channel, but we can't fix this without + // either select or a filtered recv() that only looks for messages of a given type. + data.script_join_chan.send(()).unwrap(); + let ScriptControlChan(ref chan) = data.script_chan; + chan.send(ConstellationControlMsg::ReflowComplete(self.id, data.id)).unwrap(); + } + + fn tick_all_animations<'a>(&'a self, + possibly_locked_rw_data: &mut Option>) { + let mut rw_data = self.lock_rw_data(possibly_locked_rw_data); + animation::tick_all_animations(self, &mut rw_data) + } + + pub fn tick_animation<'a>(&'a self, animation: Animation, rw_data: &mut LayoutTaskData) { + // FIXME(#5466, pcwalton): These data are lies. + let reflow_info = Reflow { + goal: ReflowGoal::ForDisplay, + url: Url::parse("http://animation.com/").unwrap(), + iframe: false, + page_clip_rect: MAX_RECT, + }; + + // Perform an abbreviated style recalc that operates without access to the DOM. + let mut layout_context = self.build_shared_layout_context(&*rw_data, + false, + None, + &reflow_info.url); + let mut root_flow = (*rw_data.root_flow.as_ref().unwrap()).clone(); + profile(time::ProfilerCategory::LayoutStyleRecalc, + self.profiler_metadata(&reflow_info), + self.time_profiler_chan.clone(), + || animation::recalc_style_for_animation(root_flow.deref_mut(), &animation)); + + self.perform_post_style_recalc_layout_passes(&reflow_info, + &mut *rw_data, + &mut layout_context); + } + + fn perform_post_style_recalc_layout_passes<'a>(&'a self, + data: &Reflow, + rw_data: &mut LayoutTaskData, + layout_context: &mut SharedLayoutContext) { + let mut root_flow = (*rw_data.root_flow.as_ref().unwrap()).clone(); profile(time::ProfilerCategory::LayoutRestyleDamagePropagation, self.profiler_metadata(data), self.time_profiler_chan.clone(), || { - if opts::get().nonincremental_layout || layout_root.compute_layout_damage() - .contains(REFLOW_ENTIRE_DOCUMENT) { - layout_root.reflow_entire_document() + if opts::get().nonincremental_layout || root_flow.deref_mut() + .compute_layout_damage() + .contains(REFLOW_ENTIRE_DOCUMENT) { + root_flow.deref_mut().reflow_entire_document() } }); // Verification of the flow tree, which ensures that all nodes were either marked as leaves // or as non-leaves. This becomes a no-op in release builds. (It is inconsequential to // memory safety but is a useful debugging tool.) - self.verify_flow_tree(&mut layout_root); + self.verify_flow_tree(&mut root_flow); if opts::get().trace_layout { - layout_debug::begin_trace(layout_root.clone()); + layout_debug::begin_trace(root_flow.clone()); } // Resolve generated content. profile(time::ProfilerCategory::LayoutGeneratedContent, self.profiler_metadata(data), self.time_profiler_chan.clone(), - || { - sequential::resolve_generated_content(&mut layout_root, &shared_layout_context) - }); + || sequential::resolve_generated_content(&mut root_flow, &layout_context)); // Perform the primary layout passes over the flow tree to compute the locations of all // the boxes. @@ -887,18 +974,17 @@ impl LayoutTask { self.profiler_metadata(data), self.time_profiler_chan.clone(), || { - let rw_data = &mut *rw_data; match rw_data.parallel_traversal { None => { // Sequential mode. - self.solve_constraints(&mut layout_root, &shared_layout_context) + self.solve_constraints(&mut root_flow, &layout_context) } Some(_) => { // Parallel mode. self.solve_constraints_parallel(data, rw_data, - &mut layout_root, - &mut shared_layout_context); + &mut root_flow, + &mut *layout_context); } } }); @@ -907,23 +993,13 @@ impl LayoutTask { match data.goal { ReflowGoal::ForDisplay => { self.build_display_list_for_reflow(data, - &mut layout_root, - &mut shared_layout_context, - &mut rw_data); + &mut root_flow, + &mut *layout_context, + rw_data); } ReflowGoal::ForScriptQuery => {} } - match data.query_type { - ReflowQueryType::ContentBoxQuery(node) => { - self.process_content_box_request(node, &mut layout_root, &mut rw_data) - } - ReflowQueryType::ContentBoxesQuery(node) => { - self.process_content_boxes_request(node, &mut layout_root, &mut rw_data) - } - ReflowQueryType::NoQuery => {} - } - self.first_reflow.set(false); if opts::get().trace_layout { @@ -931,18 +1007,10 @@ impl LayoutTask { } if opts::get().dump_flow_tree { - layout_root.dump(); + root_flow.dump(); } rw_data.generation += 1; - - // Tell script that we're done. - // - // FIXME(pcwalton): This should probably be *one* channel, but we can't fix this without - // either select or a filtered recv() that only looks for messages of a given type. - data.script_join_chan.send(()).unwrap(); - let ScriptControlChan(ref chan) = data.script_chan; - chan.send(ConstellationControlMsg::ReflowComplete(self.id, data.id)).unwrap(); } unsafe fn dirty_all_nodes(node: &mut LayoutNode) { diff --git a/components/layout/lib.rs b/components/layout/lib.rs index d2a407a2a70..646daab0480 100644 --- a/components/layout/lib.rs +++ b/components/layout/lib.rs @@ -25,40 +25,45 @@ #[macro_use] extern crate log; -#[macro_use]extern crate bitflags; -extern crate azure; -extern crate cssparser; -extern crate canvas; -extern crate geom; -extern crate gfx; -extern crate layout_traits; -extern crate script; -extern crate script_traits; -extern crate "rustc-serialize" as rustc_serialize; -extern crate png; -extern crate style; +#[macro_use] +extern crate bitflags; + #[macro_use] #[no_link] extern crate "plugins" as servo_plugins; -extern crate net; -extern crate msg; + #[macro_use] extern crate profile; -extern crate selectors; + #[macro_use] extern crate util; -extern crate string_cache; - +extern crate "rustc-serialize" as rustc_serialize; extern crate alloc; +extern crate azure; +extern crate canvas; +extern crate clock_ticks; extern crate collections; +extern crate cssparser; extern crate encoding; +extern crate geom; +extern crate gfx; +extern crate layout_traits; extern crate libc; +extern crate msg; +extern crate net; +extern crate png; +extern crate script; +extern crate script_traits; +extern crate selectors; +extern crate string_cache; +extern crate style; extern crate url; // Listed first because of macro definitions pub mod layout_debug; +pub mod animation; pub mod block; pub mod construct; pub mod context; diff --git a/components/layout/traversal.rs b/components/layout/traversal.rs index 4474cc684a6..72c1ade2062 100644 --- a/components/layout/traversal.rs +++ b/components/layout/traversal.rs @@ -180,7 +180,8 @@ impl<'a> PreorderDomTraversal for RecalcStyleForNode<'a> { node.cascade_node(self.layout_context.shared, parent_opt, &applicable_declarations, - self.layout_context.applicable_declarations_cache()); + self.layout_context.applicable_declarations_cache(), + &self.layout_context.shared.new_animations_sender); } // Add ourselves to the LRU cache. diff --git a/components/layout/wrapper.rs b/components/layout/wrapper.rs index 01128cd808b..a99dcebe187 100644 --- a/components/layout/wrapper.rs +++ b/components/layout/wrapper.rs @@ -333,11 +333,16 @@ impl<'ln> LayoutNode<'ln> { /// While doing a reflow, the node at the root has no parent, as far as we're /// concerned. This method returns `None` at the reflow root. pub fn layout_parent_node(self, shared: &SharedLayoutContext) -> Option> { - let opaque_node: OpaqueNode = OpaqueNodeMethods::from_layout_node(&self); - if opaque_node == shared.reflow_root { - None - } else { - self.parent_node() + match shared.reflow_root { + None => panic!("layout_parent_node(): This layout has no access to the DOM!"), + Some(reflow_root) => { + let opaque_node: OpaqueNode = OpaqueNodeMethods::from_layout_node(&self); + if opaque_node == reflow_root { + None + } else { + self.parent_node() + } + } } } diff --git a/components/layout_traits/lib.rs b/components/layout_traits/lib.rs index 966d940d4cb..c36de735d19 100644 --- a/components/layout_traits/lib.rs +++ b/components/layout_traits/lib.rs @@ -29,6 +29,7 @@ use url::Url; /// Messages sent to the layout task from the constellation pub enum LayoutControlMsg { ExitNowMsg(PipelineExitType), + TickAnimationsMsg, } /// A channel wrapper for constellation messages diff --git a/components/msg/constellation_msg.rs b/components/msg/constellation_msg.rs index aa0cc4230f3..d1d2822e3fc 100644 --- a/components/msg/constellation_msg.rs +++ b/components/msg/constellation_msg.rs @@ -214,6 +214,10 @@ pub enum Msg { SetCursor(Cursor), /// Dispatch a mozbrowser event to a given iframe. Only available in experimental mode. MozBrowserEventMsg(PipelineId, SubpageId, MozBrowserEvent), + /// Indicates whether this pipeline is currently running animations. + ChangeRunningAnimationsState(PipelineId, bool), + /// Requests that the constellation instruct layout to begin a new tick of the animation. + TickAnimation(PipelineId), } // https://developer.mozilla.org/en-US/docs/Web/API/Using_the_Browser_API#Events diff --git a/components/script/dom/webidls/CSSStyleDeclaration.webidl b/components/script/dom/webidls/CSSStyleDeclaration.webidl index 3be5abe1099..1da7946338b 100644 --- a/components/script/dom/webidls/CSSStyleDeclaration.webidl +++ b/components/script/dom/webidls/CSSStyleDeclaration.webidl @@ -186,4 +186,10 @@ partial interface CSSStyleDeclaration { [TreatNullAs=EmptyString] attribute DOMString zIndex; [TreatNullAs=EmptyString] attribute DOMString imageRendering; + + [TreatNullAs=EmptyString] attribute DOMString transition; + [TreatNullAs=EmptyString] attribute DOMString transitionDuration; + [TreatNullAs=EmptyString] attribute DOMString transitionTimingFunction; + [TreatNullAs=EmptyString] attribute DOMString transitionProperty; + [TreatNullAs=EmptyString] attribute DOMString transitionDelay; }; diff --git a/components/script/dom/window.rs b/components/script/dom/window.rs index 675af64d5cb..08e526df282 100644 --- a/components/script/dom/window.rs +++ b/components/script/dom/window.rs @@ -27,7 +27,7 @@ use dom::performance::Performance; use dom::screen::Screen; use dom::storage::Storage; use layout_interface::{ReflowGoal, ReflowQueryType, LayoutRPC, LayoutChan, Reflow, Msg}; -use layout_interface::{ContentBoxResponse, ContentBoxesResponse}; +use layout_interface::{ContentBoxResponse, ContentBoxesResponse, ScriptReflow}; use page::Page; use script_task::{TimerSource, ScriptChan}; use script_task::ScriptMsg; @@ -564,17 +564,19 @@ impl<'a> WindowHelpers for JSRef<'a, Window> { } // Send new document and relevant styles to layout. - let reflow = box Reflow { + let reflow = box ScriptReflow { + reflow_info: Reflow { + goal: goal, + url: self.get_url(), + iframe: self.parent_info.is_some(), + page_clip_rect: self.page_clip_rect.get(), + }, document_root: root.to_trusted_node_address(), - url: self.get_url(), - iframe: self.parent_info.is_some(), - goal: goal, window_size: window_size, script_chan: self.control_chan.clone(), script_join_chan: join_chan, id: last_reflow_id.get(), query_type: query_type, - page_clip_rect: self.page_clip_rect.get(), }; let LayoutChan(ref chan) = self.layout_chan; diff --git a/components/script/layout_interface.rs b/components/script/layout_interface.rs index 9b7e3a292ea..a0062a287b6 100644 --- a/components/script/layout_interface.rs +++ b/components/script/layout_interface.rs @@ -10,14 +10,16 @@ use dom::node::LayoutData; use geom::point::Point2D; use geom::rect::Rect; +use libc::uintptr_t; use msg::constellation_msg::{PipelineExitType, WindowSizeData}; use profile::mem::{Reporter, ReportsChan}; use script_traits::{ScriptControlChan, OpaqueScriptLayoutChannel, UntrustedNodeAddress}; use std::any::Any; use std::sync::mpsc::{channel, Receiver, Sender}; use std::boxed::BoxAny; -use style::stylesheets::Stylesheet; +use style::animation::PropertyAnimation; use style::media_queries::MediaQueryList; +use style::stylesheets::Stylesheet; use url::Url; use util::geometry::Au; @@ -35,11 +37,14 @@ pub enum Msg { SetQuirksMode, /// Requests a reflow. - Reflow(Box), + Reflow(Box), /// Get an RPC interface. GetRPC(Sender>), + /// Requests that the layout task render the next frame of all animations. + TickAnimations, + /// Destroys layout data associated with a DOM node. /// /// TODO(pcwalton): Maybe think about batching to avoid message traffic. @@ -100,14 +105,22 @@ pub enum ReflowQueryType { /// Information needed for a reflow. pub struct Reflow { - /// The document node. - pub document_root: TrustedNodeAddress, /// The goal of reflow: either to render to the screen or to flush layout info for script. pub goal: ReflowGoal, /// The URL of the page. pub url: Url, /// Is the current reflow of an iframe, as opposed to a root window? pub iframe: bool, + /// A clipping rectangle for the page, an enlarged rectangle containing the viewport. + pub page_clip_rect: Rect, +} + +/// Information needed for a script-initiated reflow. +pub struct ScriptReflow { + /// General reflow data. + pub reflow_info: Reflow, + /// The document node. + pub document_root: TrustedNodeAddress, /// The channel through which messages can be sent back to the script task. pub script_chan: ScriptControlChan, /// The current window size. @@ -118,8 +131,6 @@ pub struct Reflow { pub id: u32, /// The type of query if any to perform during this reflow. pub query_type: ReflowQueryType, - /// A clipping rectangle for the page, an enlarged rectangle containing the viewport. - pub page_clip_rect: Rect, } /// Encapsulates a channel to the layout task. @@ -165,3 +176,28 @@ impl ScriptLayoutChan for OpaqueScriptLayoutChannel { *receiver.downcast::>().unwrap() } } + +/// Type of an opaque node. +pub type OpaqueNode = uintptr_t; + +/// State relating to an animation. +#[derive(Copy, Clone)] +pub struct Animation { + /// An opaque reference to the DOM node participating in the animation. + pub node: OpaqueNode, + /// A description of the property animation that is occurring. + pub property_animation: PropertyAnimation, + /// The start time of the animation, as returned by `time::precise_time_s()`. + pub start_time: f64, + /// The end time of the animation, as returned by `time::precise_time_s()`. + pub end_time: f64, +} + +impl Animation { + /// Returns the duration of this animation in seconds. + #[inline] + pub fn duration(&self) -> f64 { + self.end_time - self.start_time + } +} + diff --git a/components/servo/Cargo.lock b/components/servo/Cargo.lock index 7f66dfcb642..c7158c4239f 100644 --- a/components/servo/Cargo.lock +++ b/components/servo/Cargo.lock @@ -68,6 +68,11 @@ dependencies = [ "gleam 0.0.1 (git+https://github.com/servo/gleam)", ] +[[package]] +name = "clock_ticks" +version = "0.0.4" +source = "git+https://github.com/tomaka/clock_ticks#6a3005279bedc406b13eea09ff92447f05ca0de6" + [[package]] name = "cocoa" version = "0.1.1" @@ -503,6 +508,7 @@ dependencies = [ "azure 0.1.0 (git+https://github.com/servo/rust-azure)", "bitflags 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "canvas 0.0.1", + "clock_ticks 0.0.4 (git+https://github.com/tomaka/clock_ticks)", "cssparser 0.2.0 (git+https://github.com/servo/rust-cssparser)", "encoding 0.2.25 (registry+https://github.com/rust-lang/crates.io-index)", "geom 0.1.0 (git+https://github.com/servo/rust-geom)", diff --git a/components/style/animation.rs b/components/style/animation.rs new file mode 100644 index 00000000000..f1237c34650 --- /dev/null +++ b/components/style/animation.rs @@ -0,0 +1,221 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +use properties::ComputedValues; +use properties::longhands::transition_property::computed_value::TransitionProperty; +use properties::longhands::transition_timing_function::computed_value::{StartEnd}; +use properties::longhands::transition_timing_function::computed_value::{TransitionTimingFunction}; +use properties::longhands::transition_property; +use values::computed::{LengthOrPercentageOrAuto, Time}; + +use std::num::Float; +use util::bezier::Bezier; +use util::geometry::Au; + +#[derive(Copy, Clone, Debug)] +pub struct PropertyAnimation { + property: AnimatedProperty, + timing_function: TransitionTimingFunction, + duration: Time, +} + +impl PropertyAnimation { + /// Creates a new property animation for the given transition index and old and new styles. + /// Any number of animations may be returned, from zero (if the property did not animate) to + /// one (for a single transition property) to arbitrarily many (for `all`). + pub fn from_transition(transition_index: usize, + old_style: &ComputedValues, + new_style: &mut ComputedValues) + -> Vec { + let mut result = Vec::new(); + let transition_property = + new_style.get_animation().transition_property.0[transition_index]; + if transition_property != TransitionProperty::All { + if let Some(property_animation) = + PropertyAnimation::from_transition_property(transition_property, + transition_index, + old_style, + new_style) { + result.push(property_animation) + } + return result + } + + for transition_property in + transition_property::computed_value::ALL_TRANSITION_PROPERTIES.iter() { + if let Some(property_animation) = + PropertyAnimation::from_transition_property(*transition_property, + transition_index, + old_style, + new_style) { + result.push(property_animation) + } + } + + result + } + + fn from_transition_property(transition_property: TransitionProperty, + transition_index: usize, + old_style: &ComputedValues, + new_style: &mut ComputedValues) + -> Option { + let animation_style = new_style.get_animation(); + let animated_property = match transition_property { + TransitionProperty::All => { + panic!("Don't use `TransitionProperty::All` with \ + `PropertyAnimation::from_transition_property`!") + } + TransitionProperty::Top => { + AnimatedProperty::Top(old_style.get_positionoffsets().top, + new_style.get_positionoffsets().top) + } + TransitionProperty::Right => { + AnimatedProperty::Right(old_style.get_positionoffsets().right, + new_style.get_positionoffsets().right) + } + TransitionProperty::Bottom => { + AnimatedProperty::Bottom(old_style.get_positionoffsets().bottom, + new_style.get_positionoffsets().bottom) + } + TransitionProperty::Left => { + AnimatedProperty::Left(old_style.get_positionoffsets().left, + new_style.get_positionoffsets().left) + } + }; + + let property_animation = PropertyAnimation { + property: animated_property, + timing_function: + *animation_style.transition_timing_function.0.get_mod(transition_index), + duration: *animation_style.transition_duration.0.get_mod(transition_index), + }; + if property_animation.does_not_animate() { + None + } else { + Some(property_animation) + } + } + + pub fn update(&self, style: &mut ComputedValues, time: f64) { + let progress = match self.timing_function { + TransitionTimingFunction::CubicBezier(p1, p2) => { + // See `WebCore::AnimationBase::solveEpsilon(double)` in WebKit. + let epsilon = 1.0 / (200.0 * self.duration.seconds()); + Bezier::new(p1, p2).solve(time, epsilon) + } + TransitionTimingFunction::Steps(steps, StartEnd::Start) => { + (time * (steps as f64)).ceil() / (steps as f64) + } + TransitionTimingFunction::Steps(steps, StartEnd::End) => { + (time * (steps as f64)).floor() / (steps as f64) + } + }; + match self.property { + AnimatedProperty::Top(ref start, ref end) => { + if let Some(value) = start.interpolate(end, progress) { + style.mutate_positionoffsets().top = value + } + } + AnimatedProperty::Right(ref start, ref end) => { + if let Some(value) = start.interpolate(end, progress) { + style.mutate_positionoffsets().right = value + } + } + AnimatedProperty::Bottom(ref start, ref end) => { + if let Some(value) = start.interpolate(end, progress) { + style.mutate_positionoffsets().bottom = value + } + } + AnimatedProperty::Left(ref start, ref end) => { + if let Some(value) = start.interpolate(end, progress) { + style.mutate_positionoffsets().left = value + } + } + } + } + + #[inline] + fn does_not_animate(&self) -> bool { + self.property.does_not_animate() || self.duration == Time(0.0) + } +} + +#[derive(Copy, Clone, Debug)] +enum AnimatedProperty { + Top(LengthOrPercentageOrAuto, LengthOrPercentageOrAuto), + Right(LengthOrPercentageOrAuto, LengthOrPercentageOrAuto), + Bottom(LengthOrPercentageOrAuto, LengthOrPercentageOrAuto), + Left(LengthOrPercentageOrAuto, LengthOrPercentageOrAuto), +} + +impl AnimatedProperty { + #[inline] + fn does_not_animate(&self) -> bool { + match *self { + AnimatedProperty::Top(ref a, ref b) | + AnimatedProperty::Right(ref a, ref b) | + AnimatedProperty::Bottom(ref a, ref b) | + AnimatedProperty::Left(ref a, ref b) => a == b, + } + } +} + +trait Interpolate { + fn interpolate(&self, other: &Self, time: f64) -> Option; +} + +impl Interpolate for Au { + #[inline] + fn interpolate(&self, other: &Au, time: f64) -> Option { + Some(Au((self.0 as f64 + (other.0 as f64 - self.0 as f64) * time).round() as i32)) + } +} + +impl Interpolate for f64 { + #[inline] + fn interpolate(&self, other: &f64, time: f64) -> Option { + Some(*self + (*other - *self) * time) + } +} + +impl Interpolate for LengthOrPercentageOrAuto { + #[inline] + fn interpolate(&self, other: &LengthOrPercentageOrAuto, time: f64) + -> Option { + match (*self, *other) { + (LengthOrPercentageOrAuto::Length(ref this), + LengthOrPercentageOrAuto::Length(ref other)) => { + this.interpolate(other, time).and_then(|value| { + Some(LengthOrPercentageOrAuto::Length(value)) + }) + } + (LengthOrPercentageOrAuto::Percentage(ref this), + LengthOrPercentageOrAuto::Percentage(ref other)) => { + this.interpolate(other, time).and_then(|value| { + Some(LengthOrPercentageOrAuto::Percentage(value)) + }) + } + (LengthOrPercentageOrAuto::Auto, LengthOrPercentageOrAuto::Auto) => { + Some(LengthOrPercentageOrAuto::Auto) + } + (_, _) => None, + } + } +} + +/// Accesses an element of an array, "wrapping around" using modular arithmetic. This is needed +/// to handle values of differing lengths according to CSS-TRANSITIONS § 2. +pub trait GetMod { + type Item; + fn get_mod(&self, i: usize) -> &Self::Item; +} + +impl GetMod for Vec { + type Item = T; + fn get_mod(&self, i: usize) -> &T { + &(*self)[i % self.len()] + } +} + diff --git a/components/style/lib.rs b/components/style/lib.rs index 4951242290b..e71828b6330 100644 --- a/components/style/lib.rs +++ b/components/style/lib.rs @@ -50,6 +50,7 @@ pub mod node; pub mod media_queries; pub mod font_face; pub mod legacy; +pub mod animation; macro_rules! reexport_computed_values { ( $( $name: ident )+ ) => { diff --git a/components/style/properties.mako.rs b/components/style/properties.mako.rs index d3699ff710f..d0503a83786 100644 --- a/components/style/properties.mako.rs +++ b/components/style/properties.mako.rs @@ -3377,6 +3377,367 @@ pub mod longhands { } } + + ${new_style_struct("Animation", is_inherited=False)} + + // TODO(pcwalton): Multiple transitions. + <%self:longhand name="transition-duration"> + use values::specified::Time; + + pub use self::computed_value::T as SpecifiedValue; + pub use values::specified::Time as SingleSpecifiedValue; + + pub mod computed_value { + use cssparser::ToCss; + use text_writer::{self, TextWriter}; + use values::computed::{Context, ToComputedValue}; + + pub use values::computed::Time as SingleComputedValue; + + #[derive(Clone, PartialEq)] + pub struct T(pub Vec); + + impl ToComputedValue for T { + type ComputedValue = T; + + #[inline] + fn to_computed_value(&self, _: &Context) -> T { + (*self).clone() + } + } + + impl ToCss for T { + fn to_css(&self, dest: &mut W) -> text_writer::Result where W: TextWriter { + if self.0.is_empty() { + return dest.write_str("none") + } + for (i, value) in self.0.iter().enumerate() { + if i != 0 { + try!(dest.write_str(", ")) + } + try!(value.to_css(dest)) + } + Ok(()) + } + } + } + + #[inline] + pub fn parse_one(input: &mut Parser) -> Result { + Time::parse(input) + } + + #[inline] + pub fn get_initial_value() -> computed_value::T { + computed_value::T(vec![get_initial_single_value()]) + } + + #[inline] + pub fn get_initial_single_value() -> Time { + Time(0.0) + } + + pub fn parse(_: &ParserContext, input: &mut Parser) -> Result { + Ok(SpecifiedValue(try!(input.parse_comma_separated(parse_one)))) + } + + + // TODO(pcwalton): Lots more timing functions. + // TODO(pcwalton): Multiple transitions. + <%self:longhand name="transition-timing-function"> + use self::computed_value::{StartEnd, TransitionTimingFunction}; + use values::computed::{Context, ToComputedValue}; + + use geom::point::Point2D; + + pub use self::computed_value::SingleComputedValue as SingleSpecifiedValue; + pub use self::computed_value::T as SpecifiedValue; + + static EASE: TransitionTimingFunction = TransitionTimingFunction::CubicBezier(Point2D { + x: 0.25, + y: 0.1, + }, Point2D { + x: 0.25, + y: 1.0, + }); + static LINEAR: TransitionTimingFunction = TransitionTimingFunction::CubicBezier(Point2D { + x: 0.0, + y: 0.0, + }, Point2D { + x: 1.0, + y: 1.0, + }); + static EASE_IN: TransitionTimingFunction = TransitionTimingFunction::CubicBezier(Point2D { + x: 0.42, + y: 0.0, + }, Point2D { + x: 1.0, + y: 1.0, + }); + static EASE_OUT: TransitionTimingFunction = TransitionTimingFunction::CubicBezier(Point2D { + x: 0.0, + y: 0.0, + }, Point2D { + x: 0.58, + y: 1.0, + }); + static EASE_IN_OUT: TransitionTimingFunction = + TransitionTimingFunction::CubicBezier(Point2D { + x: 0.42, + y: 0.0, + }, Point2D { + x: 0.58, + y: 1.0, + }); + static STEP_START: TransitionTimingFunction = + TransitionTimingFunction::Steps(1, StartEnd::Start); + static STEP_END: TransitionTimingFunction = + TransitionTimingFunction::Steps(1, StartEnd::End); + + pub mod computed_value { + use cssparser::ToCss; + use geom::point::Point2D; + use text_writer::{self, TextWriter}; + + pub use self::TransitionTimingFunction as SingleComputedValue; + + #[derive(Copy, Clone, Debug, PartialEq)] + pub enum TransitionTimingFunction { + CubicBezier(Point2D, Point2D), + Steps(u32, StartEnd), + } + + impl ToCss for TransitionTimingFunction { + fn to_css(&self, dest: &mut W) -> text_writer::Result where W: TextWriter { + match *self { + TransitionTimingFunction::CubicBezier(p1, p2) => { + try!(dest.write_str("cubic-bezier(")); + try!(p1.x.to_css(dest)); + try!(dest.write_str(", ")); + try!(p1.y.to_css(dest)); + try!(dest.write_str(", ")); + try!(p2.x.to_css(dest)); + try!(dest.write_str(", ")); + try!(p2.y.to_css(dest)); + dest.write_str(")") + } + TransitionTimingFunction::Steps(steps, start_end) => { + try!(dest.write_str("steps(")); + try!(steps.to_css(dest)); + try!(dest.write_str(", ")); + try!(start_end.to_css(dest)); + dest.write_str(")") + } + } + } + } + + #[derive(Copy, Clone, Debug, PartialEq)] + pub enum StartEnd { + Start, + End, + } + + impl ToCss for StartEnd { + fn to_css(&self, dest: &mut W) -> text_writer::Result where W: TextWriter { + match *self { + StartEnd::Start => dest.write_str("start"), + StartEnd::End => dest.write_str("end"), + } + } + } + + #[derive(Clone, Debug, PartialEq)] + pub struct T(pub Vec); + + impl ToCss for T { + fn to_css(&self, dest: &mut W) -> text_writer::Result where W: TextWriter { + if self.0.is_empty() { + return dest.write_str("none") + } + for (i, value) in self.0.iter().enumerate() { + if i != 0 { + try!(dest.write_str(", ")) + } + try!(value.to_css(dest)) + } + Ok(()) + } + } + } + + impl ToComputedValue for SpecifiedValue { + type ComputedValue = computed_value::T; + + #[inline] + fn to_computed_value(&self, _: &Context) -> computed_value::T { + (*self).clone() + } + } + + #[inline] + pub fn get_initial_value() -> computed_value::T { + computed_value::T(vec![get_initial_single_value()]) + } + + #[inline] + pub fn get_initial_single_value() -> TransitionTimingFunction { + EASE + } + + pub fn parse_one(input: &mut Parser) -> Result { + if let Ok(function_name) = input.try(|input| input.expect_function()) { + return match_ignore_ascii_case! { + function_name, + "cubic-bezier" => { + let (mut p1x, mut p1y, mut p2x, mut p2y) = (0.0, 0.0, 0.0, 0.0); + try!(input.parse_nested_block(|input| { + p1x = try!(input.expect_number()); + try!(input.expect_comma()); + p1y = try!(input.expect_number()); + try!(input.expect_comma()); + p2x = try!(input.expect_number()); + try!(input.expect_comma()); + p2y = try!(input.expect_number()); + Ok(()) + })); + let (p1, p2) = (Point2D(p1x, p1y), Point2D(p2x, p2y)); + Ok(TransitionTimingFunction::CubicBezier(p1, p2)) + }, + "steps" => { + let (mut step_count, mut start_end) = (0, computed_value::StartEnd::Start); + try!(input.parse_nested_block(|input| { + step_count = try!(input.expect_integer()); + try!(input.expect_comma()); + start_end = try!(match_ignore_ascii_case! { + try!(input.expect_ident()), + "start" => Ok(computed_value::StartEnd::Start), + "end" => Ok(computed_value::StartEnd::End) + _ => Err(()) + }); + Ok(()) + })); + Ok(TransitionTimingFunction::Steps(step_count as u32, start_end)) + } + _ => Err(()) + } + } + match_ignore_ascii_case! { + try!(input.expect_ident()), + "ease" => Ok(EASE), + "linear" => Ok(LINEAR), + "ease-in" => Ok(EASE_IN), + "ease-out" => Ok(EASE_OUT), + "ease-in-out" => Ok(EASE_IN_OUT), + "step-start" => Ok(STEP_START), + "step-end" => Ok(STEP_END) + _ => Err(()) + } + } + + pub fn parse(_: &ParserContext, input: &mut Parser) -> Result { + Ok(SpecifiedValue(try!(input.parse_comma_separated(parse_one)))) + } + + + // TODO(pcwalton): Lots more properties. + <%self:longhand name="transition-property"> + use self::computed_value::TransitionProperty; + use values::computed::{ToComputedValue, Context}; + + pub use self::computed_value::SingleComputedValue as SingleSpecifiedValue; + pub use self::computed_value::T as SpecifiedValue; + + pub mod computed_value { + use cssparser::ToCss; + use text_writer::{self, TextWriter}; + + pub use self::TransitionProperty as SingleComputedValue; + + #[derive(Copy, Clone, Debug, PartialEq)] + pub enum TransitionProperty { + All, + Top, + Right, + Bottom, + Left, + } + + pub static ALL_TRANSITION_PROPERTIES: [TransitionProperty; 4] = [ + TransitionProperty::Top, + TransitionProperty::Right, + TransitionProperty::Bottom, + TransitionProperty::Left, + ]; + + impl ToCss for TransitionProperty { + fn to_css(&self, dest: &mut W) -> text_writer::Result where W: TextWriter { + match *self { + TransitionProperty::All => dest.write_str("all"), + TransitionProperty::Top => dest.write_str("top"), + TransitionProperty::Right => dest.write_str("right"), + TransitionProperty::Bottom => dest.write_str("bottom"), + TransitionProperty::Left => dest.write_str("left"), + } + } + } + + #[derive(Clone, Debug, PartialEq)] + pub struct T(pub Vec); + + impl ToCss for T { + fn to_css(&self, dest: &mut W) -> text_writer::Result where W: TextWriter { + if self.0.is_empty() { + return dest.write_str("none") + } + for (i, value) in self.0.iter().enumerate() { + if i != 0 { + try!(dest.write_str(", ")) + } + try!(value.to_css(dest)) + } + Ok(()) + } + } + } + + #[inline] + pub fn get_initial_value() -> computed_value::T { + computed_value::T(Vec::new()) + } + + pub fn parse_one(input: &mut Parser) -> Result { + match_ignore_ascii_case! { + try!(input.expect_ident()), + "all" => Ok(TransitionProperty::All), + "top" => Ok(TransitionProperty::Top), + "right" => Ok(TransitionProperty::Right), + "bottom" => Ok(TransitionProperty::Bottom), + "left" => Ok(TransitionProperty::Left) + _ => Err(()) + } + } + + pub fn parse(_: &ParserContext, input: &mut Parser) -> Result { + Ok(SpecifiedValue(try!(input.parse_comma_separated(parse_one)))) + } + + impl ToComputedValue for SpecifiedValue { + type ComputedValue = computed_value::T; + + #[inline] + fn to_computed_value(&self, _: &Context) -> computed_value::T { + (*self).clone() + } + } + + + <%self:longhand name="transition-delay"> + pub use properties::longhands::transition_duration::{SingleSpecifiedValue, SpecifiedValue}; + pub use properties::longhands::transition_duration::{computed_value}; + pub use properties::longhands::transition_duration::{get_initial_single_value}; + pub use properties::longhands::transition_duration::{get_initial_value, parse, parse_one}; + } @@ -3401,6 +3762,8 @@ pub mod shorthands { Option, % endfor } + + #[allow(unused_variables)] pub fn parse(context: &ParserContext, input: &mut Parser) -> Result { ${caller.body()} } @@ -3879,6 +4242,99 @@ pub mod shorthands { overflow_y: Some(overflow_y::SpecifiedValue(overflow)), }) + + <%self:shorthand name="transition" + sub_properties="transition-property transition-duration transition-timing-function transition-delay"> + use properties::longhands::{transition_delay, transition_duration, transition_property}; + use properties::longhands::{transition_timing_function}; + + struct SingleTransition { + transition_property: transition_property::SingleSpecifiedValue, + transition_duration: transition_duration::SingleSpecifiedValue, + transition_timing_function: transition_timing_function::SingleSpecifiedValue, + transition_delay: transition_delay::SingleSpecifiedValue, + } + + fn parse_one_transition(input: &mut Parser) -> Result { + let (mut property, mut duration) = (None, None); + let (mut timing_function, mut delay) = (None, None); + loop { + if property.is_none() { + if let Ok(value) = input.try(|input| transition_property::parse_one(input)) { + property = Some(value); + continue + } + } + + if duration.is_none() { + if let Ok(value) = input.try(|input| transition_duration::parse_one(input)) { + duration = Some(value); + continue + } + } + + if timing_function.is_none() { + if let Ok(value) = input.try(|input| { + transition_timing_function::parse_one(input) + }) { + timing_function = Some(value); + continue + } + } + + if delay.is_none() { + if let Ok(value) = input.try(|input| transition_delay::parse_one(input)) { + delay = Some(value); + continue; + } + } + + break + } + + if let Some(property) = property { + Ok(SingleTransition { + transition_property: property, + transition_duration: + duration.unwrap_or(transition_duration::get_initial_single_value()), + transition_timing_function: + timing_function.unwrap_or( + transition_timing_function::get_initial_single_value()), + transition_delay: + delay.unwrap_or(transition_delay::get_initial_single_value()), + }) + } else { + Err(()) + } + } + + if input.try(|input| input.expect_ident_matching("none")).is_ok() { + return Ok(Longhands { + transition_property: None, + transition_duration: None, + transition_timing_function: None, + transition_delay: None, + }) + } + + let results = try!(input.parse_comma_separated(parse_one_transition)); + let (mut properties, mut durations) = (Vec::new(), Vec::new()); + let (mut timing_functions, mut delays) = (Vec::new(), Vec::new()); + for result in results.into_iter() { + properties.push(result.transition_property); + durations.push(result.transition_duration); + timing_functions.push(result.transition_timing_function); + delays.push(result.transition_delay); + } + + Ok(Longhands { + transition_property: Some(transition_property::SpecifiedValue(properties)), + transition_duration: Some(transition_duration::SpecifiedValue(durations)), + transition_timing_function: + Some(transition_timing_function::SpecifiedValue(timing_functions)), + transition_delay: Some(transition_delay::SpecifiedValue(delays)), + }) + } @@ -4328,6 +4784,11 @@ impl ComputedValues { <'a>(&'a self) -> &'a style_structs::${style_struct.name} { &*self.${style_struct.ident} } + #[inline] + pub fn mutate_${style_struct.name.lower()} + <'a>(&'a mut self) -> &'a mut style_structs::${style_struct.name} { + &mut *self.${style_struct.ident}.make_unique() + } % endfor } @@ -4391,12 +4852,13 @@ fn initial_writing_mode_is_empty() { /// Fast path for the function below. Only computes new inherited styles. #[allow(unused_mut)] -fn cascade_with_cached_declarations(applicable_declarations: &[DeclarationBlock>], - shareable: bool, - parent_style: &ComputedValues, - cached_style: &ComputedValues, - context: &computed::Context) - -> ComputedValues { +fn cascade_with_cached_declarations( + applicable_declarations: &[DeclarationBlock>], + shareable: bool, + parent_style: &ComputedValues, + cached_style: &ComputedValues, + context: &computed::Context) + -> ComputedValues { % for style_struct in STYLE_STRUCTS: % if style_struct.inherited: let mut style_${style_struct.ident} = parent_style.${style_struct.ident}.clone(); @@ -4415,7 +4877,9 @@ fn cascade_with_cached_declarations(applicable_declarations: &[DeclarationBlock< % for style_struct in STYLE_STRUCTS: % for property in style_struct.longhands: % if property.derived_from is None: - PropertyDeclaration::${property.camel_case}(ref ${'_' if not style_struct.inherited else ''}declared_value) => { + PropertyDeclaration::${property.camel_case}(ref + ${'_' if not style_struct.inherited else ''}declared_value) + => { % if style_struct.inherited: if seen.get_${property.ident}() { continue @@ -4556,9 +5020,13 @@ pub fn cascade(viewport_size: Size2D, context.font_size = match *value { DeclaredValue::SpecifiedValue(ref specified_value) => { match specified_value.0 { - Length::FontRelative(value) => value.to_computed_value(context.inherited_font_size, - context.root_font_size), - Length::ServoCharacterWidth(value) => value.to_computed_value(context.inherited_font_size), + Length::FontRelative(value) => { + value.to_computed_value(context.inherited_font_size, + context.root_font_size) + } + Length::ServoCharacterWidth(value) => { + value.to_computed_value(context.inherited_font_size) + } _ => specified_value.0.to_computed_value(&context) } } @@ -4568,7 +5036,9 @@ pub fn cascade(viewport_size: Size2D, } PropertyDeclaration::Color(ref value) => { context.color = match *value { - DeclaredValue::SpecifiedValue(ref specified_value) => specified_value.parsed, + DeclaredValue::SpecifiedValue(ref specified_value) => { + specified_value.parsed + } DeclaredValue::Initial => longhands::color::get_initial_value(), DeclaredValue::Inherit => inherited_style.get_color().color.clone(), }; diff --git a/components/style/values.rs b/components/style/values.rs index 8cf1ca60d3a..e26c37191a3 100644 --- a/components/style/values.rs +++ b/components/style/values.rs @@ -862,11 +862,57 @@ pub mod specified { "inset" => inset, "outset" => outset, } + + /// A time in seconds according to CSS-VALUES § 6.2. + #[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] + pub struct Time(pub CSSFloat); + + impl Time { + /// Returns the time in fractional seconds. + pub fn seconds(self) -> f64 { + let Time(seconds) = self; + seconds + } + + /// Parses a time according to CSS-VALUES § 6.2. + fn parse_dimension(value: CSSFloat, unit: &str) -> Result { + if unit.eq_ignore_ascii_case("s") { + Ok(Time(value)) + } else if unit.eq_ignore_ascii_case("ms") { + Ok(Time(value / 1000.0)) + } else { + Err(()) + } + } + + pub fn parse(input: &mut Parser) -> Result { + match input.next() { + Ok(Token::Dimension(ref value, ref unit)) => { + Time::parse_dimension(value.value, unit.as_slice()) + } + _ => Err(()), + } + } + } + + impl super::computed::ToComputedValue for Time { + type ComputedValue = Time; + + #[inline] + fn to_computed_value(&self, _: &super::computed::Context) -> Time { + *self + } + } + + impl ToCss for Time { + fn to_css(&self, dest: &mut W) -> text_writer::Result where W: TextWriter { + dest.write_str(format!("{}ms", self.0).as_slice()) + } + } } - pub mod computed { - pub use super::specified::BorderStyle; + pub use super::specified::{BorderStyle, Time}; use super::specified::{AngleOrCorner}; use super::{specified, CSSFloat}; pub use cssparser::Color as CSSColor; diff --git a/components/util/bezier.rs b/components/util/bezier.rs new file mode 100644 index 00000000000..7928a1cab4e --- /dev/null +++ b/components/util/bezier.rs @@ -0,0 +1,116 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +//! Parametric Bézier curves. +//! +//! This is based on `WebCore/platform/graphics/UnitBezier.h` in WebKit. + +use geom::point::Point2D; +use std::num::Float; + +const NEWTON_METHOD_ITERATIONS: u8 = 8; + +pub struct Bezier { + ax: f64, + bx: f64, + cx: f64, + ay: f64, + by: f64, + cy: f64, +} + +impl Bezier { + #[inline] + pub fn new(p1: Point2D, p2: Point2D) -> Bezier { + let cx = 3.0 * p1.x; + let bx = 3.0 * (p2.x - p1.x) - cx; + + let cy = 3.0 * p1.y; + let by = 3.0 * (p2.y - p1.y) - cy; + + Bezier { + ax: 1.0 - cx - bx, + bx: bx, + cx: cx, + ay: 1.0 - cy - by, + by: by, + cy: cy, + } + } + + #[inline] + fn sample_curve_x(&self, t: f64) -> f64 { + // ax * t^3 + bx * t^2 + cx * t + ((self.ax * t + self.bx) * t + self.cx) * t + } + + #[inline] + fn sample_curve_y(&self, t: f64) -> f64 { + ((self.ay * t + self.by) * t + self.cy) * t + } + + #[inline] + fn sample_curve_derivative_x(&self, t: f64) -> f64 { + (3.0 * self.ax * t + 2.0 * self.bx) * t + self.cx + } + + #[inline] + fn solve_curve_x(&self, x: f64, epsilon: f64) -> f64 { + // Fast path: Use Newton's method. + let mut t = x; + for _ in range(0, NEWTON_METHOD_ITERATIONS) { + let x2 = self.sample_curve_x(t); + if x2.approx_eq(x, epsilon) { + return t + } + let dx = self.sample_curve_derivative_x(t); + if dx.approx_eq(0.0, 1e-6) { + break + } + t -= (x2 - x) / dx; + } + + // Slow path: Use bisection. + let (mut lo, mut hi, mut t) = (0.0, 1.0, x); + + if t < lo { + return lo + } + if t > hi { + return hi + } + + while lo < hi { + let x2 = self.sample_curve_x(t); + if x2.approx_eq(x, epsilon) { + return t + } + if x > x2 { + lo = t + } else { + hi = t + } + t = (hi - lo) / 2.0 + lo + } + + t + } + + #[inline] + pub fn solve(&self, x: f64, epsilon: f64) -> f64 { + self.sample_curve_y(self.solve_curve_x(x, epsilon)) + } +} + +trait ApproxEq { + fn approx_eq(self, value: Self, epsilon: Self) -> bool; +} + +impl ApproxEq for f64 { + #[inline] + fn approx_eq(self, value: f64, epsilon: f64) -> bool { + (self - value).abs() < epsilon + } +} + diff --git a/components/util/lib.rs b/components/util/lib.rs index 68de9581014..28efc0e58e1 100644 --- a/components/util/lib.rs +++ b/components/util/lib.rs @@ -41,6 +41,7 @@ pub use selectors::smallvec; use std::sync::Arc; +pub mod bezier; pub mod cache; pub mod cursor; pub mod debug_utils; diff --git a/ports/cef/Cargo.lock b/ports/cef/Cargo.lock index 48f36b591e2..bc0ed5a0da5 100644 --- a/ports/cef/Cargo.lock +++ b/ports/cef/Cargo.lock @@ -73,6 +73,11 @@ dependencies = [ "gleam 0.0.1 (git+https://github.com/servo/gleam)", ] +[[package]] +name = "clock_ticks" +version = "0.0.4" +source = "git+https://github.com/tomaka/clock_ticks#6a3005279bedc406b13eea09ff92447f05ca0de6" + [[package]] name = "cocoa" version = "0.1.1" @@ -508,6 +513,7 @@ dependencies = [ "azure 0.1.0 (git+https://github.com/servo/rust-azure)", "bitflags 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "canvas 0.0.1", + "clock_ticks 0.0.4 (git+https://github.com/tomaka/clock_ticks)", "cssparser 0.2.0 (git+https://github.com/servo/rust-cssparser)", "encoding 0.2.25 (registry+https://github.com/rust-lang/crates.io-index)", "geom 0.1.0 (git+https://github.com/servo/rust-geom)", diff --git a/ports/gonk/Cargo.lock b/ports/gonk/Cargo.lock index f8266e48511..db31cfd8db0 100644 --- a/ports/gonk/Cargo.lock +++ b/ports/gonk/Cargo.lock @@ -61,6 +61,11 @@ dependencies = [ "gleam 0.0.1 (git+https://github.com/servo/gleam)", ] +[[package]] +name = "clock_ticks" +version = "0.0.4" +source = "git+https://github.com/tomaka/clock_ticks#6a3005279bedc406b13eea09ff92447f05ca0de6" + [[package]] name = "compositing" version = "0.0.1" @@ -433,6 +438,7 @@ dependencies = [ "azure 0.1.0 (git+https://github.com/servo/rust-azure)", "bitflags 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "canvas 0.0.1", + "clock_ticks 0.0.4 (git+https://github.com/tomaka/clock_ticks)", "cssparser 0.2.0 (git+https://github.com/servo/rust-cssparser)", "encoding 0.2.25 (registry+https://github.com/rust-lang/crates.io-index)", "geom 0.1.0 (git+https://github.com/servo/rust-geom)", diff --git a/tests/html/transition_all.html b/tests/html/transition_all.html new file mode 100644 index 00000000000..1d9a199d5ca --- /dev/null +++ b/tests/html/transition_all.html @@ -0,0 +1,29 @@ + + + + + + +
+ + + + + diff --git a/tests/html/transition_multiple.html b/tests/html/transition_multiple.html new file mode 100644 index 00000000000..14d99ddfeff --- /dev/null +++ b/tests/html/transition_multiple.html @@ -0,0 +1,28 @@ + + + + + + +
+ + + + + diff --git a/tests/html/transition_simple.html b/tests/html/transition_simple.html new file mode 100644 index 00000000000..0cfdb073152 --- /dev/null +++ b/tests/html/transition_simple.html @@ -0,0 +1,96 @@ + + + + + + +
+
+
+
+
+
+
+
+
+ + + +