| // Copyright 2020 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| use { |
| anyhow::Error, |
| argh::FromArgs, |
| carnelian::{ |
| color::Color, |
| drawing::{load_font, FontFace, GlyphMap, Text}, |
| geometry::Corners, |
| input::{self}, |
| input_ext::TouchEventResampler, |
| make_app_assistant, |
| render::*, |
| App, AppAssistant, Point, RenderOptions, Size, ViewAssistant, ViewAssistantContext, |
| ViewAssistantPtr, ViewKey, |
| }, |
| euclid::{ |
| default::{Rect, Vector2D}, |
| point2, size2, vec2, |
| }, |
| fuchsia_trace::{self, counter, duration}, |
| fuchsia_trace_provider, |
| fuchsia_zircon::{self as zx, AsHandleRef, Event, Signals, Time}, |
| lazy_static::lazy_static, |
| lipsum::{lipsum_title, lipsum_words}, |
| rand::{thread_rng, Rng}, |
| std::{ |
| collections::{BTreeMap, VecDeque}, |
| f32, |
| path::PathBuf, |
| }, |
| }; |
| |
| const BACKGROUND_COLOR: Color = Color { r: 255, g: 255, b: 255, a: 255 }; |
| |
| // Clamp scroll offset to this range for sanity. |
| const SCROLL_OFFSET_RANGE: [u32; 2] = [0, 1000000]; |
| |
| // Delay before starting to simulate scrolling. |
| const FAKE_SCROLL_DELAY_SECONDS: i64 = 10; |
| |
| // Available scrolling methods. |
| #[derive(Copy, Clone, Debug)] |
| enum ScrollMethod { |
| Redraw, |
| CopyRedraw, |
| SlidingOffset, |
| MotionBlur, |
| } |
| |
| impl std::str::FromStr for ScrollMethod { |
| type Err = String; |
| |
| fn from_str(s: &str) -> Result<Self, Self::Err> { |
| match s { |
| "Redraw" => Ok(Self::Redraw), |
| "CopyRedraw" => Ok(Self::CopyRedraw), |
| "SlidingOffset" => Ok(Self::SlidingOffset), |
| "MotionBlur" => Ok(Self::MotionBlur), |
| _ => Err(format!("'{}' is not a valid value for ScrollMethod", s)), |
| } |
| } |
| } |
| |
| lazy_static! { |
| pub static ref FONT_FACE: FontFace = |
| load_font(PathBuf::from("/pkg/data/fonts/RobotoSlab-Regular.ttf")) |
| .expect("Failed to create font"); |
| } |
| |
| /// Infinite scroll. |
| #[derive(Clone, Debug, FromArgs)] |
| #[argh(name = "infinite_scroll_rs")] |
| struct Args { |
| /// use spinel (GPU rendering back-end) |
| #[argh(switch, short = 's')] |
| use_spinel: bool, |
| |
| /// contents scale (default scale is 1.0) |
| #[argh(option, default = "1.0")] |
| scale: f32, |
| |
| /// scrolling method (Redraw|CopyRedraw|SlidingOffset|MotionBlur) |
| #[argh(option, default = "ScrollMethod::Redraw")] |
| scroll_method: ScrollMethod, |
| |
| /// motion blur exposure time |
| #[argh(option, default = "1.0")] |
| exposure: f32, |
| |
| /// touch sampling offset (relative to presentation time) |
| #[argh(option, default = "38")] |
| touch_sampling_offset_ms: i64, |
| |
| /// maximum number of columns |
| #[argh(option, default = "std::u32::MAX")] |
| max_columns: u32, |
| |
| /// disable text (improves rendering performance) |
| #[argh(switch)] |
| disable_text: bool, |
| } |
| |
| #[derive(Default)] |
| struct InfiniteScrollAppAssistant { |
| args: Option<Args>, |
| } |
| |
| impl AppAssistant for InfiniteScrollAppAssistant { |
| fn setup(&mut self) -> Result<(), Error> { |
| let args: Args = argh::from_env(); |
| println!("back-end: {}", if args.use_spinel { "spinel" } else { "mold" }); |
| println!("scale: {}", args.scale); |
| println!("scroll method: {:?}", args.scroll_method); |
| match args.scroll_method { |
| ScrollMethod::MotionBlur => { |
| println!("exposure: {}", args.exposure); |
| } |
| _ => {} |
| } |
| println!("touch sampling offset: {} ms", args.touch_sampling_offset_ms); |
| self.args = Some(args); |
| Ok(()) |
| } |
| |
| fn create_view_assistant(&mut self, _: ViewKey) -> Result<ViewAssistantPtr, Error> { |
| Ok(Box::new(InfiniteScrollViewAssistant::new( |
| self.args.clone().expect("AppAssistant::setup not run"), |
| ))) |
| } |
| |
| fn get_render_options(&self) -> RenderOptions { |
| RenderOptions { |
| use_spinel: self.args.as_ref().map(|args| args.use_spinel).unwrap_or_default(), |
| } |
| } |
| } |
| |
| struct Box2D { |
| min: Point, |
| max: Point, |
| } |
| |
| impl Box2D { |
| fn new() -> Self { |
| Box2D { |
| min: point2(std::f32::MAX, std::f32::MAX), |
| max: point2(std::f32::MIN, std::f32::MIN), |
| } |
| } |
| |
| fn union(&self, point: &Point) -> Self { |
| Box2D { |
| min: point2(self.min.x.min(point.x), self.min.y.min(point.y)), |
| max: point2(self.max.x.max(point.x), self.max.y.max(point.y)), |
| } |
| } |
| |
| fn to_rect(&self) -> Rect<f32> { |
| Rect { origin: self.min, size: (self.max - self.min).to_size() } |
| } |
| } |
| |
| fn lerp(t: f32, p0: Point, p1: Point) -> Point { |
| point2(p0.x * (1.0 - t) + p1.x * t, p0.y * (1.0 - t) + p1.y * t) |
| } |
| |
| fn cubic( |
| path_builder: &mut PathBuilder, |
| p0: Point, |
| p1: Point, |
| p2: Point, |
| p3: Point, |
| bounding_box: &mut Box2D, |
| ) { |
| duration!("gfx", "cubic"); |
| |
| path_builder.move_to(p0); |
| path_builder.cubic_to(p1, p2, p3); |
| |
| // Sub-divide curve to determine bounding box. |
| |
| let deviation_x = (p0.x + p2.x - 3.0 * (p1.x + p2.x)).abs(); |
| let deviation_y = (p0.y + p2.y - 3.0 * (p1.y + p2.y)).abs(); |
| let deviation_squared = deviation_x * deviation_x + deviation_y * deviation_y; |
| |
| const PIXEL_ACCURACY: f32 = 0.25; |
| if deviation_squared < PIXEL_ACCURACY { |
| *bounding_box = bounding_box.union(&p0).union(&p3); |
| return; |
| } |
| |
| const TOLERANCE: f32 = 3.0; |
| let subdivisions = 1 + (TOLERANCE * deviation_squared).sqrt().sqrt().floor() as usize; |
| let increment = (subdivisions as f32).recip(); |
| let mut t = 0.0; |
| |
| *bounding_box = bounding_box.union(&p0); |
| for _ in 0..subdivisions - 1 { |
| t += increment; |
| let p_next = lerp( |
| t, |
| lerp(t, lerp(t, p0, p1), lerp(t, p1, p2)), |
| lerp(t, lerp(t, p1, p2), lerp(t, p2, p3)), |
| ); |
| *bounding_box = bounding_box.union(&p_next); |
| } |
| *bounding_box = bounding_box.union(&p3); |
| } |
| |
| struct Flower { |
| raster: Raster, |
| bounding_box: Rect<f32>, |
| } |
| |
| impl Flower { |
| fn new(context: &mut Context, petal_count: usize, r1: f32, r2: f32) -> Self { |
| duration!("gfx", "Flower::new"); |
| |
| let mut bounding_box = Box2D::new(); |
| let mut path_builder = context.path_builder().unwrap(); |
| let mut rng = thread_rng(); |
| let u: f32 = rng.gen_range(10.0, 100.0) / 100.0; |
| let v: f32 = rng.gen_range(0.0, 90.0) / 100.0; |
| let dt: f32 = f32::consts::PI / (petal_count as f32); |
| let mut t: f32 = 0.0; |
| |
| let mut p0 = point2(t.cos() * r1, t.sin() * r1); |
| for _ in 0..petal_count { |
| let x1 = t.cos() * r1; |
| let y1 = t.sin() * r1; |
| let x2 = (t + dt).cos() * r2; |
| let y2 = (t + dt).sin() * r2; |
| let x3 = (t + 2.0 * dt).cos() * r1; |
| let y3 = (t + 2.0 * dt).sin() * r1; |
| |
| let p1 = point2(x1 - y1 * u, y1 + x1 * u); |
| let p2 = point2(x2 + y2 * v, y2 - x2 * v); |
| let p3 = point2(x2, y2); |
| let p4 = point2(x2 - y2 * v, y2 + x2 * v); |
| let p5 = point2(x3 + y3 * u, y3 - x3 * u); |
| let p6 = point2(x3, y3); |
| |
| cubic(&mut path_builder, p0, p1, p2, p3, &mut bounding_box); |
| cubic(&mut path_builder, p3, p4, p5, p6, &mut bounding_box); |
| |
| p0 = p6; |
| t += dt * 2.0; |
| } |
| |
| let mut raster_builder = context.raster_builder().unwrap(); |
| raster_builder.add(&path_builder.build(), None); |
| |
| Self { raster: raster_builder.build(), bounding_box: bounding_box.to_rect() } |
| } |
| } |
| |
| fn random_color_element() -> u8 { |
| let mut rng = thread_rng(); |
| let c: u8 = rng.gen_range(0, 255); |
| c |
| } |
| |
| fn random_color() -> Color { |
| Color { |
| r: random_color_element(), |
| g: random_color_element(), |
| b: random_color_element(), |
| a: 255, |
| } |
| } |
| |
| const ITEM_TYPE_TITLE: i32 = 0; |
| const ITEM_TYPE_BODY_MIN: i32 = 1; |
| const ITEM_TYPE_BODY_MAX: i32 = 3; |
| const ITEM_TYPE_FLOWER: i32 = 4; |
| const ITEM_TYPE_COUNT: i32 = 5; |
| |
| struct Item { |
| raster: Raster, |
| bounding_box: Rect<f32>, |
| bounding_box_raster: Raster, |
| color: Color, |
| id: i32, |
| } |
| |
| struct Scene { |
| scale: f32, |
| exposure: f32, |
| max_columns: u32, |
| text_disabled: bool, |
| scroll_offset_y: u32, |
| last_scroll_offset_y: u32, |
| title_glyphs: GlyphMap, |
| body_glyphs: GlyphMap, |
| columns: Vec<VecDeque<Item>>, |
| total_items: usize, |
| } |
| |
| impl Scene { |
| fn new(scale: f32, exposure: f32, max_columns: u32, text_disabled: bool) -> Self { |
| // Equal amount of coordinate space above and below. |
| let scroll_offset_y = (SCROLL_OFFSET_RANGE[0] + SCROLL_OFFSET_RANGE[1]) / 2; |
| |
| Self { |
| scale, |
| exposure, |
| max_columns, |
| text_disabled, |
| scroll_offset_y, |
| last_scroll_offset_y: scroll_offset_y, |
| title_glyphs: GlyphMap::new(), |
| body_glyphs: GlyphMap::new(), |
| columns: Vec::new(), |
| total_items: 0, |
| } |
| } |
| |
| fn new_item( |
| context: &mut Context, |
| offset: Vector2D<i32>, |
| align_bottom: bool, |
| scale: f32, |
| title_glyphs: &mut GlyphMap, |
| body_glyphs: &mut GlyphMap, |
| id: i32, |
| column_width: f32, |
| text_disabled: bool, |
| ) -> Item { |
| let item_type = |
| if text_disabled { ITEM_TYPE_FLOWER } else { id.rem_euclid(ITEM_TYPE_COUNT) }; |
| // Alternate between title, body, and flower shape. |
| let (raster, bounding_box, x, color) = match item_type { |
| ITEM_TYPE_TITLE => { |
| const TITLE_SIZE: f32 = 32.0; |
| |
| let size = TITLE_SIZE * scale; |
| let wrap = column_width; |
| let title = lipsum_title(); |
| let text = Text::new(context, &title, size, wrap, &FONT_FACE, title_glyphs); |
| |
| (text.raster, text.bounding_box.round_out(), 0, Color { r: 0, g: 0, b: 0, a: 255 }) |
| } |
| ITEM_TYPE_BODY_MIN..=ITEM_TYPE_BODY_MAX => { |
| const BODY_SIZE: f32 = 20.0; |
| const BODY_MIN_WORDS: usize = 25; |
| const BODY_MAX_WORDS: usize = 100; |
| |
| let size = BODY_SIZE * scale; |
| let wrap = column_width; |
| let mut rng = thread_rng(); |
| let body = lipsum_words(rng.gen_range(BODY_MIN_WORDS, BODY_MAX_WORDS)); |
| let text = Text::new(context, &body, size, wrap, &FONT_FACE, body_glyphs); |
| |
| (text.raster, text.bounding_box.round_out(), 0, Color { r: 0, g: 0, b: 0, a: 255 }) |
| } |
| ITEM_TYPE_FLOWER => { |
| const FLOWER_MIN_PETALS: usize = 3; |
| const FLOWER_MAX_PETALS: usize = 8; |
| const FLOWER_MIN_R1: f32 = 60.0; |
| const FLOWER_MAX_R1: f32 = 95.0; |
| const FLOWER_MIN_R2: f32 = 20.0; |
| const FLOWER_MAX_R2: f32 = 60.0; |
| |
| let mut rng = thread_rng(); |
| let petal_count: usize = rng.gen_range(FLOWER_MIN_PETALS, FLOWER_MAX_PETALS); |
| let r1: f32 = rng.gen_range(FLOWER_MIN_R1, FLOWER_MAX_R1) * scale; |
| let r2: f32 = rng.gen_range(FLOWER_MIN_R2, FLOWER_MAX_R2) * scale; |
| let flower = Flower::new(context, petal_count, r1, r2); |
| let x = column_width as i32 / 2; |
| let bounding_box = flower.bounding_box.round_out(); |
| |
| (flower.raster, bounding_box.round_out(), x, random_color()) |
| } |
| _ => unreachable!(), |
| }; |
| |
| // Create a bounding box raster that can be used for efficient clearing. |
| let mut path_builder = context.path_builder().unwrap(); |
| path_builder.move_to(bounding_box.origin); |
| path_builder.line_to(bounding_box.top_right()); |
| path_builder.line_to(bounding_box.bottom_right()); |
| path_builder.line_to(bounding_box.bottom_left()); |
| path_builder.line_to(bounding_box.origin); |
| let mut raster_builder = context.raster_builder().unwrap(); |
| raster_builder.add(&path_builder.build(), None); |
| let bounding_box_raster = raster_builder.build(); |
| |
| let y = if align_bottom { -bounding_box.size.height } else { 0.0 }; |
| let offset = offset + vec2(x, (y - bounding_box.min_y()) as i32); |
| |
| Item { |
| raster: raster.translate(offset), |
| bounding_box: bounding_box.translate(offset.to_f32()), |
| bounding_box_raster: bounding_box_raster.translate(offset), |
| color, |
| id, |
| } |
| } |
| |
| fn update(&mut self, context: &mut Context, size: &Size, scroll_delta: i32) { |
| counter!("gfx", "scroll_dy", 0, "dy" => scroll_delta.abs()); |
| |
| let new_offset = self.scroll_offset_y as i32 + scroll_delta; |
| self.last_scroll_offset_y = self.scroll_offset_y; |
| self.scroll_offset_y = |
| new_offset.max(SCROLL_OFFSET_RANGE[0] as i32).min(SCROLL_OFFSET_RANGE[1] as i32) as u32; |
| let top = self.scroll_offset_y as f32; |
| |
| const COLUMN_SIZE: f32 = 400.0; |
| const PADDING: f32 = 50.0; |
| const MIN_MARGIN: f32 = 50.0; |
| const DISCARD_EXTRA_DISTANCE: f32 = 2048.0; |
| |
| let column_size = (COLUMN_SIZE * self.scale) as usize; |
| let padding = PADDING * self.scale; |
| let min_margin = (MIN_MARGIN * self.scale) as usize; |
| let width = size.width as usize; |
| let column_count = ((width - min_margin * 2) / (column_size + min_margin)) |
| .min(self.max_columns as usize) |
| .max(1); |
| let margin = (width - column_count * column_size) / (column_count + 1); |
| |
| if column_count != self.columns.len() { |
| self.columns = (0..column_count).map(|_| VecDeque::new()).collect(); |
| } |
| |
| // Skip random number of body items. |
| fn get_id_step(id: i32, base: i32) -> i32 { |
| if id.rem_euclid(ITEM_TYPE_COUNT) == base { |
| let mut rng = thread_rng(); |
| rng.gen_range(1, (ITEM_TYPE_BODY_MAX - ITEM_TYPE_BODY_MIN) + 1) |
| } else { |
| 1 |
| } |
| } |
| |
| // Discard distance needs to be at least height for clearing. Extra distance |
| // avoids unnecessary work by keeping items around that are likely to become |
| // visible again. This improves performance at the cost of increased memory |
| // usage. |
| let discard_distance = size.height + DISCARD_EXTRA_DISTANCE; |
| |
| self.total_items = 0; |
| for (i, column) in self.columns.iter_mut().enumerate() { |
| let margin = margin + (column_size + margin) * i; |
| |
| // Remove items at the bottom of column that are no longer visible and |
| // outside discard distance. |
| while !column.is_empty() |
| && (column.back().unwrap().bounding_box.min_y() - top) |
| > (size.height + discard_distance) |
| { |
| column.pop_back(); |
| } |
| |
| // Remove items at the top of column that are no longer visible and |
| // outside discard distance. |
| while !column.is_empty() |
| && (column.front().unwrap().bounding_box.max_y() - top) < -discard_distance |
| { |
| column.pop_front(); |
| } |
| |
| // Add new items at the bottom of column to fill visible region. |
| while column.is_empty() |
| || (column.back().unwrap().bounding_box.max_y() - top + padding) < size.height |
| { |
| let (id, y) = if let Some(item) = column.back() { |
| (item.id + get_id_step(item.id, ITEM_TYPE_BODY_MIN), item.bounding_box.max_y()) |
| } else { |
| (0, top) |
| }; |
| column.push_back(Self::new_item( |
| context, |
| vec2(margin as i32, (y + padding) as i32), |
| false, |
| self.scale, |
| &mut self.title_glyphs, |
| &mut self.body_glyphs, |
| id, |
| column_size as f32, |
| self.text_disabled, |
| )); |
| } |
| |
| // Add new items at the top of column to fill visible region. |
| while column.is_empty() |
| || (column.front().unwrap().bounding_box.min_y() - top - padding) > 0.0 |
| { |
| let (id, y) = if let Some(item) = column.front() { |
| (item.id - get_id_step(item.id, ITEM_TYPE_BODY_MAX), item.bounding_box.min_y()) |
| } else { |
| (0, top + size.height as f32) |
| }; |
| column.push_front(Self::new_item( |
| context, |
| vec2(margin as i32, (y - padding) as i32), |
| true, |
| self.scale, |
| &mut self.title_glyphs, |
| &mut self.body_glyphs, |
| id, |
| column_size as f32, |
| self.text_disabled, |
| )); |
| } |
| |
| self.total_items += column.len(); |
| } |
| } |
| |
| fn prepend_clear_layers_to_composition( |
| &self, |
| composition: &mut Composition, |
| viewport: &Rect<f32>, |
| ty: i32, |
| ) { |
| let layers = self |
| .columns |
| .iter() |
| .flat_map(|column| column.iter()) |
| .filter(|item| viewport.intersects(&item.bounding_box)) |
| .map(|item| Layer { |
| raster: item.bounding_box_raster.clone().translate(vec2(0, ty)), |
| style: Style { |
| fill_rule: FillRule::WholeTile, |
| fill: Fill::Solid(BACKGROUND_COLOR), |
| blend_mode: BlendMode::Over, |
| }, |
| }); |
| |
| composition.replace(..0, layers); |
| } |
| |
| fn prepend_layers_to_composition( |
| &self, |
| composition: &mut Composition, |
| viewport: &Rect<f32>, |
| ty: i32, |
| ) { |
| let layers = self |
| .columns |
| .iter() |
| .flat_map(|column| column.iter()) |
| .filter(|item| viewport.intersects(&item.bounding_box)) |
| .map(|item| Layer { |
| raster: item.raster.clone().translate(vec2(0, ty)), |
| style: Style { |
| fill_rule: FillRule::NonZero, |
| fill: Fill::Solid(item.color), |
| blend_mode: BlendMode::Over, |
| }, |
| }); |
| |
| composition.replace(..0, layers); |
| } |
| } |
| |
| struct Contents { |
| image: Image, |
| scroll_method: ScrollMethod, |
| scroll_offset_y: u32, |
| size: Size, |
| composition: Composition, |
| } |
| |
| impl Contents { |
| fn new(image: Image, scroll_method: ScrollMethod) -> Self { |
| let composition = Composition::new(BACKGROUND_COLOR); |
| |
| Self { image, scroll_method, scroll_offset_y: 0, size: Size::zero(), composition } |
| } |
| |
| fn update( |
| &mut self, |
| context: &mut Context, |
| scene: &mut Scene, |
| size: &Size, |
| staging_image: &mut Option<Image>, |
| ) { |
| let width = size.width.floor() as u32; |
| let height = size.height.floor() as u32; |
| |
| let mut ext = if self.size != *size { |
| RenderExt { |
| pre_clear: Some(PreClear { color: BACKGROUND_COLOR }), |
| ..Default::default() |
| } |
| } else { |
| RenderExt::default() |
| }; |
| |
| match self.scroll_method { |
| // Method 1: Translate paths and redraw the whole scene each frame. |
| ScrollMethod::Redraw => { |
| self.composition.clear(); |
| |
| // Add clear layers for previous viewport. |
| let viewport = Rect::new( |
| point2(0.0, self.scroll_offset_y as f32), |
| size2(width as f32, height as f32), |
| ); |
| scene.prepend_clear_layers_to_composition( |
| &mut self.composition, |
| &viewport, |
| -(self.scroll_offset_y as i32), |
| ); |
| |
| // Add layers for current viewport. |
| let viewport = Rect::new( |
| point2(0.0, scene.scroll_offset_y as f32), |
| size2(width as f32, height as f32), |
| ); |
| scene.prepend_layers_to_composition( |
| &mut self.composition, |
| &viewport, |
| -(scene.scroll_offset_y as i32), |
| ); |
| |
| // Render scene. |
| context.render(&self.composition, None, self.image, &ext); |
| } |
| // Method 2: Copy the image area that is still visible and redraw exposed region. |
| ScrollMethod::CopyRedraw => { |
| let scroll_distance = scene.scroll_offset_y as f32 - self.scroll_offset_y as f32; |
| let scroll_amount = scroll_distance.abs() as u32; |
| // |scroll_height| is the area that can be copied and doesn't need to a redraw. |
| let scroll_height = if self.size == *size { |
| // Round down to multiple of 32. This ensures that we can use the render clip |
| // to redraw the exposed region. |
| (height - scroll_amount.min(height)) & !31 |
| } else { |
| 0 |
| }; |
| let mut clip = Rect::new(point2(0, 0), size2(width, height)); |
| // Determine area to copy and clip to render depending on scroll direction. |
| if scroll_height > 0 { |
| match scroll_distance { |
| // Area at top exposed. |
| x if x > 0.0 => { |
| clip.origin.y = scroll_height; |
| clip.size.height = height - scroll_height; |
| ext.pre_copy = Some(PreCopy { |
| image: self.image, |
| copy_region: CopyRegion { |
| src_offset: point2(0, scroll_amount), |
| dst_offset: point2(0, 0), |
| extent: size2(width, height - scroll_amount), |
| }, |
| }); |
| } |
| // Area at bottom exposed. |
| x if x < 0.0 => { |
| clip.origin.y = 0; |
| clip.size.height = height - scroll_height; |
| ext.pre_copy = Some(PreCopy { |
| image: self.image, |
| copy_region: CopyRegion { |
| src_offset: point2(0, 0), |
| dst_offset: point2(0, scroll_amount), |
| extent: size2(width, height - scroll_amount), |
| }, |
| }); |
| } |
| // No new contents exposed. |
| _ => { |
| clip.size.height = 0; |
| } |
| } |
| } |
| |
| self.composition.clear(); |
| |
| // Add clear layers for previous viewport of clip. |
| let viewport = Rect::new( |
| point2( |
| clip.origin.x as f32, |
| clip.origin.y as f32 + self.scroll_offset_y as f32, |
| ), |
| size2(clip.size.width as f32, clip.size.height as f32), |
| ); |
| scene.prepend_clear_layers_to_composition( |
| &mut self.composition, |
| &viewport, |
| -(self.scroll_offset_y as i32), |
| ); |
| |
| // Add layers for current viewport of clip. |
| let viewport = Rect::new( |
| point2( |
| clip.origin.x as f32, |
| clip.origin.y as f32 + scene.scroll_offset_y as f32, |
| ), |
| size2(clip.size.width as f32, clip.size.height as f32), |
| ); |
| scene.prepend_layers_to_composition( |
| &mut self.composition, |
| &viewport, |
| -(scene.scroll_offset_y as i32), |
| ); |
| |
| // Render clip. |
| context.render(&self.composition, Some(clip), self.image, &ext); |
| } |
| // Method 3: Allocate temporary buffer and use a sliding offset to minimize |
| // redraw. Contents of temporary buffer is copied to image after rendering. |
| // Method 4: Sliding offset method with motion blur effect. |
| ScrollMethod::SlidingOffset | ScrollMethod::MotionBlur => { |
| // Add 32 and round temporary buffer height up to multiple of 32 in order |
| // to be able to use the render clip. |
| let buffer_height = (height + 63) & !31; |
| let top = scene.scroll_offset_y; |
| let bottom = scene.scroll_offset_y + height; |
| let next_y0 = top % buffer_height; |
| let next_y1 = bottom % buffer_height; |
| let scroll_distance = |
| scene.scroll_offset_y as i32 - scene.last_scroll_offset_y as i32; |
| let scroll_amount = scroll_distance.abs(); |
| |
| // Damage is the full area by default. |
| let mut damage_y0 = next_y0; |
| let mut damage_y1 = next_y1; |
| let mut clear_offset = 0; |
| |
| // Determine smaller damage area based on scroll direction. |
| if self.size == *size && scroll_amount < height as i32 { |
| if scroll_distance >= 0 { |
| damage_y0 = (bottom - scroll_amount as u32) % buffer_height; |
| damage_y1 = bottom % buffer_height; |
| clear_offset = -(buffer_height as i32); |
| } else { |
| damage_y0 = top % buffer_height; |
| damage_y1 = (top + scroll_amount as u32) % buffer_height; |
| clear_offset = buffer_height as i32; |
| } |
| } |
| |
| // Convert upper bound to height instead of 0. |
| if damage_y1 == 0 { |
| damage_y1 = buffer_height; |
| } |
| |
| // Compute top/bottom spans. We end up with both a top and |
| // bottom span if damage wraps around. |
| let mut top_y0 = damage_y0; |
| let mut top_y1 = damage_y1; |
| let mut bottom_y0 = damage_y1; |
| let mut bottom_y1 = damage_y1; |
| if damage_y0 > damage_y1 { |
| top_y0 = 0; |
| top_y1 = damage_y1; |
| bottom_y0 = damage_y0; |
| bottom_y1 = buffer_height; |
| } |
| |
| // Round to multiple of 32. This ensures that we can use the |
| // render clip to redraw the exposed region. |
| top_y0 = top_y0 & !31; |
| top_y1 = (top_y1 + 31) & !31; |
| if bottom_y0 < bottom_y1 { |
| bottom_y0 = bottom_y0 & !31; |
| bottom_y1 = (bottom_y1 + 31) & !31; |
| } |
| |
| let image = staging_image |
| .take() |
| .unwrap_or_else(|| context.new_image(size2(width, buffer_height))); |
| |
| // Offset in staging image that translate to top of output. |
| let y_start = next_y0; |
| // Offsets that needs to be applied to contents in order to |
| // render at the correct location in staging image. |
| let mut dy = (top / buffer_height) * buffer_height; |
| |
| // Render bottom span if needed. |
| if bottom_y0 < bottom_y1 { |
| let mut composition = Composition::new(BACKGROUND_COLOR); |
| let clip = Rect::new(point2(0, bottom_y0), size2(width, bottom_y1 - bottom_y0)); |
| |
| // Add layers for previous viewport of bottom clip. |
| let viewport = Rect::new( |
| point2(0.0, (clip.origin.y + dy) as f32 + clear_offset as f32), |
| clip.size.to_f32(), |
| ); |
| scene.prepend_clear_layers_to_composition( |
| &mut composition, |
| &viewport, |
| -(dy as i32 + clear_offset as i32), |
| ); |
| |
| // Add layers for current viewport of bottom clip. |
| let viewport = |
| Rect::new(point2(0.0, (clip.origin.y + dy) as f32), clip.size.to_f32()); |
| scene.prepend_layers_to_composition(&mut composition, &viewport, -(dy as i32)); |
| |
| // Render bottom clip. |
| context.render(&composition, Some(clip), image, &ext); |
| |
| ext.pre_clear = None; |
| } |
| |
| // Offset contents of top span if it is below top of output. |
| if top_y1 <= y_start { |
| dy += buffer_height; |
| } |
| |
| // Determine exposure if motion blur mode is used. |
| let exposure = match self.scroll_method { |
| ScrollMethod::MotionBlur => { |
| (scroll_distance as f32 * scene.exposure).round() as i32 |
| } |
| _ => 0, |
| }; |
| |
| // Copy temporary buffer to output image and apply motion blur. |
| ext.post_copy = Some(PostCopy { |
| image: self.image, |
| color: BACKGROUND_COLOR, |
| exposure_distance: vec2(0, exposure), |
| copy_region: CopyRegion { |
| src_offset: point2(0, y_start), |
| dst_offset: point2(0, 0), |
| extent: size2(width, height), |
| }, |
| }); |
| |
| let mut composition = Composition::new(BACKGROUND_COLOR); |
| let clip = Rect::new(point2(0, top_y0), size2(width, top_y1 - top_y0)); |
| |
| // Add layers for previous viewport of top clip. |
| let viewport = Rect::new( |
| point2(0.0, (clip.origin.y + dy) as f32 + clear_offset as f32), |
| clip.size.to_f32(), |
| ); |
| scene.prepend_clear_layers_to_composition( |
| &mut composition, |
| &viewport, |
| -(dy as i32 + clear_offset as i32), |
| ); |
| |
| // Add layers for current viewport of top clip. |
| let viewport = |
| Rect::new(point2(0.0, (clip.origin.y + dy) as f32), clip.size.to_f32()); |
| scene.prepend_layers_to_composition(&mut composition, &viewport, -(dy as i32)); |
| |
| // Render top clip. |
| context.render(&composition, Some(clip), image, &ext); |
| |
| staging_image.replace(image); |
| } |
| } |
| |
| self.size = *size; |
| self.scroll_offset_y = scene.scroll_offset_y; |
| } |
| } |
| |
| struct FlingCurve { |
| curve_duration: f32, |
| start_timestamp: Time, |
| displacement_ratio: Vector2D<f32>, |
| cumulative_scroll: Vector2D<f32>, |
| previous_timestamp: Time, |
| time_offset: f32, |
| position_offset: f32, |
| } |
| |
| // Alpha-beta-gamma filter constants taken from the Chromium project. |
| // These constants come from UX experiments. |
| const ALPHA: f32 = -5.70762e+03; |
| const BETA: f32 = 1.72e+02; |
| const GAMMA: f32 = 3.7e+00; |
| |
| impl FlingCurve { |
| fn get_position_at_time(t: f32) -> f32 { |
| ALPHA * (-GAMMA * t).exp() - BETA * t - ALPHA |
| } |
| |
| fn get_velocity_at_time(t: f32) -> f32 { |
| -ALPHA * GAMMA * (-GAMMA * t).exp() - BETA |
| } |
| |
| fn get_time_at_velocity(v: f32) -> f32 { |
| -((v + BETA) / (-ALPHA * GAMMA)).ln() / GAMMA |
| } |
| |
| fn new(velocity: Vector2D<f32>, start_timestamp: Time) -> Self { |
| let max_start_velocity = |
| velocity.x.abs().max(velocity.y.abs()).min(Self::get_velocity_at_time(0.0)); |
| assert!(max_start_velocity > 0.0); |
| let displacement_ratio = velocity / max_start_velocity; |
| let time_offset = Self::get_time_at_velocity(max_start_velocity); |
| let position_offset = Self::get_position_at_time(time_offset); |
| let curve_duration = Self::get_time_at_velocity(0.0); |
| |
| FlingCurve { |
| curve_duration, |
| start_timestamp, |
| displacement_ratio, |
| cumulative_scroll: Vector2D::zero(), |
| previous_timestamp: start_timestamp, |
| time_offset, |
| position_offset, |
| } |
| } |
| |
| fn compute_scroll_offset(&mut self, timestamp: Time) -> (bool, Vector2D<f32>, Vector2D<f32>) { |
| let elapsed_time = timestamp - self.start_timestamp; |
| if elapsed_time < zx::Duration::from_nanos(0) { |
| return (true, Vector2D::zero(), Vector2D::zero()); |
| } |
| |
| const SECONDS_PER_NANOSECOND: f32 = 1e-9; |
| |
| let offset_time = |
| elapsed_time.into_nanos() as f32 * SECONDS_PER_NANOSECOND + self.time_offset; |
| let (still_active, scalar_offset, scalar_velocity) = if offset_time < self.curve_duration { |
| ( |
| true, |
| Self::get_position_at_time(offset_time) - self.position_offset, |
| Self::get_velocity_at_time(offset_time), |
| ) |
| } else { |
| (false, Self::get_position_at_time(self.curve_duration) - self.position_offset, 0.0) |
| }; |
| |
| ( |
| still_active, |
| self.displacement_ratio * scalar_offset, |
| self.displacement_ratio * scalar_velocity, |
| ) |
| } |
| |
| fn compute_scroll_delta_at_time(&mut self, current: Time) -> (bool, Vector2D<f32>) { |
| if current <= self.previous_timestamp { |
| (true, Vector2D::zero()) |
| } else { |
| self.previous_timestamp = current; |
| let (still_active, offset, _) = self.compute_scroll_offset(current); |
| let delta = offset - self.cumulative_scroll; |
| self.cumulative_scroll = offset; |
| (still_active, delta) |
| } |
| } |
| } |
| |
| struct InfiniteScroll { |
| scroll_method: ScrollMethod, |
| touch_sampling_offset: zx::Duration, |
| scene: Scene, |
| contents: BTreeMap<u64, Contents>, |
| last_presentation_time: Time, |
| touch_event_resampler: TouchEventResampler, |
| touch_points: Vec<Point>, |
| touch_time: Time, |
| previous_touch_points: Vec<Point>, |
| scroll_origin: Vector2D<f32>, |
| fling_curve: Option<FlingCurve>, |
| fake_scroll_start: Time, |
| fake_scroll_velocity: Option<f32>, |
| staging_image: Option<Image>, |
| } |
| |
| impl InfiniteScroll { |
| pub fn new( |
| scale: f32, |
| scroll_method: ScrollMethod, |
| exposure: f32, |
| touch_sampling_offset: zx::Duration, |
| max_columns: u32, |
| text_disabled: bool, |
| ) -> Self { |
| let scene = Scene::new(scale, exposure, max_columns, text_disabled); |
| let fake_scroll_start = |
| Time::from_nanos(Time::get_monotonic().into_nanos().saturating_add( |
| zx::Duration::from_seconds(FAKE_SCROLL_DELAY_SECONDS).into_nanos(), |
| )); |
| |
| Self { |
| scroll_method, |
| touch_sampling_offset, |
| scene, |
| contents: BTreeMap::new(), |
| last_presentation_time: Time::get_monotonic(), |
| touch_event_resampler: TouchEventResampler::new(), |
| touch_points: Vec::new(), |
| touch_time: Time::from_nanos(0), |
| previous_touch_points: Vec::new(), |
| scroll_origin: Vector2D::zero(), |
| fling_curve: None, |
| fake_scroll_start, |
| fake_scroll_velocity: None, |
| staging_image: None, |
| } |
| } |
| |
| fn update( |
| &mut self, |
| render_context: &mut Context, |
| context: &ViewAssistantContext, |
| ) -> Result<(), Error> { |
| duration!("gfx", "InfiniteScrollViewAssistant::update"); |
| |
| let size = &context.size; |
| let presentation_time = context.presentation_time; |
| let elapsed = presentation_time - self.last_presentation_time; |
| let scroll_method = self.scroll_method; |
| |
| // Scroll distance is set by input processing code below. |
| let mut scroll_distance = None; |
| |
| // Process touch events. |
| // |
| // Smooth motion is achieved by re-sampling touch events at |
| // a fixed offset relative to presentation time. |
| // |
| // Determine sample time by removing sampling offset from |
| // presentation time. |
| let sample_time = presentation_time - self.touch_sampling_offset; |
| |
| fn average(points: &Vec<Point>) -> Vector2D<f32> { |
| points.iter().fold(Vector2D::zero(), |sum, x| sum + x.to_vector()) / points.len() as f32 |
| } |
| |
| let events = self.touch_event_resampler.dequeue_and_sample(sample_time); |
| for event in events { |
| if let input::EventType::Touch(touch_event) = event.event_type { |
| // Save previous touch state. |
| self.previous_touch_points.clear(); |
| self.previous_touch_points.extend(self.touch_points.drain(..)); |
| // Update touch points and origin. Origin is the average of |
| // all touch points. |
| for contact in touch_event.contacts.iter() { |
| match contact.phase { |
| input::touch::Phase::Down(point, _) => { |
| self.touch_points.push(point.to_f32()) |
| } |
| input::touch::Phase::Moved(point, _) => { |
| self.touch_points.push(point.to_f32()) |
| } |
| _ => {} |
| } |
| } |
| counter!("input", "touch_y", 0, "y" => (average(&self.touch_points)).y.round() as u32); |
| |
| // Ignore previous touch points and set scroll origin if number of |
| // touch points changed. |
| if self.previous_touch_points.len() != self.touch_points.len() { |
| self.scroll_origin = average(&self.touch_points); |
| self.previous_touch_points.clear(); |
| } |
| } |
| } |
| |
| if !self.touch_points.is_empty() && !self.previous_touch_points.is_empty() { |
| let origin = average(&self.touch_points); |
| let touch_latency = presentation_time - self.touch_time; |
| // Print a warning if touch sampling offset is too low for |
| // accurate touch point sampling. |
| if touch_latency > self.touch_sampling_offset { |
| println!( |
| "warning: high touch latency {:?} ms", |
| touch_latency.into_nanos() as f32 / 1e+6 |
| ); |
| } |
| scroll_distance = Some(-(origin.y - self.scroll_origin.y)); |
| |
| // Delay the start of fake scrolling. |
| self.fake_scroll_start = |
| Time::from_nanos(presentation_time.into_nanos().saturating_add( |
| zx::Duration::from_seconds(FAKE_SCROLL_DELAY_SECONDS).into_nanos(), |
| )); |
| self.fake_scroll_velocity = None; |
| } |
| |
| const SECONDS_PER_NANOSECOND: f32 = 1e-9; |
| |
| // Fake scroll when idle. |
| if presentation_time.into_nanos() > self.fake_scroll_start.into_nanos() { |
| const MIN_FAKE_SPEED: f32 = 2048.0; |
| const MAX_FAKE_SPEED: f32 = 8192.0; |
| const FAKE_DIRECTIONS: [f32; 2] = [1.0, -1.0]; |
| |
| let fake_scroll_velocity = self.fake_scroll_velocity.get_or_insert_with(|| { |
| let mut rng = thread_rng(); |
| let speed: f32 = rng.gen_range(MIN_FAKE_SPEED, MAX_FAKE_SPEED); |
| let direction: usize = rng.gen_range(0, FAKE_DIRECTIONS.len()); |
| speed * FAKE_DIRECTIONS[direction] |
| }); |
| |
| const ACCELERATION: f32 = 12000.0; |
| let elapsed_seconds = (presentation_time.into_nanos() |
| - self.fake_scroll_start.into_nanos()) as f32 |
| * SECONDS_PER_NANOSECOND; |
| let velocity = (ACCELERATION * elapsed_seconds).min(fake_scroll_velocity.abs()) |
| * fake_scroll_velocity.signum(); |
| |
| scroll_distance = |
| Some(velocity * (elapsed.into_nanos() as f32 * SECONDS_PER_NANOSECOND)); |
| |
| // Fake scroll last for one second. Add small delay before next |
| // fake scroll starts. |
| if elapsed_seconds > 1.0 { |
| const NEXT_FAKE_SCROLL_DELAY_SECONDS: i64 = 2; |
| self.fake_scroll_start = |
| Time::from_nanos(presentation_time.into_nanos().saturating_add( |
| zx::Duration::from_seconds(NEXT_FAKE_SCROLL_DELAY_SECONDS).into_nanos(), |
| )); |
| self.fake_scroll_velocity = None; |
| } |
| } |
| |
| // Calculate scroll delta based on scroll distance and update fling |
| // curve. If scroll distance is not set then allow fling curve to |
| // determine scroll delta. |
| let mut scroll_delta = 0; |
| if let Some(distance) = scroll_distance { |
| scroll_delta = distance.round() as i32; |
| self.scroll_origin.y -= scroll_delta as f32; |
| if distance != 0.0 && elapsed.into_nanos() > 0 { |
| let velocity = distance / (elapsed.into_nanos() as f32 * SECONDS_PER_NANOSECOND); |
| self.fling_curve = Some(FlingCurve::new(vec2(0.0, velocity), presentation_time)); |
| } |
| } else if let Some(fling) = &mut self.fling_curve { |
| let (fling_active, delta) = fling.compute_scroll_delta_at_time(presentation_time); |
| scroll_delta = delta.y as i32; |
| if !fling_active { |
| self.fling_curve = None; |
| } |
| } |
| |
| // Update the scene using our scroll delta. |
| self.scene.update(render_context, size, scroll_delta); |
| |
| let image_id = context.image_id; |
| let image = render_context.get_current_image(context); |
| let content = |
| self.contents.entry(image_id).or_insert_with(|| Contents::new(image, scroll_method)); |
| |
| content.update(render_context, &mut self.scene, size, &mut self.staging_image); |
| |
| self.last_presentation_time = presentation_time; |
| |
| Ok(()) |
| } |
| |
| fn handle_pointer_event(&mut self, event: &input::Event) { |
| self.touch_event_resampler.enqueue(event.clone()); |
| if let input::EventType::Touch(_) = &event.event_type { |
| self.touch_time = Time::from_nanos(event.event_time as i64); |
| } |
| } |
| } |
| |
| struct InfiniteScrollViewAssistant { |
| size: Size, |
| args: Args, |
| infinite_scroll: Option<InfiniteScroll>, |
| } |
| |
| impl InfiniteScrollViewAssistant { |
| pub fn new(args: Args) -> Self { |
| Self { size: Size::zero(), args, infinite_scroll: None } |
| } |
| } |
| |
| impl ViewAssistant for InfiniteScrollViewAssistant { |
| fn render( |
| &mut self, |
| render_context: &mut Context, |
| ready_event: Event, |
| context: &ViewAssistantContext, |
| ) -> Result<(), Error> { |
| if context.size != self.size || self.infinite_scroll.is_none() { |
| let infinite_scroll = InfiniteScroll::new( |
| self.args.scale, |
| self.args.scroll_method, |
| self.args.exposure, |
| zx::Duration::from_millis(self.args.touch_sampling_offset_ms), |
| self.args.max_columns, |
| self.args.disable_text, |
| ); |
| |
| self.size = context.size; |
| self.infinite_scroll = Some(infinite_scroll); |
| } |
| |
| if let Some(infinite_scroll) = self.infinite_scroll.as_mut() { |
| infinite_scroll.update(render_context, context).expect("infinite_scroll.update"); |
| } |
| |
| ready_event.as_handle_ref().signal(Signals::NONE, Signals::EVENT_SIGNALED)?; |
| |
| context.request_render(); |
| |
| Ok(()) |
| } |
| |
| fn handle_pointer_event( |
| &mut self, |
| _context: &mut ViewAssistantContext, |
| event: &input::Event, |
| _pointer_event: &input::pointer::Event, |
| ) -> Result<(), Error> { |
| if let Some(infinite_scroll) = self.infinite_scroll.as_mut() { |
| infinite_scroll.handle_pointer_event(event); |
| } |
| Ok(()) |
| } |
| } |
| |
| fn main() -> Result<(), Error> { |
| fuchsia_trace_provider::trace_provider_create_with_fdio(); |
| |
| println!("Infinite Scroll Example"); |
| App::run(make_app_assistant::<InfiniteScrollAppAssistant>()) |
| } |