summaryrefslogtreecommitdiffstats
path: root/gfx/wr/wrench/src/yaml_frame_reader.rs
diff options
context:
space:
mode:
Diffstat (limited to 'gfx/wr/wrench/src/yaml_frame_reader.rs')
-rw-r--r--gfx/wr/wrench/src/yaml_frame_reader.rs2125
1 files changed, 2125 insertions, 0 deletions
diff --git a/gfx/wr/wrench/src/yaml_frame_reader.rs b/gfx/wr/wrench/src/yaml_frame_reader.rs
new file mode 100644
index 0000000000..9bd8b56943
--- /dev/null
+++ b/gfx/wr/wrench/src/yaml_frame_reader.rs
@@ -0,0 +1,2125 @@
+/* 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 euclid::SideOffsets2D;
+use gleam::gl;
+use image::GenericImageView;
+use crate::parse_function::parse_function;
+use crate::premultiply::premultiply;
+use std::collections::HashMap;
+use std::convert::TryInto;
+use std::fs::File;
+use std::io::Read;
+use std::path::{Path, PathBuf};
+use std::usize;
+use webrender::api::*;
+use webrender::render_api::*;
+use webrender::api::units::*;
+use webrender::api::FillRule;
+use crate::wrench::{FontDescriptor, Wrench, WrenchThing};
+use crate::yaml_helper::{StringEnum, YamlHelper, make_perspective};
+use yaml_rust::{Yaml, YamlLoader};
+use crate::PLATFORM_DEFAULT_FACE_NAME;
+
+macro_rules! try_intersect {
+ ($first: expr, $second: expr) => {
+ if let Some(rect) = ($first).intersection($second) {
+ rect
+ } else {
+ warn!("skipping item with non-intersecting bounds and clip_rect");
+ return;
+ }
+ }
+}
+
+fn rsrc_path(item: &Yaml, aux_dir: &Path) -> PathBuf {
+ let filename = item.as_str().unwrap();
+ let mut file = aux_dir.to_path_buf();
+ file.push(filename);
+ file
+}
+
+impl FontDescriptor {
+ fn from_yaml(item: &Yaml, aux_dir: &Path) -> FontDescriptor {
+ if !item["family"].is_badvalue() {
+ FontDescriptor::Properties {
+ family: item["family"].as_str().unwrap().to_owned(),
+ weight: item["weight"].as_i64().unwrap_or(400) as u32,
+ style: item["style"].as_i64().unwrap_or(0) as u32,
+ stretch: item["stretch"].as_i64().unwrap_or(5) as u32,
+ }
+ } else if !item["font"].is_badvalue() {
+ let path = rsrc_path(&item["font"], aux_dir);
+ FontDescriptor::Path {
+ path,
+ font_index: item["font-index"].as_i64().unwrap_or(0) as u32,
+ }
+ } else {
+ FontDescriptor::Family {
+ name: PLATFORM_DEFAULT_FACE_NAME.to_string(),
+ }
+ }
+ }
+}
+
+struct LocalExternalImageHandler {
+ texture_ids: Vec<(gl::GLuint, ImageDescriptor)>,
+}
+
+impl LocalExternalImageHandler {
+ pub fn new() -> LocalExternalImageHandler {
+ LocalExternalImageHandler {
+ texture_ids: Vec::new(),
+ }
+ }
+
+ fn init_gl_texture(
+ id: gl::GLuint,
+ gl_target: gl::GLuint,
+ format_desc: webrender::FormatDesc,
+ width: gl::GLint,
+ height: gl::GLint,
+ bytes: &[u8],
+ gl: &dyn gl::Gl,
+ ) {
+ gl.bind_texture(gl_target, id);
+ gl.tex_parameter_i(gl_target, gl::TEXTURE_MAG_FILTER, gl::LINEAR as gl::GLint);
+ gl.tex_parameter_i(gl_target, gl::TEXTURE_MIN_FILTER, gl::LINEAR as gl::GLint);
+ gl.tex_parameter_i(gl_target, gl::TEXTURE_WRAP_S, gl::CLAMP_TO_EDGE as gl::GLint);
+ gl.tex_parameter_i(gl_target, gl::TEXTURE_WRAP_T, gl::CLAMP_TO_EDGE as gl::GLint);
+ gl.tex_image_2d(
+ gl_target,
+ 0,
+ format_desc.internal as gl::GLint,
+ width,
+ height,
+ 0,
+ format_desc.external,
+ format_desc.pixel_type,
+ Some(bytes),
+ );
+ gl.bind_texture(gl_target, 0);
+ }
+
+ pub fn add_image(&mut self,
+ device: &webrender::Device,
+ desc: ImageDescriptor,
+ target: ImageBufferKind,
+ image_data: ImageData,
+ ) -> ImageData {
+ let (image_id, channel_idx) = match image_data {
+ ImageData::Raw(ref data) => {
+ let gl = device.gl();
+ let texture_ids = gl.gen_textures(1);
+ let format_desc = if desc.format == ImageFormat::BGRA8 {
+ // Force BGRA8 data to RGBA8 layout to avoid potential
+ // need for usage of texture-swizzle.
+ webrender::FormatDesc {
+ external: gl::BGRA,
+ .. device.gl_describe_format(ImageFormat::RGBA8)
+ }
+ } else {
+ device.gl_describe_format(desc.format)
+ };
+
+ LocalExternalImageHandler::init_gl_texture(
+ texture_ids[0],
+ webrender::get_gl_target(target),
+ format_desc,
+ desc.size.width as gl::GLint,
+ desc.size.height as gl::GLint,
+ data,
+ gl,
+ );
+ self.texture_ids.push((texture_ids[0], desc));
+ (ExternalImageId((self.texture_ids.len() - 1) as u64), 0)
+ },
+ _ => panic!("unsupported!"),
+ };
+
+ ImageData::External(
+ ExternalImageData {
+ id: image_id,
+ channel_index: channel_idx,
+ image_type: ExternalImageType::TextureHandle(target)
+ }
+ )
+ }
+}
+
+impl ExternalImageHandler for LocalExternalImageHandler {
+ fn lock(
+ &mut self,
+ key: ExternalImageId,
+ _channel_index: u8,
+ ) -> ExternalImage {
+ let (id, desc) = self.texture_ids[key.0 as usize];
+ ExternalImage {
+ uv: TexelRect::new(0.0, 0.0, desc.size.width as f32, desc.size.height as f32),
+ source: ExternalImageSource::NativeTexture(id),
+ }
+ }
+ fn unlock(&mut self, _key: ExternalImageId, _channel_index: u8) {}
+}
+
+fn broadcast<T: Clone>(base_vals: &[T], num_items: usize) -> Vec<T> {
+ if base_vals.len() == num_items {
+ return base_vals.to_vec();
+ }
+
+ assert_eq!(
+ num_items % base_vals.len(),
+ 0,
+ "Cannot broadcast {} elements into {}",
+ base_vals.len(),
+ num_items
+ );
+
+ let mut vals = vec![];
+ loop {
+ if vals.len() == num_items {
+ break;
+ }
+ vals.extend_from_slice(base_vals);
+ }
+ vals
+}
+
+enum CheckerboardKind {
+ BlackGrey,
+ BlackTransparent,
+}
+
+fn generate_checkerboard_image(
+ border: u32,
+ tile_x_size: u32,
+ tile_y_size: u32,
+ tile_x_count: u32,
+ tile_y_count: u32,
+ kind: CheckerboardKind,
+) -> (ImageDescriptor, ImageData) {
+ let width = 2 * border + tile_x_size * tile_x_count;
+ let height = 2 * border + tile_y_size * tile_y_count;
+ let mut pixels = Vec::new();
+
+ for y in 0 .. height {
+ for x in 0 .. width {
+ if y < border || y >= (height - border) ||
+ x < border || x >= (width - border) {
+ pixels.push(0);
+ pixels.push(0);
+ pixels.push(0xff);
+ pixels.push(0xff);
+ } else {
+ let xon = ((x - border) % (2 * tile_x_size)) < tile_x_size;
+ let yon = ((y - border) % (2 * tile_y_size)) < tile_y_size;
+ match kind {
+ CheckerboardKind::BlackGrey => {
+ let value = if xon ^ yon { 0xff } else { 0x7f };
+ pixels.push(value);
+ pixels.push(value);
+ pixels.push(value);
+ pixels.push(0xff);
+ }
+ CheckerboardKind::BlackTransparent => {
+ let value = if xon ^ yon { 0xff } else { 0x00 };
+ pixels.push(value);
+ pixels.push(value);
+ pixels.push(value);
+ pixels.push(value);
+ }
+ }
+ }
+ }
+ }
+
+ let flags = match kind {
+ CheckerboardKind::BlackGrey => ImageDescriptorFlags::IS_OPAQUE,
+ CheckerboardKind::BlackTransparent => ImageDescriptorFlags::empty(),
+ };
+
+ (
+ ImageDescriptor::new(width as i32, height as i32, ImageFormat::BGRA8, flags),
+ ImageData::new(pixels),
+ )
+}
+
+fn generate_xy_gradient_image(w: u32, h: u32) -> (ImageDescriptor, ImageData) {
+ let mut pixels = Vec::with_capacity((w * h * 4) as usize);
+ for y in 0 .. h {
+ for x in 0 .. w {
+ let grid = if x % 100 < 3 || y % 100 < 3 { 0.9 } else { 1.0 };
+ pixels.push((y as f32 / h as f32 * 255.0 * grid) as u8);
+ pixels.push(0);
+ pixels.push((x as f32 / w as f32 * 255.0 * grid) as u8);
+ pixels.push(255);
+ }
+ }
+
+ (
+ ImageDescriptor::new(w as i32, h as i32, ImageFormat::BGRA8, ImageDescriptorFlags::IS_OPAQUE),
+ ImageData::new(pixels),
+ )
+}
+
+fn generate_solid_color_image(
+ r: u8,
+ g: u8,
+ b: u8,
+ a: u8,
+ w: u32,
+ h: u32,
+) -> (ImageDescriptor, ImageData) {
+ let num_pixels: usize = (w * h).try_into().unwrap();
+ let pixels = [b, g, r, a].repeat(num_pixels);
+
+ let mut flags = ImageDescriptorFlags::empty();
+ if a == 255 {
+ flags |= ImageDescriptorFlags::IS_OPAQUE;
+ }
+
+ (
+ ImageDescriptor::new(w as i32, h as i32, ImageFormat::BGRA8, flags),
+ ImageData::new(pixels),
+ )
+}
+
+
+
+fn is_image_opaque(format: ImageFormat, bytes: &[u8]) -> bool {
+ match format {
+ ImageFormat::BGRA8 |
+ ImageFormat::RGBA8 => {
+ let mut is_opaque = true;
+ for i in 0 .. (bytes.len() / 4) {
+ if bytes[i * 4 + 3] != 255 {
+ is_opaque = false;
+ break;
+ }
+ }
+ is_opaque
+ }
+ ImageFormat::RG8 => true,
+ ImageFormat::RG16 => true,
+ ImageFormat::R8 => false,
+ ImageFormat::R16 => false,
+ ImageFormat::RGBAF32 |
+ ImageFormat::RGBAI32 => unreachable!(),
+ }
+}
+
+struct IsRoot(bool);
+
+pub struct YamlFrameReader {
+ yaml_path: PathBuf,
+ aux_dir: PathBuf,
+ frame_count: u32,
+
+ display_lists: Vec<(PipelineId, BuiltDisplayList)>,
+
+ watch_source: bool,
+ list_resources: bool,
+
+ /// A HashMap of offsets which specify what scroll offsets particular
+ /// scroll layers should be initialized with.
+ scroll_offsets: HashMap<ExternalScrollId, Vec<SampledScrollOffset>>,
+ next_external_scroll_id: u64,
+
+ image_map: HashMap<(PathBuf, Option<i64>), (ImageKey, LayoutSize)>,
+
+ fonts: HashMap<FontDescriptor, FontKey>,
+ font_instances: HashMap<(FontKey, FontSize, FontInstanceFlags, Option<ColorU>, SyntheticItalics), FontInstanceKey>,
+ font_render_mode: Option<FontRenderMode>,
+ allow_mipmaps: bool,
+
+ /// A HashMap that allows specifying a numeric id for clip and clip chains in YAML
+ /// and having each of those ids correspond to a unique ClipId.
+ user_clip_id_map: HashMap<u64, ClipId>,
+ user_clipchain_id_map: HashMap<u64, ClipChainId>,
+ user_spatial_id_map: HashMap<u64, SpatialId>,
+
+ spatial_id_stack: Vec<SpatialId>,
+
+ requested_frame: usize,
+ built_frame: usize,
+
+ yaml_string: String,
+ keyframes: Option<Yaml>,
+
+ external_image_handler: Option<Box<LocalExternalImageHandler>>,
+
+ next_spatial_key: u64,
+}
+
+impl YamlFrameReader {
+ pub fn new(yaml_path: &Path) -> YamlFrameReader {
+ YamlFrameReader {
+ watch_source: false,
+ list_resources: false,
+ yaml_path: yaml_path.to_owned(),
+ aux_dir: yaml_path.parent().unwrap().to_owned(),
+ frame_count: 0,
+ display_lists: Vec::new(),
+ scroll_offsets: HashMap::new(),
+ fonts: HashMap::new(),
+ font_instances: HashMap::new(),
+ font_render_mode: None,
+ allow_mipmaps: false,
+ image_map: HashMap::new(),
+ user_clip_id_map: HashMap::new(),
+ user_clipchain_id_map: HashMap::new(),
+ user_spatial_id_map: HashMap::new(),
+ spatial_id_stack: Vec::new(),
+ yaml_string: String::new(),
+ requested_frame: 0,
+ built_frame: usize::MAX,
+ keyframes: None,
+ external_image_handler: Some(Box::new(LocalExternalImageHandler::new())),
+ next_external_scroll_id: 1000, // arbitrary to easily see in logs which are implicit
+ next_spatial_key: 0,
+ }
+ }
+
+ pub fn deinit(mut self, wrench: &mut Wrench) {
+ let mut txn = Transaction::new();
+
+ for (_, font_instance) in self.font_instances.drain() {
+ txn.delete_font_instance(font_instance);
+ }
+
+ for (_, font) in self.fonts.drain() {
+ txn.delete_font(font);
+ }
+
+ wrench.api.send_transaction(wrench.document_id, txn);
+ }
+
+ fn top_space(&self) -> SpatialId {
+ *self.spatial_id_stack.last().unwrap()
+ }
+
+ pub fn yaml_path(&self) -> &PathBuf {
+ &self.yaml_path
+ }
+
+ pub fn new_from_args(args: &clap::ArgMatches) -> YamlFrameReader {
+ let yaml_file = args.value_of("INPUT").map(PathBuf::from).unwrap();
+
+ let mut y = YamlFrameReader::new(&yaml_file);
+
+ y.keyframes = args.value_of("keyframes").map(|path| {
+ let mut file = File::open(&path).unwrap();
+ let mut keyframes_string = String::new();
+ file.read_to_string(&mut keyframes_string).unwrap();
+ YamlLoader::load_from_str(&keyframes_string)
+ .expect("Failed to parse keyframes file")
+ .pop()
+ .unwrap()
+ });
+ y.list_resources = args.is_present("list-resources");
+ y.watch_source = args.is_present("watch");
+ y
+ }
+
+ pub fn reset(&mut self) {
+ self.scroll_offsets.clear();
+ self.display_lists.clear();
+ }
+
+ fn build(&mut self, wrench: &mut Wrench) {
+ let yaml = YamlLoader::load_from_str(&self.yaml_string)
+ .map(|mut yaml| {
+ assert_eq!(yaml.len(), 1);
+ yaml.pop().unwrap()
+ })
+ .expect("Failed to parse YAML file");
+
+ self.reset();
+
+ if let Some(pipelines) = yaml["pipelines"].as_vec() {
+ for pipeline in pipelines {
+ self.build_pipeline(wrench, pipeline["id"].as_pipeline_id().unwrap(), pipeline);
+ }
+ }
+
+ let root_stacking_context = &yaml["root"];
+ assert_ne!(*root_stacking_context, Yaml::BadValue);
+ self.build_pipeline(wrench, wrench.root_pipeline_id, root_stacking_context);
+
+ // If replaying the same frame during interactive use, the frame gets rebuilt,
+ // but the external image handler has already been consumed by the renderer.
+ if let Some(external_image_handler) = self.external_image_handler.take() {
+ wrench.renderer.set_external_image_handler(external_image_handler);
+ }
+ }
+
+ fn build_pipeline(
+ &mut self,
+ wrench: &mut Wrench,
+ pipeline_id: PipelineId,
+ yaml: &Yaml
+ ) {
+ // Don't allow referencing clips between pipelines for now.
+ self.user_clip_id_map.clear();
+ self.user_clipchain_id_map.clear();
+ self.user_spatial_id_map.clear();
+ self.spatial_id_stack.clear();
+ self.spatial_id_stack.push(SpatialId::root_scroll_node(pipeline_id));
+
+ let mut builder = DisplayListBuilder::new(pipeline_id);
+ builder.begin();
+ let mut info = CommonItemProperties {
+ clip_rect: LayoutRect::zero(),
+ clip_chain_id: ClipChainId::INVALID,
+ spatial_id: SpatialId::new(0, PipelineId::dummy()),
+ flags: PrimitiveFlags::default(),
+ };
+ self.add_stacking_context_from_yaml(&mut builder, wrench, yaml, IsRoot(true), &mut info);
+ self.display_lists.push(builder.end());
+
+ assert_eq!(self.spatial_id_stack.len(), 1);
+ }
+
+ fn to_clip_chain_id(
+ &self,
+ item: &Yaml,
+ builder: &mut DisplayListBuilder,
+ ) -> Option<ClipChainId> {
+ match *item {
+ Yaml::Integer(value) => {
+ Some(self.user_clipchain_id_map[&(value as u64)])
+ }
+ Yaml::Array(ref array) => {
+ let clip_ids: Vec<ClipId> = array
+ .iter()
+ .map(|id| {
+ let id = id.as_i64().expect("invalid clip id") as u64;
+ self.user_clip_id_map[&id]
+ })
+ .collect();
+
+ Some(builder.define_clip_chain(None, clip_ids))
+ }
+ _ => None,
+ }
+ }
+
+ fn to_spatial_id(&self, item: &Yaml, pipeline_id: PipelineId) -> Option<SpatialId> {
+ match *item {
+ Yaml::Integer(value) => Some(self.user_spatial_id_map[&(value as u64)]),
+ Yaml::String(ref id_string) if id_string == "root-reference-frame" =>
+ Some(SpatialId::root_reference_frame(pipeline_id)),
+ Yaml::String(ref id_string) if id_string == "root-scroll-node" =>
+ Some(SpatialId::root_scroll_node(pipeline_id)),
+ Yaml::BadValue => None,
+ _ => {
+ println!("Unable to parse SpatialId {:?}", item);
+ None
+ }
+ }
+ }
+
+ fn add_clip_id_mapping(&mut self, numeric_id: u64, real_id: ClipId) {
+ assert_ne!(numeric_id, 0, "id=0 is reserved for the root clip");
+ self.user_clip_id_map.insert(numeric_id, real_id);
+ }
+
+ fn add_clip_chain_id_mapping(&mut self, numeric_id: u64, real_id: ClipChainId) {
+ assert_ne!(numeric_id, 0, "id=0 is reserved for the root clip-chain");
+ self.user_clipchain_id_map.insert(numeric_id, real_id);
+ }
+
+ fn add_spatial_id_mapping(&mut self, numeric_id: u64, real_id: SpatialId) {
+ assert_ne!(numeric_id, 0, "id=0 is reserved for the root reference frame");
+ assert_ne!(numeric_id, 1, "id=1 is reserved for the root scroll node");
+ self.user_spatial_id_map.insert(numeric_id, real_id);
+ }
+
+ fn to_hit_testing_tag(&self, item: &Yaml) -> Option<ItemTag> {
+ match *item {
+ Yaml::Array(ref array) if array.len() == 2 => {
+ match (array[0].as_i64(), array[1].as_i64()) {
+ (Some(first), Some(second)) => Some((first as u64, second as u16)),
+ _ => None,
+ }
+ }
+ _ => None,
+ }
+
+ }
+
+ fn add_or_get_image(
+ &mut self,
+ file: &Path,
+ tiling: Option<i64>,
+ item: &Yaml,
+ wrench: &mut Wrench,
+ ) -> (ImageKey, LayoutSize) {
+ let key = (file.to_owned(), tiling);
+ if let Some(k) = self.image_map.get(&key) {
+ return *k;
+ }
+
+ if self.list_resources { println!("{}", file.to_string_lossy()); }
+ let (descriptor, image_data) = match image::open(file) {
+ Ok(image) => {
+ let (image_width, image_height) = image.dimensions();
+ let (format, bytes) = match image {
+ image::DynamicImage::ImageLuma8(_) => {
+ (ImageFormat::R8, image.to_bytes())
+ }
+ image::DynamicImage::ImageRgba8(_) => {
+ let mut pixels = image.to_bytes();
+ premultiply(pixels.as_mut_slice());
+ (ImageFormat::BGRA8, pixels)
+ }
+ image::DynamicImage::ImageRgb8(_) => {
+ let bytes = image.to_bytes();
+ let mut pixels = Vec::with_capacity(image_width as usize * image_height as usize * 4);
+ for bgr in bytes.chunks(3) {
+ pixels.extend_from_slice(&[
+ bgr[2],
+ bgr[1],
+ bgr[0],
+ 0xff
+ ]);
+ }
+ (ImageFormat::BGRA8, pixels)
+ }
+ _ => panic!("We don't support whatever your crazy image type is, come on"),
+ };
+ let mut flags = ImageDescriptorFlags::empty();
+ if is_image_opaque(format, &bytes[..]) {
+ flags |= ImageDescriptorFlags::IS_OPAQUE;
+ }
+ if self.allow_mipmaps {
+ flags |= ImageDescriptorFlags::ALLOW_MIPMAPS;
+ }
+ let descriptor = ImageDescriptor::new(
+ image_width as i32,
+ image_height as i32,
+ format,
+ flags,
+ );
+ let data = ImageData::new(bytes);
+ (descriptor, data)
+ }
+ _ => {
+ // This is a hack but it is convenient when generating test cases and avoids
+ // bloating the repository.
+ match parse_function(
+ file.components()
+ .last()
+ .unwrap()
+ .as_os_str()
+ .to_str()
+ .unwrap(),
+ ) {
+ ("xy-gradient", args, _) => generate_xy_gradient_image(
+ args.get(0).unwrap_or(&"1000").parse::<u32>().unwrap(),
+ args.get(1).unwrap_or(&"1000").parse::<u32>().unwrap(),
+ ),
+ ("solid-color", args, _) => generate_solid_color_image(
+ args.get(0).unwrap_or(&"255").parse::<u8>().unwrap(),
+ args.get(1).unwrap_or(&"255").parse::<u8>().unwrap(),
+ args.get(2).unwrap_or(&"255").parse::<u8>().unwrap(),
+ args.get(3).unwrap_or(&"255").parse::<u8>().unwrap(),
+ args.get(4).unwrap_or(&"1000").parse::<u32>().unwrap(),
+ args.get(5).unwrap_or(&"1000").parse::<u32>().unwrap(),
+ ),
+ (name @ "transparent-checkerboard", args, _) |
+ (name @ "checkerboard", args, _) => {
+ let border = args.get(0).unwrap_or(&"4").parse::<u32>().unwrap();
+
+ let (x_size, y_size, x_count, y_count) = match args.len() {
+ 3 => {
+ let size = args.get(1).unwrap_or(&"32").parse::<u32>().unwrap();
+ let count = args.get(2).unwrap_or(&"8").parse::<u32>().unwrap();
+ (size, size, count, count)
+ }
+ 5 => {
+ let x_size = args.get(1).unwrap_or(&"32").parse::<u32>().unwrap();
+ let y_size = args.get(2).unwrap_or(&"32").parse::<u32>().unwrap();
+ let x_count = args.get(3).unwrap_or(&"8").parse::<u32>().unwrap();
+ let y_count = args.get(4).unwrap_or(&"8").parse::<u32>().unwrap();
+ (x_size, y_size, x_count, y_count)
+ }
+ _ => {
+ panic!("invalid checkerboard function");
+ }
+ };
+
+ let kind = if name == "transparent-checkerboard" {
+ CheckerboardKind::BlackTransparent
+ } else {
+ CheckerboardKind::BlackGrey
+ };
+
+ generate_checkerboard_image(
+ border,
+ x_size,
+ y_size,
+ x_count,
+ y_count,
+ kind,
+ )
+ }
+ _ => {
+ panic!("Failed to load image {:?}", file.to_str());
+ }
+ }
+ }
+ };
+ let tiling = tiling.map(|tile_size| tile_size as u16);
+ let image_key = wrench.api.generate_image_key();
+ let mut txn = Transaction::new();
+
+ let external = item["external"].as_bool().unwrap_or(false);
+ if external {
+ // This indicates we want to simulate an external texture,
+ // ensure it gets created as such
+ let external_target = match item["external-target"].as_str() {
+ Some("2d") => ImageBufferKind::Texture2D,
+ Some("rect") => ImageBufferKind::TextureRect,
+ Some(t) => panic!("Unsupported external texture target: {}", t),
+ None => ImageBufferKind::Texture2D,
+ };
+
+ let external_image_data =
+ self.external_image_handler.as_mut().unwrap().add_image(
+ &wrench.renderer.device,
+ descriptor,
+ external_target,
+ image_data
+ );
+ txn.add_image(image_key, descriptor, external_image_data, tiling);
+ } else {
+ txn.add_image(image_key, descriptor, image_data, tiling);
+ }
+
+ wrench.api.send_transaction(wrench.document_id, txn);
+ let val = (
+ image_key,
+ LayoutSize::new(descriptor.size.width as f32, descriptor.size.height as f32),
+ );
+ self.image_map.insert(key, val);
+ val
+ }
+
+ fn get_or_create_font(&mut self, desc: FontDescriptor, wrench: &mut Wrench) -> FontKey {
+ let list_resources = self.list_resources;
+ *self.fonts
+ .entry(desc.clone())
+ .or_insert_with(|| match desc {
+ FontDescriptor::Path {
+ ref path,
+ font_index,
+ } => {
+ if list_resources { println!("{}", path.to_string_lossy()); }
+ let mut file = File::open(path).expect("Couldn't open font file");
+ let mut bytes = vec![];
+ file.read_to_end(&mut bytes)
+ .expect("failed to read font file");
+ wrench.font_key_from_bytes(bytes, font_index)
+ }
+ FontDescriptor::Family { ref name } => wrench.font_key_from_name(name),
+ FontDescriptor::Properties {
+ ref family,
+ weight,
+ style,
+ stretch,
+ } => wrench.font_key_from_properties(family, weight, style, stretch),
+ })
+ }
+
+ pub fn allow_mipmaps(&mut self, allow_mipmaps: bool) {
+ self.allow_mipmaps = allow_mipmaps;
+ }
+
+ pub fn set_font_render_mode(&mut self, render_mode: Option<FontRenderMode>) {
+ self.font_render_mode = render_mode;
+ }
+
+ fn get_or_create_font_instance(
+ &mut self,
+ font_key: FontKey,
+ size: f32,
+ bg_color: Option<ColorU>,
+ flags: FontInstanceFlags,
+ synthetic_italics: SyntheticItalics,
+ wrench: &mut Wrench,
+ ) -> FontInstanceKey {
+ let font_render_mode = self.font_render_mode;
+
+ *self.font_instances
+ .entry((font_key, size.into(), flags, bg_color, synthetic_italics))
+ .or_insert_with(|| {
+ wrench.add_font_instance(
+ font_key,
+ size,
+ flags,
+ font_render_mode,
+ bg_color,
+ synthetic_italics,
+ )
+ })
+ }
+
+ fn as_image_mask(&mut self, item: &Yaml, wrench: &mut Wrench) -> Option<ImageMask> {
+ item.as_hash()?;
+
+ let tiling = item["tile-size"].as_i64();
+
+ let (image_key, image_dims) = match item["image"].as_str() {
+ Some("invalid") => (ImageKey::DUMMY, LayoutSize::new(100.0, 100.0)),
+ Some(filename) => {
+ let mut file = self.aux_dir.clone();
+ file.push(filename);
+ self.add_or_get_image(&file, tiling, item, wrench)
+ }
+ None => {
+ warn!("No image provided for the image-mask!");
+ return None;
+ }
+ };
+
+ let image_rect = item["rect"]
+ .as_rect()
+ .unwrap_or_else(|| LayoutRect::from_size(image_dims));
+ Some(ImageMask {
+ image: image_key,
+ rect: image_rect,
+ })
+ }
+
+ fn handle_rect(
+ &self,
+ dl: &mut DisplayListBuilder,
+ item: &Yaml,
+ info: &CommonItemProperties,
+ ) {
+ let bounds_key = if item["type"].is_badvalue() {
+ "rect"
+ } else {
+ "bounds"
+ };
+
+ let bounds = self.resolve_rect(&item[bounds_key]);
+ let color = self.resolve_colorf(&item["color"]).unwrap_or(ColorF::BLACK);
+ dl.push_rect(info, bounds, color);
+ }
+
+ fn handle_clear_rect(
+ &self,
+ dl: &mut DisplayListBuilder,
+ item: &Yaml,
+ info: &CommonItemProperties,
+ ) {
+ let bounds = item["bounds"].as_rect().expect("clear-rect type must have bounds");
+ dl.push_clear_rect(info, bounds);
+ }
+
+ fn handle_hit_test(
+ &mut self,
+ dl: &mut DisplayListBuilder,
+ item: &Yaml,
+ info: &mut CommonItemProperties,
+ ) {
+ info.clip_rect = try_intersect!(
+ item["bounds"].as_rect().expect("hit-test type must have bounds"),
+ &info.clip_rect
+ );
+
+ if let Some(tag) = self.to_hit_testing_tag(&item["hit-testing-tag"]) {
+ dl.push_hit_test(
+ info.clip_rect,
+ info.clip_chain_id,
+ info.spatial_id,
+ info.flags,
+ tag,
+ );
+ }
+ }
+
+ fn handle_line(
+ &mut self,
+ dl: &mut DisplayListBuilder,
+ item: &Yaml,
+ info: &mut CommonItemProperties,
+ ) {
+ let color = item["color"].as_colorf().unwrap_or(ColorF::BLACK);
+ let orientation = item["orientation"]
+ .as_str()
+ .and_then(LineOrientation::from_str)
+ .expect("line must have orientation");
+ let style = item["style"]
+ .as_str()
+ .and_then(LineStyle::from_str)
+ .expect("line must have style");
+
+ let wavy_line_thickness = if let LineStyle::Wavy = style {
+ item["thickness"].as_f32().expect("wavy lines must have a thickness")
+ } else {
+ 0.0
+ };
+
+ let area = if item["baseline"].is_badvalue() {
+ let bounds_key = if item["type"].is_badvalue() {
+ "rect"
+ } else {
+ "bounds"
+ };
+
+ item[bounds_key]
+ .as_rect()
+ .expect("line type must have bounds")
+ } else {
+ // Legacy line representation
+ let baseline = item["baseline"].as_f32().expect("line must have baseline");
+ let start = item["start"].as_f32().expect("line must have start");
+ let end = item["end"].as_f32().expect("line must have end");
+ let width = item["width"].as_f32().expect("line must have width");
+
+ match orientation {
+ LineOrientation::Horizontal => {
+ LayoutRect::from_origin_and_size(
+ LayoutPoint::new(start, baseline),
+ LayoutSize::new(end - start, width),
+ )
+ }
+ LineOrientation::Vertical => {
+ LayoutRect::from_origin_and_size(
+ LayoutPoint::new(baseline, start),
+ LayoutSize::new(width, end - start),
+ )
+ }
+ }
+ };
+
+ dl.push_line(
+ info,
+ &area,
+ wavy_line_thickness,
+ orientation,
+ &color,
+ style,
+ );
+ }
+
+ fn handle_gradient(
+ &mut self,
+ dl: &mut DisplayListBuilder,
+ item: &Yaml,
+ info: &mut CommonItemProperties,
+ ) {
+ let bounds_key = if item["type"].is_badvalue() {
+ "gradient"
+ } else {
+ "bounds"
+ };
+ let bounds = item[bounds_key]
+ .as_rect()
+ .expect("gradient must have bounds");
+
+ let gradient = item.as_gradient(dl);
+ let tile_size = item["tile-size"].as_size().unwrap_or_else(|| bounds.size());
+ let tile_spacing = item["tile-spacing"].as_size().unwrap_or_else(LayoutSize::zero);
+
+ dl.push_gradient(
+ info,
+ bounds,
+ gradient,
+ tile_size,
+ tile_spacing
+ );
+ }
+
+ fn handle_radial_gradient(
+ &mut self,
+ dl: &mut DisplayListBuilder,
+ item: &Yaml,
+ info: &mut CommonItemProperties,
+ ) {
+ let bounds_key = if item["type"].is_badvalue() {
+ "radial-gradient"
+ } else {
+ "bounds"
+ };
+ let bounds = item[bounds_key]
+ .as_rect()
+ .expect("radial gradient must have bounds");
+ let gradient = item.as_radial_gradient(dl);
+ let tile_size = item["tile-size"].as_size().unwrap_or_else(|| bounds.size());
+ let tile_spacing = item["tile-spacing"].as_size().unwrap_or_else(LayoutSize::zero);
+
+ dl.push_radial_gradient(
+ info,
+ bounds,
+ gradient,
+ tile_size,
+ tile_spacing,
+ );
+ }
+
+ fn handle_conic_gradient(
+ &mut self,
+ dl: &mut DisplayListBuilder,
+ item: &Yaml,
+ info: &mut CommonItemProperties,
+ ) {
+ let bounds_key = if item["type"].is_badvalue() {
+ "conic-gradient"
+ } else {
+ "bounds"
+ };
+ let bounds = item[bounds_key]
+ .as_rect()
+ .expect("conic gradient must have bounds");
+ let gradient = item.as_conic_gradient(dl);
+ let tile_size = item["tile-size"].as_size().unwrap_or_else(|| bounds.size());
+ let tile_spacing = item["tile-spacing"].as_size().unwrap_or_else(LayoutSize::zero);
+
+ dl.push_conic_gradient(
+ info,
+ bounds,
+ gradient,
+ tile_size,
+ tile_spacing,
+ );
+ }
+
+ fn handle_border(
+ &mut self,
+ dl: &mut DisplayListBuilder,
+ wrench: &mut Wrench,
+ item: &Yaml,
+ info: &mut CommonItemProperties,
+ ) {
+ let bounds_key = if item["type"].is_badvalue() {
+ "border"
+ } else {
+ "bounds"
+ };
+ let bounds = item[bounds_key].as_rect().expect("borders must have bounds");
+ let widths = item["width"]
+ .as_vec_f32()
+ .expect("borders must have width(s)");
+ let widths = broadcast(&widths, 4);
+ let widths = LayoutSideOffsets::new(widths[0], widths[3], widths[2], widths[1]);
+ let border_details = if let Some(border_type) = item["border-type"].as_str() {
+ match border_type {
+ "normal" => {
+ let colors = item["color"]
+ .as_vec_colorf()
+ .expect("borders must have color(s)");
+ let styles = item["style"]
+ .as_vec_string()
+ .expect("borders must have style(s)");
+ let styles = styles
+ .iter()
+ .map(|s| match s.as_str() {
+ "none" => BorderStyle::None,
+ "solid" => BorderStyle::Solid,
+ "double" => BorderStyle::Double,
+ "dotted" => BorderStyle::Dotted,
+ "dashed" => BorderStyle::Dashed,
+ "hidden" => BorderStyle::Hidden,
+ "ridge" => BorderStyle::Ridge,
+ "inset" => BorderStyle::Inset,
+ "outset" => BorderStyle::Outset,
+ "groove" => BorderStyle::Groove,
+ s => {
+ panic!("Unknown border style '{}'", s);
+ }
+ })
+ .collect::<Vec<BorderStyle>>();
+ let radius = item["radius"]
+ .as_border_radius()
+ .unwrap_or_else(BorderRadius::zero);
+
+ let colors = broadcast(&colors, 4);
+ let styles = broadcast(&styles, 4);
+
+ let top = BorderSide {
+ color: colors[0],
+ style: styles[0],
+ };
+ let right = BorderSide {
+ color: colors[1],
+ style: styles[1],
+ };
+ let bottom = BorderSide {
+ color: colors[2],
+ style: styles[2],
+ };
+ let left = BorderSide {
+ color: colors[3],
+ style: styles[3],
+ };
+ let do_aa = item["do_aa"].as_bool().unwrap_or(true);
+ Some(BorderDetails::Normal(NormalBorder {
+ top,
+ left,
+ bottom,
+ right,
+ radius,
+ do_aa,
+ }))
+ }
+ "image" | "gradient" | "radial-gradient" | "conic-gradient" => {
+ let image_width = item["image-width"]
+ .as_i64()
+ .unwrap_or(bounds.width() as i64);
+ let image_height = item["image-height"]
+ .as_i64()
+ .unwrap_or(bounds.height() as i64);
+ let fill = item["fill"].as_bool().unwrap_or(false);
+
+ let slice = if let Some(slice) = item["slice"].as_vec_u32() {
+ broadcast(&slice, 4)
+ } else {
+ vec![widths.top as u32, widths.left as u32, widths.bottom as u32, widths.right as u32]
+ };
+
+ let repeat_horizontal = match item["repeat-horizontal"]
+ .as_str()
+ .unwrap_or("stretch")
+ {
+ "stretch" => RepeatMode::Stretch,
+ "repeat" => RepeatMode::Repeat,
+ "round" => RepeatMode::Round,
+ "space" => RepeatMode::Space,
+ s => panic!("Unknown box border image repeat mode {}", s),
+ };
+ let repeat_vertical = match item["repeat-vertical"]
+ .as_str()
+ .unwrap_or("stretch")
+ {
+ "stretch" => RepeatMode::Stretch,
+ "repeat" => RepeatMode::Repeat,
+ "round" => RepeatMode::Round,
+ "space" => RepeatMode::Space,
+ s => panic!("Unknown box border image repeat mode {}", s),
+ };
+ let source = match border_type {
+ "image" => {
+ let file = rsrc_path(&item["image-source"], &self.aux_dir);
+ let (image_key, _) = self
+ .add_or_get_image(&file, None, item, wrench);
+ NinePatchBorderSource::Image(image_key, ImageRendering::Auto)
+ }
+ "gradient" => {
+ let gradient = item.as_gradient(dl);
+ NinePatchBorderSource::Gradient(gradient)
+ }
+ "radial-gradient" => {
+ let gradient = item.as_radial_gradient(dl);
+ NinePatchBorderSource::RadialGradient(gradient)
+ }
+ "conic-gradient" => {
+ let gradient = item.as_conic_gradient(dl);
+ NinePatchBorderSource::ConicGradient(gradient)
+ }
+ _ => unreachable!("Unexpected border type"),
+ };
+
+ Some(BorderDetails::NinePatch(NinePatchBorder {
+ source,
+ width: image_width as i32,
+ height: image_height as i32,
+ slice: SideOffsets2D::new(slice[0] as i32, slice[1] as i32, slice[2] as i32, slice[3] as i32),
+ fill,
+ repeat_horizontal,
+ repeat_vertical,
+ }))
+ }
+ _ => {
+ println!("Unable to parse border {:?}", item);
+ None
+ }
+ }
+ } else {
+ println!("Unable to parse border {:?}", item);
+ None
+ };
+ if let Some(details) = border_details {
+ dl.push_border(info, bounds, widths, details);
+ }
+ }
+
+ fn handle_box_shadow(
+ &mut self,
+ dl: &mut DisplayListBuilder,
+ item: &Yaml,
+ info: &mut CommonItemProperties,
+ ) {
+ let bounds_key = if item["type"].is_badvalue() {
+ "box-shadow"
+ } else {
+ "bounds"
+ };
+ let bounds = item[bounds_key]
+ .as_rect()
+ .expect("box shadow must have bounds");
+ let box_bounds = item["box-bounds"].as_rect().unwrap_or(bounds);
+ let offset = self.resolve_vector(&item["offset"], LayoutVector2D::zero());
+ let color = item["color"]
+ .as_colorf()
+ .unwrap_or_else(|| ColorF::new(0.0, 0.0, 0.0, 1.0));
+ let blur_radius = item["blur-radius"].as_force_f32().unwrap_or(0.0);
+ let spread_radius = item["spread-radius"].as_force_f32().unwrap_or(0.0);
+ let border_radius = item["border-radius"]
+ .as_border_radius()
+ .unwrap_or_else(BorderRadius::zero);
+ let clip_mode = if let Some(mode) = item["clip-mode"].as_str() {
+ match mode {
+ "outset" => BoxShadowClipMode::Outset,
+ "inset" => BoxShadowClipMode::Inset,
+ s => panic!("Unknown box shadow clip mode {}", s),
+ }
+ } else {
+ BoxShadowClipMode::Outset
+ };
+
+ dl.push_box_shadow(
+ info,
+ box_bounds,
+ offset,
+ color,
+ blur_radius,
+ spread_radius,
+ border_radius,
+ clip_mode,
+ );
+ }
+
+ fn handle_yuv_image(
+ &mut self,
+ dl: &mut DisplayListBuilder,
+ wrench: &mut Wrench,
+ item: &Yaml,
+ info: &mut CommonItemProperties,
+ ) {
+ // TODO(gw): Support other YUV color depth and spaces.
+ let color_depth = ColorDepth::Color8;
+ let color_space = YuvColorSpace::Rec709;
+ let color_range = ColorRange::Limited;
+
+ let yuv_data = match item["format"].as_str().expect("no format supplied") {
+ "planar" => {
+ let y_path = rsrc_path(&item["src-y"], &self.aux_dir);
+ let (y_key, _) = self.add_or_get_image(&y_path, None, item, wrench);
+
+ let u_path = rsrc_path(&item["src-u"], &self.aux_dir);
+ let (u_key, _) = self.add_or_get_image(&u_path, None, item, wrench);
+
+ let v_path = rsrc_path(&item["src-v"], &self.aux_dir);
+ let (v_key, _) = self.add_or_get_image(&v_path, None, item, wrench);
+
+ YuvData::PlanarYCbCr(y_key, u_key, v_key)
+ }
+ "nv12" => {
+ let y_path = rsrc_path(&item["src-y"], &self.aux_dir);
+ let (y_key, _) = self.add_or_get_image(&y_path, None, item, wrench);
+
+ let uv_path = rsrc_path(&item["src-uv"], &self.aux_dir);
+ let (uv_key, _) = self.add_or_get_image(&uv_path, None, item, wrench);
+
+ YuvData::NV12(y_key, uv_key)
+ }
+ "p010" => {
+ let y_path = rsrc_path(&item["src-y"], &self.aux_dir);
+ let (y_key, _) = self.add_or_get_image(&y_path, None, item, wrench);
+
+ let uv_path = rsrc_path(&item["src-uv"], &self.aux_dir);
+ let (uv_key, _) = self.add_or_get_image(&uv_path, None, item, wrench);
+
+ YuvData::P010(y_key, uv_key)
+ }
+ "interleaved" => {
+ let yuv_path = rsrc_path(&item["src"], &self.aux_dir);
+ let (yuv_key, _) = self.add_or_get_image(&yuv_path, None, item, wrench);
+
+ YuvData::InterleavedYCbCr(yuv_key)
+ }
+ _ => {
+ panic!("unexpected yuv format");
+ }
+ };
+
+ let bounds = item["bounds"].as_vec_f32().unwrap();
+ let bounds = LayoutRect::from_origin_and_size(
+ LayoutPoint::new(bounds[0], bounds[1]),
+ LayoutSize::new(bounds[2], bounds[3]),
+ );
+
+ dl.push_yuv_image(
+ info,
+ bounds,
+ yuv_data,
+ color_depth,
+ color_space,
+ color_range,
+ ImageRendering::Auto,
+ );
+ }
+
+ fn handle_image(
+ &mut self,
+ dl: &mut DisplayListBuilder,
+ wrench: &mut Wrench,
+ item: &Yaml,
+ info: &mut CommonItemProperties,
+ ) {
+ let filename = &item[if item["type"].is_badvalue() {
+ "image"
+ } else {
+ "src"
+ }];
+ let tiling = item["tile-size"].as_i64();
+ let file = rsrc_path(filename, &self.aux_dir);
+ let (image_key, image_dims) =
+ self.add_or_get_image(&file, tiling, item, wrench);
+
+ let bounds_raws = item["bounds"].as_vec_f32().unwrap();
+ let bounds = if bounds_raws.len() == 2 {
+ LayoutRect::from_origin_and_size(LayoutPoint::new(bounds_raws[0], bounds_raws[1]), image_dims)
+ } else if bounds_raws.len() == 4 {
+ LayoutRect::from_origin_and_size(
+ LayoutPoint::new(bounds_raws[0], bounds_raws[1]),
+ LayoutSize::new(bounds_raws[2], bounds_raws[3]),
+ )
+ } else {
+ panic!(
+ "image expected 2 or 4 values in bounds, got '{:?}'",
+ item["bounds"]
+ );
+ };
+ let rendering = match item["rendering"].as_str() {
+ Some("auto") | None => ImageRendering::Auto,
+ Some("crisp-edges") => ImageRendering::CrispEdges,
+ Some("pixelated") => ImageRendering::Pixelated,
+ Some(_) => panic!(
+ "ImageRendering can be auto, crisp-edges, or pixelated -- got {:?}",
+ item
+ ),
+ };
+ let alpha_type = match item["alpha-type"].as_str() {
+ Some("premultiplied-alpha") | None => AlphaType::PremultipliedAlpha,
+ Some("alpha") => AlphaType::Alpha,
+ Some(_) => panic!(
+ "AlphaType can be premultiplied-alpha or alpha -- got {:?}",
+ item
+ ),
+ };
+ let color = item["color"]
+ .as_colorf()
+ .unwrap_or_else(|| ColorF::WHITE);
+ let stretch_size = item["stretch-size"].as_size();
+ let tile_spacing = item["tile-spacing"].as_size();
+ if stretch_size.is_none() && tile_spacing.is_none() {
+ dl.push_image(
+ info,
+ bounds,
+ rendering,
+ alpha_type,
+ image_key,
+ color,
+ );
+ } else {
+ dl.push_repeating_image(
+ info,
+ bounds,
+ stretch_size.unwrap_or(image_dims),
+ tile_spacing.unwrap_or_else(LayoutSize::zero),
+ rendering,
+ alpha_type,
+ image_key,
+ color,
+ );
+ }
+ }
+
+ fn handle_text(
+ &mut self,
+ dl: &mut DisplayListBuilder,
+ wrench: &mut Wrench,
+ item: &Yaml,
+ info: &mut CommonItemProperties,
+ ) {
+ let size = item["size"].as_pt_to_f32().unwrap_or(16.0);
+ let color = item["color"].as_colorf().unwrap_or(ColorF::BLACK);
+ let bg_color = item["bg-color"].as_colorf().map(|c| c.into());
+ let synthetic_italics = if let Some(angle) = item["synthetic-italics"].as_f32() {
+ SyntheticItalics::from_degrees(angle)
+ } else if item["synthetic-italics"].as_bool().unwrap_or(false) {
+ SyntheticItalics::enabled()
+ } else {
+ SyntheticItalics::disabled()
+ };
+
+ let mut flags = FontInstanceFlags::empty();
+ if item["synthetic-bold"].as_bool().unwrap_or(false) {
+ flags |= FontInstanceFlags::SYNTHETIC_BOLD;
+ }
+ if item["embedded-bitmaps"].as_bool().unwrap_or(false) {
+ flags |= FontInstanceFlags::EMBEDDED_BITMAPS;
+ }
+ if item["transpose"].as_bool().unwrap_or(false) {
+ flags |= FontInstanceFlags::TRANSPOSE;
+ }
+ if item["flip-x"].as_bool().unwrap_or(false) {
+ flags |= FontInstanceFlags::FLIP_X;
+ }
+ if item["flip-y"].as_bool().unwrap_or(false) {
+ flags |= FontInstanceFlags::FLIP_Y;
+ }
+
+ assert!(
+ item["blur-radius"].is_badvalue(),
+ "text no longer has a blur radius, use PushShadow and PopAllShadows"
+ );
+
+ let desc = FontDescriptor::from_yaml(item, &self.aux_dir);
+ let font_key = self.get_or_create_font(desc, wrench);
+ let font_instance_key = self.get_or_create_font_instance(font_key,
+ size,
+ bg_color,
+ flags,
+ synthetic_italics,
+ wrench);
+
+ assert!(
+ !(item["glyphs"].is_badvalue() && item["text"].is_badvalue()),
+ "text item had neither text nor glyphs!"
+ );
+
+ let (glyphs, rect) = if item["text"].is_badvalue() {
+ // if glyphs are specified, then the glyph positions can have the
+ // origin baked in.
+ let origin = item["origin"]
+ .as_point()
+ .unwrap_or(LayoutPoint::new(0.0, 0.0));
+ let glyph_indices = item["glyphs"].as_vec_u32().unwrap();
+ let glyph_offsets = item["offsets"].as_vec_f32().unwrap();
+ assert_eq!(glyph_offsets.len(), glyph_indices.len() * 2);
+
+ let glyphs = glyph_indices
+ .iter()
+ .enumerate()
+ .map(|k| {
+ GlyphInstance {
+ index: *k.1,
+ // In the future we want to change the API to be relative, eliminating this
+ point: LayoutPoint::new(
+ origin.x + glyph_offsets[k.0 * 2],
+ origin.y + glyph_offsets[k.0 * 2 + 1],
+ ),
+ }
+ })
+ .collect::<Vec<_>>();
+ // TODO(gw): We could optionally use the WR API to query glyph dimensions
+ // here and calculate the bounding region here if we want to.
+ let rect = item["bounds"]
+ .as_rect()
+ .expect("Text items with glyphs require bounds [for now]");
+ (glyphs, rect)
+ } else {
+ let text = item["text"].as_str().unwrap();
+ let origin = item["origin"]
+ .as_point()
+ .expect("origin required for text without glyphs");
+ let (glyph_indices, glyph_positions, bounds) = wrench.layout_simple_ascii(
+ font_key,
+ font_instance_key,
+ text,
+ size,
+ origin,
+ flags,
+ );
+
+ let glyphs = glyph_indices
+ .iter()
+ .zip(glyph_positions)
+ .map(|arg| {
+ GlyphInstance {
+ index: *arg.0 as u32,
+ point: arg.1,
+ }
+ })
+ .collect::<Vec<_>>();
+ (glyphs, bounds)
+ };
+
+ dl.push_text(
+ info,
+ rect,
+ &glyphs,
+ font_instance_key,
+ color,
+ None,
+ );
+ }
+
+ fn handle_iframe(
+ &mut self,
+ dl: &mut DisplayListBuilder,
+ item: &Yaml,
+ info: &mut CommonItemProperties,
+ ) {
+ let bounds = item["bounds"].as_rect().expect("iframe must have bounds");
+ let pipeline_id = item["id"].as_pipeline_id().unwrap();
+ let ignore = item["ignore_missing_pipeline"].as_bool().unwrap_or(true);
+ dl.push_iframe(
+ bounds,
+ info.clip_rect,
+ &SpaceAndClipInfo {
+ spatial_id: info.spatial_id,
+ clip_chain_id: info.clip_chain_id
+ },
+ pipeline_id,
+ ignore
+ );
+ }
+
+ fn get_item_type_from_yaml(item: &Yaml) -> &str {
+ let shorthands = [
+ "rect",
+ "image",
+ "text",
+ "glyphs",
+ "box-shadow", // Note: box_shadow shorthand check has to come before border.
+ "border",
+ "gradient",
+ "radial-gradient",
+ "conic-gradient"
+ ];
+
+ for shorthand in shorthands.iter() {
+ if !item[*shorthand].is_badvalue() {
+ return shorthand;
+ }
+ }
+ item["type"].as_str().unwrap_or("unknown")
+ }
+
+ fn add_display_list_items_from_yaml(
+ &mut self,
+ dl: &mut DisplayListBuilder,
+ wrench: &mut Wrench,
+ yaml_items: &[Yaml],
+ ) {
+ // A very large number (but safely far away from finite limits of f32)
+ let big_number = 1.0e30;
+ // A rect that should in practical terms serve as a no-op for clipping
+ let full_clip = LayoutRect::from_origin_and_size(
+ LayoutPoint::new(-big_number / 2.0, -big_number / 2.0),
+ LayoutSize::new(big_number, big_number));
+
+ for item in yaml_items {
+ let item_type = Self::get_item_type_from_yaml(item);
+
+ let spatial_id = self.to_spatial_id(&item["spatial-id"], dl.pipeline_id);
+
+ if let Some(spatial_id) = spatial_id {
+ self.spatial_id_stack.push(spatial_id);
+ }
+
+ let clip_rect = item["clip-rect"].as_rect().unwrap_or(full_clip);
+ let clip_chain_id = self.to_clip_chain_id(&item["clip-chain"], dl).unwrap_or(ClipChainId::INVALID);
+
+ let mut flags = PrimitiveFlags::default();
+ for (key, flag) in [
+ ("backface-visible", PrimitiveFlags::IS_BACKFACE_VISIBLE),
+ ("scrollbar-container", PrimitiveFlags::IS_SCROLLBAR_CONTAINER),
+ ("prefer-compositor-surface", PrimitiveFlags::PREFER_COMPOSITOR_SURFACE),
+ ] {
+ if let Some(value) = item[key].as_bool() {
+ flags.set(flag, value);
+ }
+ }
+
+
+ let mut info = CommonItemProperties {
+ clip_rect,
+ clip_chain_id,
+ spatial_id: self.top_space(),
+ flags,
+ };
+
+ match item_type {
+ "rect" => self.handle_rect(dl, item, &info),
+ "hit-test" => self.handle_hit_test(dl, item, &mut info),
+ "clear-rect" => self.handle_clear_rect(dl, item, &info),
+ "line" => self.handle_line(dl, item, &mut info),
+ "image" => self.handle_image(dl, wrench, item, &mut info),
+ "yuv-image" => self.handle_yuv_image(dl, wrench, item, &mut info),
+ "text" | "glyphs" => self.handle_text(dl, wrench, item, &mut info),
+ "scroll-frame" => self.handle_scroll_frame(dl, wrench, item),
+ "sticky-frame" => self.handle_sticky_frame(dl, wrench, item),
+ "clip" => self.handle_clip(dl, wrench, item),
+ "clip-chain" => self.handle_clip_chain(dl, item),
+ "border" => self.handle_border(dl, wrench, item, &mut info),
+ "gradient" => self.handle_gradient(dl, item, &mut info),
+ "radial-gradient" => self.handle_radial_gradient(dl, item, &mut info),
+ "conic-gradient" => self.handle_conic_gradient(dl, item, &mut info),
+ "box-shadow" => self.handle_box_shadow(dl, item, &mut info),
+ "iframe" => self.handle_iframe(dl, item, &mut info),
+ "stacking-context" => {
+ self.add_stacking_context_from_yaml(dl, wrench, item, IsRoot(false), &mut info)
+ }
+ "reference-frame" => self.handle_reference_frame(dl, wrench, item),
+ "computed-frame" => self.handle_computed_frame(dl, wrench, item),
+ "shadow" => self.handle_push_shadow(dl, item, &mut info),
+ "pop-all-shadows" => self.handle_pop_all_shadows(dl),
+ "backdrop-filter" => self.handle_backdrop_filter(dl, item, &mut info),
+ _ => println!("Skipping unknown item type: {:?}", item),
+ }
+
+ if spatial_id.is_some() {
+ self.spatial_id_stack.pop().unwrap();
+ }
+ }
+ }
+
+ fn next_spatial_key(&mut self) -> SpatialTreeItemKey {
+ let key = SpatialTreeItemKey::new(self.next_spatial_key, 0);
+ self.next_spatial_key += 1;
+ key
+ }
+
+ fn handle_scroll_frame(
+ &mut self,
+ dl: &mut DisplayListBuilder,
+ wrench: &mut Wrench,
+ yaml: &Yaml,
+ ) {
+ let clip_rect = yaml["bounds"]
+ .as_rect()
+ .expect("scroll frame must have a bounds");
+ let content_size = yaml["content-size"].as_size().unwrap_or_else(|| clip_rect.size());
+ let content_rect = LayoutRect::from_origin_and_size(clip_rect.min, content_size);
+ let external_scroll_offset = yaml["external-scroll-offset"].as_vector().unwrap_or_else(LayoutVector2D::zero);
+ let scroll_generation = yaml["scroll-generation"].as_i64().map_or(APZScrollGeneration::default(), |v| v as u64);
+ let has_scroll_linked_effect =
+ yaml["has-scroll-linked-effect"].as_bool().map_or(HasScrollLinkedEffect::default(),
+ |v| if v { HasScrollLinkedEffect::Yes } else { HasScrollLinkedEffect::No }
+ );
+
+ let numeric_id = yaml["id"].as_i64().map(|id| id as u64);
+
+ let external_id = ExternalScrollId(self.next_external_scroll_id, dl.pipeline_id);
+ self.next_external_scroll_id += 1;
+
+ if let Some(vector) = yaml["scroll-offset"].as_vector() {
+ self.scroll_offsets.insert(
+ external_id,
+ vec![SampledScrollOffset {
+ offset: vector,
+ generation: APZScrollGeneration::default(),
+ }],
+ );
+ }
+
+ if !yaml["scroll-offsets"].is_badvalue() {
+ let mut offsets = Vec::new();
+ for entry in yaml["scroll-offsets"].as_vec().unwrap() {
+ let offset = entry["offset"].as_vector().unwrap_or(LayoutVector2D::zero());
+ let generation = entry["generation"].as_i64().map_or(APZScrollGeneration::default(), |v| v as u64);
+ offsets.push(SampledScrollOffset { offset, generation });
+ }
+ self.scroll_offsets.insert(external_id, offsets);
+ }
+
+ let clip_to_frame = yaml["clip-to-frame"].as_bool().unwrap_or(false);
+
+ let clip_id = if clip_to_frame {
+ Some(dl.define_clip_rect(
+ self.top_space(),
+ clip_rect,
+ ))
+ } else {
+ None
+ };
+
+ let spatial_id = dl.define_scroll_frame(
+ self.top_space(),
+ external_id,
+ content_rect,
+ clip_rect,
+ external_scroll_offset,
+ scroll_generation,
+ has_scroll_linked_effect,
+ self.next_spatial_key(),
+ );
+ if let Some(numeric_id) = numeric_id {
+ self.add_spatial_id_mapping(numeric_id, spatial_id);
+ if let Some(clip_id) = clip_id {
+ self.add_clip_id_mapping(numeric_id, clip_id);
+ }
+ }
+
+ if let Some(yaml_items) = yaml["items"].as_vec() {
+ self.spatial_id_stack.push(spatial_id);
+ self.add_display_list_items_from_yaml(dl, wrench, yaml_items);
+ self.spatial_id_stack.pop().unwrap();
+ }
+ }
+
+ fn handle_sticky_frame(
+ &mut self,
+ dl: &mut DisplayListBuilder,
+ wrench: &mut Wrench,
+ yaml: &Yaml,
+ ) {
+ let bounds = yaml["bounds"].as_rect().expect("sticky frame must have a bounds");
+ let numeric_id = yaml["id"].as_i64().map(|id| id as u64);
+
+ let real_id = dl.define_sticky_frame(
+ *self.spatial_id_stack.last().unwrap(),
+ bounds,
+ SideOffsets2D::new(
+ yaml["margin-top"].as_f32(),
+ yaml["margin-right"].as_f32(),
+ yaml["margin-bottom"].as_f32(),
+ yaml["margin-left"].as_f32(),
+ ),
+ yaml["vertical-offset-bounds"].as_sticky_offset_bounds(),
+ yaml["horizontal-offset-bounds"].as_sticky_offset_bounds(),
+ yaml["previously-applied-offset"].as_vector().unwrap_or_else(LayoutVector2D::zero),
+ self.next_spatial_key(),
+ );
+
+ if let Some(numeric_id) = numeric_id {
+ self.add_spatial_id_mapping(numeric_id, real_id);
+ }
+
+ if let Some(yaml_items) = yaml["items"].as_vec() {
+ self.spatial_id_stack.push(real_id);
+ self.add_display_list_items_from_yaml(dl, wrench, yaml_items);
+ self.spatial_id_stack.pop().unwrap();
+ }
+ }
+
+ fn resolve_binding<'a>(
+ &'a self,
+ yaml: &'a Yaml,
+ ) -> &'a Yaml {
+ if let Some(keyframes) = &self.keyframes {
+ if let Some(s) = yaml.as_str() {
+ const PREFIX: &str = "key(";
+ const SUFFIX: &str = ")";
+ if let Some(key) = s.strip_prefix(PREFIX).and_then(|s| s.strip_suffix(SUFFIX)) {
+ return &keyframes[key][self.requested_frame];
+ }
+ }
+ }
+
+ yaml
+ }
+
+ fn resolve_colorf(
+ &self,
+ yaml: &Yaml,
+ ) -> Option<ColorF> {
+ self.resolve_binding(yaml)
+ .as_colorf()
+ }
+
+ fn resolve_rect(
+ &self,
+ yaml: &Yaml,
+ ) -> LayoutRect {
+ self.resolve_binding(yaml)
+ .as_rect()
+ .unwrap_or_else(|| panic!("invalid rect {:?}", yaml))
+ }
+
+ fn resolve_vector(
+ &self,
+ yaml: &Yaml,
+ default: LayoutVector2D,
+ ) -> LayoutVector2D {
+ self.resolve_binding(yaml)
+ .as_vector()
+ .unwrap_or(default)
+ }
+
+ fn handle_push_shadow(
+ &mut self,
+ dl: &mut DisplayListBuilder,
+ yaml: &Yaml,
+ info: &mut CommonItemProperties,
+ ) {
+ let blur_radius = yaml["blur-radius"].as_f32().unwrap_or(0.0);
+ let offset = yaml["offset"].as_vector().unwrap_or_else(LayoutVector2D::zero);
+ let color = yaml["color"].as_colorf().unwrap_or(ColorF::BLACK);
+
+ dl.push_shadow(
+ &SpaceAndClipInfo { spatial_id: info.spatial_id, clip_chain_id: info.clip_chain_id },
+ Shadow {
+ blur_radius,
+ offset,
+ color,
+ },
+ true,
+ );
+ }
+
+ fn handle_pop_all_shadows(&mut self, dl: &mut DisplayListBuilder) {
+ dl.pop_all_shadows();
+ }
+
+ fn handle_clip_chain(&mut self, builder: &mut DisplayListBuilder, yaml: &Yaml) {
+ let numeric_id = yaml["id"].as_i64().expect("clip chains must have an id");
+ let clip_ids: Vec<ClipId> = yaml["clips"]
+ .as_vec_u64()
+ .unwrap_or_default()
+ .iter()
+ .map(|id| self.user_clip_id_map[id])
+ .collect();
+
+ let parent = self.to_clip_chain_id(&yaml["parent"], builder);
+ let real_id = builder.define_clip_chain(parent, clip_ids);
+ self.add_clip_chain_id_mapping(numeric_id as u64, real_id);
+ }
+
+ fn handle_clip(&mut self, dl: &mut DisplayListBuilder, wrench: &mut Wrench, yaml: &Yaml) {
+ let numeric_id = yaml["id"].as_i64();
+ let spatial_id = self.top_space();
+ let complex_clips = yaml["complex"].as_complex_clip_regions();
+ let mut clip_id = None;
+
+ if let Some(clip_rect) = yaml["bounds"].as_rect() {
+ clip_id = Some(dl.define_clip_rect(
+ spatial_id,
+ clip_rect,
+ ));
+ }
+
+ if let Some(image_mask) = self.as_image_mask(&yaml["image-mask"], wrench) {
+ assert!(clip_id.is_none(), "invalid clip definition");
+
+ clip_id = Some(dl.define_clip_image_mask(
+ spatial_id,
+ image_mask,
+ &[],
+ FillRule::Nonzero,
+ ));
+ }
+
+ if !complex_clips.is_empty() {
+ // Only 1 complex clip is supported per clip (todo: change yaml format)
+ assert_eq!(complex_clips.len(), 1);
+ assert!(clip_id.is_none(), "invalid clip definition");
+
+ clip_id = Some(dl.define_clip_rounded_rect(
+ spatial_id,
+ complex_clips[0],
+ ));
+ }
+
+ if let Some(clip_id) = clip_id {
+ if let Some(numeric_id) = numeric_id {
+ self.add_clip_id_mapping(numeric_id as u64, clip_id);
+ }
+ }
+ }
+
+ fn push_reference_frame(
+ &mut self,
+ dl: &mut DisplayListBuilder,
+ default_bounds: impl Fn() -> LayoutRect,
+ yaml: &Yaml,
+ ) -> SpatialId {
+ let bounds = yaml["bounds"].as_rect().unwrap_or_else(default_bounds);
+ let default_transform_origin = LayoutPoint::new(
+ bounds.min.x + bounds.width() * 0.5,
+ bounds.min.y + bounds.height() * 0.5,
+ );
+
+ let transform_style = yaml["transform-style"]
+ .as_transform_style()
+ .unwrap_or(TransformStyle::Flat);
+
+ let transform_origin = yaml["transform-origin"]
+ .as_point()
+ .unwrap_or(default_transform_origin);
+
+ assert!(
+ yaml["transform"].is_badvalue() ||
+ yaml["perspective"].is_badvalue(),
+ "Should have one of either transform or perspective"
+ );
+
+ let perspective_origin = yaml["perspective-origin"]
+ .as_point()
+ .unwrap_or(default_transform_origin);
+
+ let reference_frame_kind = if !yaml["perspective"].is_badvalue() {
+ ReferenceFrameKind::Perspective { scrolling_relative_to: None }
+ } else {
+ ReferenceFrameKind::Transform {
+ is_2d_scale_translation: false,
+ should_snap: false,
+ paired_with_perspective: yaml["paired-with-perspective"].as_bool().unwrap_or(false),
+ }
+ };
+
+ let transform = yaml["transform"]
+ .as_transform(&transform_origin);
+
+ let perspective = match yaml["perspective"].as_f32() {
+ Some(value) if value != 0.0 => {
+ Some(make_perspective(perspective_origin, value as f32))
+ }
+ Some(..) => None,
+ _ => yaml["perspective"].as_matrix4d(),
+ };
+
+ let reference_frame_id = dl.push_reference_frame(
+ bounds.min,
+ *self.spatial_id_stack.last().unwrap(),
+ transform_style,
+ transform.or(perspective).unwrap_or_default().into(),
+ reference_frame_kind,
+ self.next_spatial_key(),
+ );
+
+ let numeric_id = yaml["id"].as_i64();
+ if let Some(numeric_id) = numeric_id {
+ self.add_spatial_id_mapping(numeric_id as u64, reference_frame_id);
+ }
+
+ reference_frame_id
+ }
+
+ fn handle_reference_frame(
+ &mut self,
+ dl: &mut DisplayListBuilder,
+ wrench: &mut Wrench,
+ yaml: &Yaml,
+ ) {
+ let default_bounds = || LayoutRect::from_size(wrench.window_size_f32());
+ let real_id = self.push_reference_frame(dl, default_bounds, yaml);
+ self.spatial_id_stack.push(real_id);
+
+ if let Some(yaml_items) = yaml["items"].as_vec() {
+ self.add_display_list_items_from_yaml(dl, wrench, yaml_items);
+ }
+
+ self.spatial_id_stack.pop().unwrap();
+ dl.pop_reference_frame();
+ }
+
+ fn push_computed_frame(
+ &mut self,
+ dl: &mut DisplayListBuilder,
+ default_bounds: impl Fn() -> LayoutRect,
+ yaml: &Yaml,
+ ) -> SpatialId {
+ let bounds = yaml["bounds"].as_rect().unwrap_or_else(default_bounds);
+
+ let scale_from = yaml["scale-from"].as_size();
+ let vertical_flip = yaml["vertical-flip"].as_bool().unwrap_or(false);
+ let rotation = yaml["rotation"].as_rotation().unwrap_or(Rotation::Degree0);
+
+ let reference_frame_id = dl.push_computed_frame(
+ bounds.min,
+ *self.spatial_id_stack.last().unwrap(),
+ scale_from,
+ vertical_flip,
+ rotation,
+ self.next_spatial_key(),
+ );
+
+ let numeric_id = yaml["id"].as_i64();
+ if let Some(numeric_id) = numeric_id {
+ self.add_spatial_id_mapping(numeric_id as u64, reference_frame_id);
+ }
+
+ reference_frame_id
+ }
+
+ fn handle_computed_frame(
+ &mut self,
+ dl: &mut DisplayListBuilder,
+ wrench: &mut Wrench,
+ yaml: &Yaml,
+ ) {
+ let default_bounds = || LayoutRect::from_size(wrench.window_size_f32());
+ let real_id = self.push_computed_frame(dl, default_bounds, yaml);
+ self.spatial_id_stack.push(real_id);
+
+ if let Some(yaml_items) = yaml["items"].as_vec() {
+ self.add_display_list_items_from_yaml(dl, wrench, yaml_items);
+ }
+
+ self.spatial_id_stack.pop().unwrap();
+ dl.pop_reference_frame();
+ }
+
+ fn add_stacking_context_from_yaml(
+ &mut self,
+ dl: &mut DisplayListBuilder,
+ wrench: &mut Wrench,
+ yaml: &Yaml,
+ IsRoot(is_root): IsRoot,
+ info: &mut CommonItemProperties,
+ ) {
+ let default_bounds = || LayoutRect::from_size(wrench.window_size_f32());
+ let mut bounds = yaml["bounds"].as_rect().unwrap_or_else(default_bounds);
+
+ let pushed_reference_frame =
+ if !yaml["transform"].is_badvalue() || !yaml["perspective"].is_badvalue() {
+ let reference_frame_id = self.push_reference_frame(dl, default_bounds, yaml);
+ self.spatial_id_stack.push(reference_frame_id);
+ bounds.max -= bounds.min.to_vector();
+ bounds.min = LayoutPoint::zero();
+ true
+ } else {
+ false
+ };
+
+ let clip_chain_id = self.to_clip_chain_id(&yaml["clip-chain"], dl);
+
+ let transform_style = yaml["transform-style"]
+ .as_transform_style()
+ .unwrap_or(TransformStyle::Flat);
+ let mix_blend_mode = yaml["mix-blend-mode"]
+ .as_mix_blend_mode()
+ .unwrap_or(MixBlendMode::Normal);
+ let raster_space = yaml["raster-space"]
+ .as_raster_space()
+ .unwrap_or(RasterSpace::Screen);
+ let is_blend_container = yaml["blend-container"].as_bool().unwrap_or(false);
+ let wraps_backdrop_filter = yaml["wraps-backdrop-filter"].as_bool().unwrap_or(false);
+
+ if is_root {
+ if let Some(vector) = yaml["scroll-offset"].as_vector() {
+ let external_id = ExternalScrollId(0, dl.pipeline_id);
+ self.scroll_offsets.insert(
+ external_id,
+ vec![SampledScrollOffset {
+ offset: vector,
+ generation: APZScrollGeneration::default(),
+ }],
+ );
+ }
+ }
+
+ let filters = yaml["filters"].as_vec_filter_op().unwrap_or_default();
+ let filter_datas = yaml["filter-datas"].as_vec_filter_data().unwrap_or_default();
+ let filter_primitives = yaml["filter-primitives"].as_vec_filter_primitive().unwrap_or_default();
+
+ let mut flags = StackingContextFlags::empty();
+ flags.set(StackingContextFlags::IS_BLEND_CONTAINER, is_blend_container);
+ flags.set(StackingContextFlags::WRAPS_BACKDROP_FILTER, wraps_backdrop_filter);
+
+ dl.push_stacking_context(
+ bounds.min,
+ *self.spatial_id_stack.last().unwrap(),
+ info.flags,
+ clip_chain_id,
+ transform_style,
+ mix_blend_mode,
+ &filters,
+ &filter_datas,
+ &filter_primitives,
+ raster_space,
+ flags,
+ );
+
+ if let Some(yaml_items) = yaml["items"].as_vec() {
+ self.add_display_list_items_from_yaml(dl, wrench, yaml_items);
+ }
+
+ dl.pop_stacking_context();
+
+ if pushed_reference_frame {
+ self.spatial_id_stack.pop().unwrap();
+ dl.pop_reference_frame();
+ }
+ }
+
+ fn handle_backdrop_filter(
+ &mut self,
+ dl: &mut DisplayListBuilder,
+ item: &Yaml,
+ info: &mut CommonItemProperties,
+ ) {
+ info.clip_rect = try_intersect!(
+ self.resolve_rect(&item["bounds"]),
+ &info.clip_rect
+ );
+
+ let filters = item["filters"].as_vec_filter_op().unwrap_or_default();
+ let filter_datas = item["filter-datas"].as_vec_filter_data().unwrap_or_default();
+ let filter_primitives = item["filter-primitives"].as_vec_filter_primitive().unwrap_or_default();
+
+ dl.push_backdrop_filter(
+ info,
+ &filters,
+ &filter_datas,
+ &filter_primitives,
+ );
+ }
+}
+
+impl WrenchThing for YamlFrameReader {
+ fn do_frame(&mut self, wrench: &mut Wrench) -> u32 {
+ let mut should_build_yaml = false;
+
+ // If YAML isn't read yet, or watching source file, reload from disk.
+ if self.yaml_string.is_empty() || self.watch_source {
+ self.yaml_string = std::fs::read_to_string(&self.yaml_path)
+ .unwrap_or_else(|_| panic!("YAML '{:?}' doesn't exist", self.yaml_path));
+ should_build_yaml = true;
+ }
+
+ // Evaluate conditions that require parsing the YAML.
+ if self.built_frame != self.requested_frame {
+ // Requested frame has changed
+ should_build_yaml = true;
+ }
+
+ // Build the DL from YAML if required
+ if should_build_yaml {
+ self.build(wrench);
+ }
+
+ // Determine whether to send a new DL, or just refresh.
+ if should_build_yaml || wrench.should_rebuild_display_lists() {
+ wrench.begin_frame();
+ wrench.send_lists(
+ self.frame_count,
+ self.display_lists.clone(),
+ &self.scroll_offsets,
+ );
+ } else {
+ wrench.refresh();
+ }
+
+ self.frame_count += 1;
+ self.frame_count
+ }
+
+ fn next_frame(&mut self) {
+ let mut max_frame_count = 0;
+ if let Some(ref keyframes) = self.keyframes {
+ for (_, values) in keyframes.as_hash().unwrap() {
+ max_frame_count = max_frame_count.max(values.as_vec().unwrap().len());
+ }
+ }
+ if self.requested_frame + 1 < max_frame_count {
+ self.requested_frame += 1;
+ }
+ }
+
+ fn prev_frame(&mut self) {
+ if self.requested_frame > 0 {
+ self.requested_frame -= 1;
+ }
+ }
+}