blob: 22affe6d7301aec4f90b2625c0351bbf7c751e53 [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 {
carnelian::{
color::Color,
drawing::{path_for_rectangle, FontFace, Glyph},
render::{BlendMode, Context as RenderContext, Fill, FillRule, Layer, Raster, Style},
Coord, Point, Rect, Size,
},
euclid::default::Vector2D,
fuchsia_trace as ftrace,
term_model::{
ansi::CursorStyle,
term::{CursorKey, RenderableCellContent, RenderableCellsIter},
},
};
const UNDERLINE_CURSOR_CHAR: char = '\u{10a3e2}';
const BEAM_CURSOR_CHAR: char = '\u{10a3e3}';
const BOX_CURSOR_CHAR: char = '\u{10a3e4}';
const MAXIMUM_THUMB_RATIO: f32 = 0.8;
const MINIMUM_THUMB_RATIO: f32 = 0.05;
const SCROLL_BAR_MOVEMENT_THRESHOLD: f32 = 1.0;
static FONT_DATA: &'static [u8] =
include_bytes!("../../../../../prebuilt/third_party/fonts/robotomono/RobotoMono-Regular.ttf");
fn make_color(term_color: &term_model::term::color::Rgb) -> Color {
Color { r: term_color.r, g: term_color.g, b: term_color.b, a: 0xFF }
}
fn raster_for_rectangle(bounds: &Rect, render_context: &mut RenderContext) -> Raster {
let mut raster_builder = render_context.raster_builder().expect("raster_builder");
raster_builder.add(&path_for_rectangle(bounds, render_context), None);
raster_builder.build()
}
pub struct GridView {
font: FontFace,
background_color: Color,
pub frame: Rect,
pub cell_size: Size,
}
impl Default for GridView {
fn default() -> Self {
GridView::new(&Color::new())
}
}
impl GridView {
pub fn new(background_color: &Color) -> GridView {
GridView {
frame: Rect::zero(),
background_color: *background_color,
font: FontFace::new(FONT_DATA).expect("unable to load font data"),
cell_size: Size::zero(),
}
}
pub fn render<'a, C>(
&self,
render_context: &mut RenderContext,
cells: RenderableCellsIter<'a, C>,
) -> Vec<Layer> {
let size = self.cell_size;
let font = &self.font;
let font_size = size.height * 0.9;
let baseline = font_size * 0.9;
let background_color = self.background_color;
let (mut layers, maybe_bg_layers): (Vec<_>, Vec<_>) = cells
.filter_map(|cell| {
if let Some(character) = maybe_char_for_renderable_cell_content(cell.inner) {
let cell_position = Point::new(
size.width * cell.column.0 as f32,
size.height * cell.line.0 as f32,
);
let char_position = cell_position + Vector2D::new(0.0, baseline);
let glyph_index = font.face.glyph_index(character);
let glyph = Glyph::new(render_context, &self.font, font_size, glyph_index);
let cell_bounds = Rect::new(cell_position, size);
let fg_raster = if glyph_index.is_none() {
raster_for_rectangle(&cell_bounds, render_context)
} else {
let pos_vec = char_position.to_vector().to_i32();
glyph.raster.translate(pos_vec)
};
let cell_background_color = make_color(&cell.bg);
let bg_layer = if cell_background_color != background_color {
let bg_raster = raster_for_rectangle(&cell_bounds, render_context);
Some(Layer {
raster: bg_raster,
style: Style {
fill_rule: FillRule::NonZero,
fill: Fill::Solid(cell_background_color),
blend_mode: BlendMode::Over,
},
})
} else {
None
};
Some((
Layer {
raster: fg_raster,
style: Style {
fill_rule: FillRule::NonZero,
fill: Fill::Solid(make_color(&cell.fg)),
blend_mode: BlendMode::Over,
},
},
bg_layer,
))
} else {
None
}
})
.unzip();
let bg_layers: Vec<Layer> = maybe_bg_layers.into_iter().filter_map(|a| a).collect();
layers.extend(bg_layers);
layers
}
}
// The term-model library gives us zero-width characters in our array of chars. However,
// we do not support this at thsi point so we just pull out the first char for rendering.
fn maybe_char_for_renderable_cell_content(content: RenderableCellContent) -> Option<char> {
match content {
RenderableCellContent::Cursor(cursor_key) => chars_for_cursor(cursor_key),
RenderableCellContent::Chars(chars) => Some(chars[0]),
}
}
fn chars_for_cursor(cursor: CursorKey) -> Option<char> {
match cursor.style {
CursorStyle::Block => Some(BOX_CURSOR_CHAR),
CursorStyle::Underline => Some(UNDERLINE_CURSOR_CHAR),
CursorStyle::Beam => Some(BEAM_CURSOR_CHAR),
//TODO add support for HollowBlock style
CursorStyle::HollowBlock => Some(UNDERLINE_CURSOR_CHAR),
CursorStyle::Hidden => None,
}
}
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,
/// The frame to draw the scroll bar thumb
thumb_frame: Option<Rect>,
/// Indicates whether we are tracking a scroll or not. This will
/// eventually need to track the device_id when we handle multiple
/// input events.
last_pointer_tracking_location: Option<Point>,
}
impl Default for ScrollBar {
fn default() -> Self {
ScrollBar {
frame: Rect::zero(),
content_height: 0.0,
content_offset: 0.0,
thumb_frame: None,
last_pointer_tracking_location: None,
}
}
}
impl ScrollBar {
pub fn render(&self, render_context: &mut RenderContext) -> Option<Layer> {
ftrace::duration!("terminal", "Views:ScrollBar:render2");
self.thumb_frame
.and_then(|thumb_frame| Some(Self::render_thumb_pattern(render_context, &thumb_frame)))
}
/// 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_frame(&mut self) {
self.update_thumb_frame();
}
pub fn begin_tracking_pointer_event(&mut self, point: Point) {
if let Some(frame) = &self.thumb_frame {
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);
}
self.last_pointer_tracking_location = Some(point);
}
}
pub fn handle_pointer_move(&mut self, point: Point) {
if let (Some(last_point), Some(thumb_frame)) =
(self.last_pointer_tracking_location, self.thumb_frame)
{
let dy = last_point.y - point.y;
// We do not want to respond to every micro pixel change. Only
// move if we are above the threshold
if dy.abs() < SCROLL_BAR_MOVEMENT_THRESHOLD {
return;
}
let conversion_factor = self.pixel_space_to_content_space_conversion_factor();
let proposed_offset = self.content_offset + (conversion_factor * dy);
self.propose_offset(proposed_offset, conversion_factor, thumb_frame.size.height);
self.last_pointer_tracking_location = Some(point);
}
}
pub fn cancel_pointer_event(&mut self) {
self.last_pointer_tracking_location = 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_frame();
}
#[inline]
pub fn is_tracking(&self) -> bool {
self.last_pointer_tracking_location.is_some()
}
fn update_thumb_frame(&mut self) {
if let Some(thumb_info) = self.calculate_thumb_render_info() {
let thumb_frame = thumb_info.calculate_frame_in_rect(&self.frame);
self.thumb_frame = Some(thumb_frame);
} else {
self.thumb_frame = None;
}
}
fn render_thumb_pattern(render_context: &mut RenderContext, frame: &Rect) -> Layer {
let white = Color::white();
let raster = raster_for_rectangle(&frame, render_context);
Layer {
raster: raster,
style: Style {
fill_rule: FillRule::NonZero,
fill: Fill::Solid(white),
blend_mode: BlendMode::Over,
},
}
}
fn calculate_thumb_render_info(&self) -> Option<ThumbRenderInfo> {
if self.content_height <= self.frame.size.height {
return None;
}
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);
Some(ThumbRenderInfo { height, vertical_offset })
}
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 height of the ScrollBarThumb
height: Coord,
/// The y position of the bottom of the ScrollBarThumb
vertical_offset: Coord,
}
impl ThumbRenderInfo {
fn calculate_frame_in_rect(&self, outer_rect: &Rect) -> Rect {
let size = Size::new(outer_rect.size.width, self.height);
let origin = Point::new(
outer_rect.origin.x,
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.last_pointer_tracking_location = Some(Point::new(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_does_not_change_content_offset_if_less_than_threshold() {
let mut scroll_bar = ScrollBar::default();
scroll_bar.frame = rect_with_height(100.0);
scroll_bar.content_height = 400.0;
scroll_bar.invalidate_thumb_frame();
scroll_bar.last_pointer_tracking_location = Some(Point::new(10.0, 90.0));
scroll_bar.handle_pointer_move(Point::new(10.0, 89.9));
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_frame();
scroll_bar.last_pointer_tracking_location = Some(Point::new(10.0, 90.0));
// 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_frame();
scroll_bar.last_pointer_tracking_location = Some(Point::new(10.0, 90.0));
// 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_frame();
scroll_bar.last_pointer_tracking_location = Some(Point::new(10.0, 90.0));
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_frame();
scroll_bar.last_pointer_tracking_location = Some(Point::new(10.0, 90.0));
scroll_bar.handle_pointer_move(Point::new(10.0, 0.0));
assert_eq!(scroll_bar.content_offset, 300.0);
}
#[test]
fn scroll_bar_handle_pointer_move_updates_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_frame();
scroll_bar.last_pointer_tracking_location = Some(Point::new(10.0, 10.0));
scroll_bar.handle_pointer_move(Point::new(10.0, 11.0));
assert_eq!(scroll_bar.last_pointer_tracking_location.unwrap(), Point::new(10.0, 11.0));
}
#[test]
fn scroll_bar_begin_pointer_move_updates_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_frame();
scroll_bar.begin_tracking_pointer_event(Point::new(5.0, 90.0));
assert_eq!(scroll_bar.last_pointer_tracking_location.unwrap(), Point::new(5.0, 90.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_frame();
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_frame();
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_frame();
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_frame();
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_frame();
scroll_bar.begin_tracking_pointer_event(Point::new(10.0, 10.0));
scroll_bar.cancel_pointer_event();
assert!(scroll_bar.last_pointer_tracking_location.is_none());
}
#[test]
fn thumb_frame_updated_when_told_thumb_frame_is_invalidated() {
let mut scroll_bar = ScrollBar::default();
scroll_bar.content_height = 10_000.0;
scroll_bar.invalidate_thumb_frame();
assert!(scroll_bar.thumb_frame.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 { height: 100.0, vertical_offset: 100.0 };
let second = ThumbRenderInfo { height: 100.0, vertical_offset: 100.0 };
assert_eq!(first, second);
}
#[test]
fn scroll_context_thumb_render_info_not_equal_diff_offset() {
let first = ThumbRenderInfo { height: 100.0, vertical_offset: 100.0 };
let second = ThumbRenderInfo { height: 100.0, vertical_offset: 0.0 };
assert_ne!(first, second);
}
#[test]
fn scroll_context_thumb_render_info_equality_not_equal_diff_height() {
let first = ThumbRenderInfo { height: 100.0, vertical_offset: 100.0 };
let second = ThumbRenderInfo { height: 10.0, vertical_offset: 100.0 };
assert_ne!(first, second);
}
#[test]
fn thumb_render_info_calculate_frame_in_rect() {
let thumb_info = ThumbRenderInfo { height: 10.0, vertical_offset: 10.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(10.0, 990.0), Size::new(10.0, 10.0)));
}
}