blob: 178f2ec6c5fe080b4824aa643cbea6b1840d8cfd [file] [log] [blame]
// 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>())
}