blob: a9702b578af960db11c6b363db7b643092801d4d [file] [log] [blame]
// Copyright 2019 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 {
crate::ui::TerminalMessages,
carnelian::{make_message, AppSender, Coord, MessageTarget, Point, Rect, Size, ViewKey},
};
const MAXIMUM_THUMB_RATIO: f32 = 0.8;
const MINIMUM_THUMB_RATIO: f32 = 0.05;
// Default width of scroll bar thumb.
const THUMB_WIDTH: f32 = 8.0;
// Alpha values for thumb.
const THUMB_NORMAL_ALPHA: f32 = 0.6;
const THUMB_TRACKING_ALPHA: f32 = 0.8;
const THUMB_DIM_ALPHA: f32 = 0.4;
pub struct ScrollBar {
pub frame: Rect,
/// The content size of the scrollable area.
pub content_height: Coord,
/// The vertical distance that the content is offset from the bottom
pub content_offset: Coord,
/// Whether scroll bar thumb should be hidden or not.
pub hidden_thumb: bool,
/// Whether to draw a wide thumb or not.
pub wide_thumb: bool,
/// Whether to dim thumb or not.
pub dim_thumb: bool,
/// The rectangle and alpha used to draw the scroll bar thumb
thumb: Option<(Rect, f32)>,
/// Indicates whether we are tracking a scroll or not. This will
/// eventually need to track the device_id when we handle multiple
/// input events.
pointer_tracking_start: Option<(Point, Coord)>,
// AppSender used to update scroll thumb rendering.
app_sender: Option<AppSender>,
view_key: ViewKey,
}
impl Default for ScrollBar {
fn default() -> Self {
ScrollBar {
app_sender: None,
view_key: ViewKey::default(),
frame: Rect::zero(),
content_height: 0.0,
content_offset: 0.0,
hidden_thumb: false,
wide_thumb: true,
dim_thumb: true,
thumb: None,
pointer_tracking_start: None,
}
}
}
impl ScrollBar {
pub fn new(app_sender: AppSender, view_key: ViewKey) -> Self {
ScrollBar {
app_sender: Some(app_sender),
view_key,
frame: Rect::zero(),
content_height: 0.0,
content_offset: 0.0,
hidden_thumb: true,
wide_thumb: false,
dim_thumb: true,
thumb: None,
pointer_tracking_start: None,
}
}
/// This method must be called after the client has updated
/// the frame, content_height or content_offset. We leave this
/// up to the caller to allow for the optimization of batching
/// these updates without needing to recalculate the frame.
pub fn invalidate_thumb(&mut self) {
self.update_thumb();
}
pub fn begin_tracking_pointer_event(&mut self, point: Point) {
if let Some((frame, _)) = &self.thumb {
if !frame.contains(point) {
// jump the middle of the thumb to the middle of the point
let thumb_height = frame.size.height;
let conversion_factor = self.pixel_space_to_content_space_conversion_factor();
let proposed_offset = conversion_factor
* (self.frame.size.height
- (point.y - self.frame.origin.y)
- (thumb_height / 2.0));
self.propose_offset(proposed_offset, conversion_factor, thumb_height);
}
let start_content_offset = self.content_offset;
self.pointer_tracking_start = Some((point, start_content_offset));
}
}
pub fn handle_pointer_move(&mut self, point: Point) {
if let (Some((start_point, start_offset)), Some((frame, _))) =
(self.pointer_tracking_start, self.thumb)
{
let dy = start_point.y - point.y;
let conversion_factor = self.pixel_space_to_content_space_conversion_factor();
let proposed_offset = start_offset + (conversion_factor * dy);
self.propose_offset(proposed_offset, conversion_factor, frame.size.height);
}
}
pub fn cancel_pointer_event(&mut self) {
self.pointer_tracking_start = None;
}
fn propose_offset(
&mut self,
proposed_offset: Coord,
conversion_factor: f32,
thumb_height: f32,
) {
// we have some rounding errors which make us loose 2 pixels. We round our inputs
// to get those pixels back when calculating the max_offset.
let max_offset =
f32::ceil((self.frame.size.height - f32::floor(thumb_height)) * conversion_factor);
self.content_offset = Coord::min(Coord::max(proposed_offset, 0.0), max_offset);
self.invalidate_thumb();
}
#[inline]
pub fn is_tracking(&self) -> bool {
self.pointer_tracking_start.is_some()
}
#[inline]
pub fn thumb_contains(&self, point: Point) -> bool {
self.thumb.map(|(frame, _)| frame.contains(point)).unwrap_or(false)
}
fn update_thumb(&mut self) {
let thumb = if let Some(thumb_info) = self.calculate_thumb_render_info() {
let thumb_frame = thumb_info.calculate_frame_in_rect(&self.frame);
Some((thumb_frame, thumb_info.alpha))
} else {
None
};
if self.thumb != thumb {
self.thumb = thumb;
if let Some(app_sender) = &self.app_sender {
app_sender.queue_message(
MessageTarget::View(self.view_key),
make_message(TerminalMessages::SetScrollThumbMessage(thumb)),
);
app_sender.request_render(self.view_key);
}
}
}
fn calculate_thumb_render_info(&self) -> Option<ThumbRenderInfo> {
if self.hidden_thumb || self.content_height <= self.frame.size.height {
return None;
}
let width = if self.wide_thumb { self.frame.size.width } else { THUMB_WIDTH };
let height =
Self::calculate_thumb_height_ratio(self.frame.size.height, self.content_height)
* self.frame.size.height;
let vertical_offset =
Coord::floor(self.content_space_to_pixel_space_factor(&height) * self.content_offset);
let alpha = if self.is_tracking() {
THUMB_TRACKING_ALPHA
} else if self.dim_thumb {
THUMB_DIM_ALPHA
} else {
THUMB_NORMAL_ALPHA
};
Some(ThumbRenderInfo { width, height, vertical_offset, alpha })
}
fn calculate_thumb_height_ratio(frame_height: Coord, content_height: Coord) -> Coord {
let ratio = frame_height / content_height;
Coord::min(Coord::max(MINIMUM_THUMB_RATIO, ratio), MAXIMUM_THUMB_RATIO)
}
#[inline]
fn pixel_space_to_content_space_conversion_factor(&self) -> Coord {
// this method is different from the thumb_height_ratio in that it will never round
// so it can be used to calculate offsets from pixel positions.
self.content_height / self.frame.size.height
}
#[inline]
fn content_space_to_pixel_space_factor(&self, thumb_height: &Coord) -> Coord {
(self.frame.size.height - thumb_height) / (self.content_height - self.frame.size.height)
}
}
#[derive(PartialEq, Debug)]
struct ThumbRenderInfo {
// The width of the ScrollBarThumb
width: Coord,
// The height of the ScrollBarThumb
height: Coord,
// The y position of the bottom of the ScrollBarThumb
vertical_offset: Coord,
// The alpha value of the ScrollBarThumb
alpha: f32,
}
impl ThumbRenderInfo {
fn calculate_frame_in_rect(&self, outer_rect: &Rect) -> Rect {
let size = Size::new(self.width, self.height);
let origin = Point::new(
outer_rect.origin.x + outer_rect.size.width - self.width,
outer_rect.origin.y + outer_rect.size.height - self.vertical_offset - self.height,
);
Rect::new(origin, size)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn rect_with_height(height: Coord) -> Rect {
Rect::new(Point::zero(), Size::new(10.0, height))
}
#[test]
fn sroll_bar_is_tracking_flag() {
let mut scroll_bar = ScrollBar::default();
assert_eq!(scroll_bar.is_tracking(), false);
scroll_bar.pointer_tracking_start = Some((Point::new(0.0, 0.0), 0.0));
assert_eq!(scroll_bar.is_tracking(), true);
}
#[test]
fn scroll_bar_does_not_change_content_offset_if_not_tracking() {
let mut scroll_bar = ScrollBar::default();
scroll_bar.frame = rect_with_height(100.0);
scroll_bar.content_height = 400.0;
scroll_bar.handle_pointer_move(Point::new(50.0, 50.0));
assert_eq!(scroll_bar.content_offset, 0.0);
}
#[test]
fn scroll_bar_updates_content_offset_on_move_when_tracking() {
let mut scroll_bar = ScrollBar::default();
scroll_bar.frame = rect_with_height(100.0);
scroll_bar.content_height = 400.0;
scroll_bar.invalidate_thumb();
scroll_bar.pointer_tracking_start =
Some((Point::new(10.0, 90.0), scroll_bar.content_offset));
// a movement of 1 pixel in view space should equate to a movement
// of 4 points in content space
scroll_bar.handle_pointer_move(Point::new(10.0, 89.0));
assert_eq!(scroll_bar.content_offset, 4.0);
}
#[test]
fn scroll_bar_updates_content_offset_on_move_when_tracking_nonzero_origin() {
let mut scroll_bar = ScrollBar::default();
scroll_bar.frame = Rect::new(Point::new(10.0, 10.0), Size::new(10.0, 100.0));
scroll_bar.content_height = 400.0;
scroll_bar.invalidate_thumb();
scroll_bar.pointer_tracking_start =
Some((Point::new(10.0, 90.0), scroll_bar.content_offset));
// a movement of 1 pixel in view space should equate to a movement
// of 4 points in content space
scroll_bar.handle_pointer_move(Point::new(10.0, 89.0));
assert_eq!(scroll_bar.content_offset, 4.0);
}
#[test]
fn scroll_bar_updates_content_offset_on_move_when_tracking_stays_above_zero() {
let mut scroll_bar = ScrollBar::default();
scroll_bar.frame = rect_with_height(100.0);
scroll_bar.content_height = 400.0;
scroll_bar.invalidate_thumb();
scroll_bar.pointer_tracking_start =
Some((Point::new(10.0, 90.0), scroll_bar.content_offset));
scroll_bar.handle_pointer_move(Point::new(10.0, 91.0));
assert_eq!(scroll_bar.content_offset, 0.0);
}
#[test]
fn scroll_bar_updates_content_offset_on_move_when_tracking_does_not_exceed_maximum() {
let mut scroll_bar = ScrollBar::default();
scroll_bar.frame = rect_with_height(100.0);
scroll_bar.content_height = 400.0;
scroll_bar.invalidate_thumb();
scroll_bar.pointer_tracking_start =
Some((Point::new(10.0, 90.0), scroll_bar.content_offset));
scroll_bar.handle_pointer_move(Point::new(10.0, 0.0));
assert_eq!(scroll_bar.content_offset, 300.0);
}
#[test]
fn scroll_bar_begin_pointer_move_jumps_if_initial_point_outside_thumb_min() {
let mut scroll_bar = ScrollBar::default();
scroll_bar.frame = rect_with_height(100.0);
scroll_bar.content_height = 400.0;
scroll_bar.invalidate_thumb();
scroll_bar.begin_tracking_pointer_event(Point::new(5.0, 10.0));
assert_eq!(scroll_bar.content_offset, 300.0);
}
#[test]
fn scroll_bar_begin_pointer_move_jumps_if_initial_point_outside_thumb() {
let mut scroll_bar = ScrollBar::default();
scroll_bar.frame = rect_with_height(100.0);
scroll_bar.content_height = 500.0;
scroll_bar.invalidate_thumb();
scroll_bar.begin_tracking_pointer_event(Point::new(5.0, 50.0));
assert_eq!(scroll_bar.content_offset, 200.0);
}
#[test]
fn scroll_bar_begin_pointer_move_jumps_if_initial_point_outside_thumb_nonzero_origin() {
let mut scroll_bar = ScrollBar::default();
scroll_bar.frame = Rect::new(Point::new(10.0, 10.0), Size::new(10.0, 100.0));
scroll_bar.content_height = 500.0;
scroll_bar.invalidate_thumb();
scroll_bar.begin_tracking_pointer_event(Point::new(5.0, 60.0));
assert_eq!(scroll_bar.content_offset, 200.0);
}
#[test]
fn scroll_bar_begin_pointer_move_jumps_if_initial_point_outside_thumb_max() {
let mut scroll_bar = ScrollBar::default();
scroll_bar.frame = rect_with_height(100.0);
scroll_bar.content_height = 400.0;
scroll_bar.content_offset = 300.0;
scroll_bar.invalidate_thumb();
scroll_bar.begin_tracking_pointer_event(Point::new(5.0, 99.0));
assert_eq!(scroll_bar.content_offset, 0.0);
}
#[test]
fn scroll_bar_cancel_pointer_event_drops_last_tracking_point() {
let mut scroll_bar = ScrollBar::default();
scroll_bar.frame = rect_with_height(100.0);
scroll_bar.content_height = 400.0;
scroll_bar.invalidate_thumb();
scroll_bar.begin_tracking_pointer_event(Point::new(10.0, 10.0));
scroll_bar.cancel_pointer_event();
assert!(scroll_bar.pointer_tracking_start.is_none());
}
#[test]
fn thumb_updated_when_told_thumb_is_invalidated() {
let mut scroll_bar = ScrollBar::default();
scroll_bar.content_height = 10_000.0;
scroll_bar.invalidate_thumb();
assert!(scroll_bar.thumb.is_some());
}
#[test]
fn thumb_render_info_none_same_content_size_and_frame() {
let scroll_bar = ScrollBar::default();
let thumb_info = scroll_bar.calculate_thumb_render_info();
assert!(thumb_info.is_none());
}
#[test]
fn thumb_render_info_none_not_scrollable() {
let mut scroll_bar = ScrollBar::default();
scroll_bar.frame = rect_with_height(1000.0);
scroll_bar.content_height = 900.0;
let thumb_info = scroll_bar.calculate_thumb_render_info();
assert!(thumb_info.is_none());
}
#[test]
fn scroll_bar_thumb_render_info_returns_proper_height() {
let mut scroll_bar = ScrollBar::default();
scroll_bar.content_height = 2_000.0;
scroll_bar.frame.size.height = 1_000.0;
let render_info = scroll_bar.calculate_thumb_render_info().unwrap();
assert_eq!(render_info.height, 500.0,);
}
#[test]
fn calculate_thumb_height_ratio_pins_to_min() {
let ratio = ScrollBar::calculate_thumb_height_ratio(100.0, 10_100.0);
assert_eq!(ratio, super::MINIMUM_THUMB_RATIO);
}
#[test]
fn calculate_thumb_height_ratio_pins_to_max() {
let ratio = ScrollBar::calculate_thumb_height_ratio(100.0, 101.0);
assert_eq!(ratio, super::MAXIMUM_THUMB_RATIO);
}
#[test]
fn calculate_thumb_height_ratio() {
let ratio = ScrollBar::calculate_thumb_height_ratio(10.0, 40.0);
assert_eq!(ratio, 0.25);
}
#[test]
fn calculate_thumb_vertical_offset_top() {
let mut scroll_bar = ScrollBar::default();
scroll_bar.frame = rect_with_height(100.0);
scroll_bar.content_height = 400.0;
scroll_bar.content_offset = 300.0;
let render_info = scroll_bar.calculate_thumb_render_info().unwrap();
assert_eq!(render_info.vertical_offset, 75.0);
}
#[test]
fn calculate_thumb_vertical_offset_mid() {
let mut scroll_bar = ScrollBar::default();
scroll_bar.frame = rect_with_height(100.0);
scroll_bar.content_height = 400.0;
scroll_bar.content_offset = 100.0;
let render_info = scroll_bar.calculate_thumb_render_info().unwrap();
assert_eq!(render_info.vertical_offset, 25.0);
}
#[test]
fn calculate_thumb_vertical_offset_with_round() {
let mut scroll_bar = ScrollBar::default();
scroll_bar.frame = rect_with_height(100.0);
scroll_bar.content_height = 300.0;
scroll_bar.content_offset = 100.0;
let render_info = scroll_bar.calculate_thumb_render_info().unwrap();
assert_eq!(render_info.vertical_offset, 33.0);
}
#[test]
fn calculate_thumb_vertical_offset_bottom() {
let mut scroll_bar = ScrollBar::default();
scroll_bar.frame = rect_with_height(100.0);
scroll_bar.content_height = 302.0;
scroll_bar.content_offset = 0.0;
let render_info = scroll_bar.calculate_thumb_render_info().unwrap();
assert_eq!(render_info.vertical_offset, 0.0);
}
#[test]
fn scroll_context_thumb_render_info_equality() {
let first =
ThumbRenderInfo { width: 1.0, height: 100.0, vertical_offset: 100.0, alpha: 1.0 };
let second =
ThumbRenderInfo { width: 1.0, height: 100.0, vertical_offset: 100.0, alpha: 1.0 };
assert_eq!(first, second);
}
#[test]
fn scroll_context_thumb_render_info_not_equal_diff_offset() {
let first =
ThumbRenderInfo { width: 1.0, height: 100.0, vertical_offset: 100.0, alpha: 1.0 };
let second =
ThumbRenderInfo { width: 1.0, height: 100.0, vertical_offset: 0.0, alpha: 1.0 };
assert_ne!(first, second);
}
#[test]
fn scroll_context_thumb_render_info_equality_not_equal_diff_height() {
let first =
ThumbRenderInfo { width: 1.0, height: 100.0, vertical_offset: 100.0, alpha: 1.0 };
let second =
ThumbRenderInfo { width: 1.0, height: 10.0, vertical_offset: 100.0, alpha: 1.0 };
assert_ne!(first, second);
}
#[test]
fn thumb_render_info_calculate_frame_in_rect() {
let thumb_info =
ThumbRenderInfo { width: 1.0, height: 10.0, vertical_offset: 10.0, alpha: 1.0 };
let outer = Rect::new(Point::new(10.0, 10.0), Size::new(10.0, 1000.0));
let rect = thumb_info.calculate_frame_in_rect(&outer);
assert_eq!(rect, Rect::new(Point::new(19.0, 990.0), Size::new(1.0, 10.0)));
}
}