use std::rc::Rc;
use crate::render::Canvas;
use crate::types::Rect;
use super::metrics::MultilineMetrics;
use super::theme;
use super::TextFont;
use super::wrapping::{ compute_visual_lines, VisualLine };
pub( crate ) const WHEEL_LINES: f32 = 3.0;
#[ derive( Clone, Copy, Debug, Default, PartialEq ) ]
pub struct TextScroll
{
pub( crate ) first_line: usize,
pub( crate ) cursor_pos: usize,
pub( crate ) remainder: f32,
}
impl TextScroll
{
pub( crate ) fn first_visible_line(
self,
visible: usize,
total_lines: usize,
cursor_line: usize,
cursor_pos: usize,
) -> usize
{
let max_first = total_lines.saturating_sub( visible );
let first = self.first_line.min( max_first );
if self.cursor_pos == cursor_pos
{
first
} else if cursor_line < first {
cursor_line
} else if cursor_line + 1 > first + visible {
cursor_line + 1 - visible
} else {
first
}
}
pub( crate ) fn scrolled(
self,
first_line: usize,
max_first: usize,
cursor_pos: usize,
delta_px: f32,
line_h: f32,
) -> ( Self, bool )
{
let px = self.remainder + delta_px;
let lines = ( px / line_h ).trunc();
let remainder = px - lines * line_h;
let target = ( first_line as i64 + lines as i64 ).clamp( 0, max_first as i64 ) as usize;
let next = Self { first_line: target, cursor_pos, remainder };
( next, target != first_line )
}
}
pub( crate ) struct Window
{
pub metrics: MultilineMetrics,
pub lines: Rc<Vec<VisualLine>>,
pub cursor_line: usize,
pub first_line: usize,
pub max_first: usize,
pub visible: usize,
}
#[ derive( Clone, Copy, Debug ) ]
pub( crate ) struct Scrollbar
{
pub track: Rect,
pub thumb: Rect,
pub hit: Rect,
}
impl Window
{
pub( crate ) fn resolve(
canvas: &Canvas,
rect: Rect,
value: &str,
font: &TextFont,
cursor_pos: usize,
scroll: TextScroll,
) -> Self
{
let metrics = MultilineMetrics::resolve( canvas, font.size );
let lines = compute_visual_lines( canvas, font, value, metrics.inner_width( rect ), metrics.font_size );
let cursor_pos = cursor_pos.min( value.len() );
let scroll = TextScroll { cursor_pos: scroll.cursor_pos.min( value.len() ), ..scroll };
let cursor_line = lines.iter()
.position( |vl| cursor_pos >= vl.start && cursor_pos <= vl.end )
.unwrap_or( lines.len().saturating_sub( 1 ) );
let visible = metrics.visible_lines( rect );
let first_line = scroll.first_visible_line( visible, lines.len(), cursor_line, cursor_pos );
let max_first = lines.len().saturating_sub( visible );
Self { metrics, lines, cursor_line, first_line, max_first, visible }
}
pub( crate ) fn shown(
canvas: &Canvas,
rect: Rect,
value: &str,
font: &TextFont,
cursor_pos: usize,
scroll: TextScroll,
) -> Self
{
let mut window = Self::resolve( canvas, rect, value, font, cursor_pos, scroll );
window.first_line = scroll.first_line.min( window.max_first );
window
}
pub( crate ) fn scrollbar( &self, canvas: &Canvas, rect: Rect ) -> Option<Scrollbar>
{
if self.max_first == 0 { return None; }
let margin = canvas.geom_px( theme::SCROLLBAR_MARGIN );
let width = canvas.geom_px( theme::SCROLLBAR_W );
let track = Rect
{
x: rect.x + rect.width - margin - width,
y: rect.y + margin,
width,
height: ( rect.height - margin * 2.0 ).max( 0.0 ),
};
let share = self.visible as f32 / self.lines.len() as f32;
let thumb_h = ( track.height * share ).max( canvas.geom_px( theme::SCROLLBAR_MIN ) ).min( track.height );
let thumb_y = track.y + ( track.height - thumb_h ) * self.first_line as f32 / self.max_first as f32;
let hit_w = canvas.geom_px( theme::SCROLLBAR_HIT ).min( rect.width );
Some( Scrollbar
{
track,
thumb: Rect { y: thumb_y, height: thumb_h, ..track },
hit: Rect { x: rect.x + rect.width - hit_w, width: hit_w, ..rect },
} )
}
pub( crate ) fn first_line_for( &self, bar: &Scrollbar, thumb_y: f32 ) -> usize
{
let travel = bar.track.height - bar.thumb.height;
if travel <= 0.0 { return 0; }
let share = ( ( thumb_y - bar.track.y ) / travel ).clamp( 0.0, 1.0 );
( share * self.max_first as f32 ).round() as usize
}
pub( crate ) fn can_scroll( &self, delta_px: f32 ) -> bool
{
( delta_px < 0.0 && self.first_line > 0 ) || ( delta_px > 0.0 && self.first_line < self.max_first )
}
}
#[ cfg( test ) ]
mod tests
{
use super::TextScroll;
fn at( first_line: usize, cursor_pos: usize ) -> TextScroll
{
TextScroll { first_line, cursor_pos, remainder: 0.0 }
}
#[ test ]
fn without_a_request_the_view_follows_the_cursor()
{
let s = TextScroll::default();
assert_eq!( s.first_visible_line( 10, 30, 4, 40 ), 0 );
assert_eq!( s.first_visible_line( 10, 30, 12, 130 ), 3 );
assert_eq!( s.first_visible_line( 10, 30, 29, 300 ), 20 );
}
#[ test ]
fn a_request_holds_while_the_cursor_stays()
{
let s = at( 15, 290 );
assert_eq!( s.first_visible_line( 10, 30, 29, 290 ), 15 );
}
#[ test ]
fn a_request_is_clamped_to_the_last_full_window()
{
assert_eq!( at( 50, 0 ).first_visible_line( 10, 30, 0, 0 ), 20 );
assert_eq!( at( 5, 0 ).first_visible_line( 10, 8, 0, 0 ), 0 );
}
#[ test ]
fn once_the_cursor_moves_the_view_shows_it_with_the_least_motion()
{
let s = at( 15, 290 );
assert_eq!( s.first_visible_line( 10, 30, 29, 291 ), 20 );
assert_eq!( s.first_visible_line( 10, 30, 2, 20 ), 2 );
assert_eq!( s.first_visible_line( 10, 30, 18, 180 ), 15 );
}
#[ test ]
fn a_wheel_notch_moves_three_lines_and_stops_at_the_ends()
{
let line_h = 20.0;
let ( s, moved ) = TextScroll::default().scrolled( 0, 20, 7, super::WHEEL_LINES * line_h, line_h );
assert!( moved );
assert_eq!( ( s.first_line, s.cursor_pos, s.remainder ), ( 3, 7, 0.0 ) );
let ( s, moved ) = s.scrolled( 19, 20, 7, super::WHEEL_LINES * line_h, line_h );
assert!( moved );
assert_eq!( s.first_line, 20 );
let ( s, moved ) = s.scrolled( 20, 20, 7, line_h, line_h );
assert!( !moved );
assert_eq!( s.first_line, 20 );
let ( s, moved ) = s.scrolled( 0, 20, 7, -line_h, line_h );
assert!( !moved );
assert_eq!( s.first_line, 0 );
}
#[ test ]
fn touchpad_motion_adds_up_to_whole_lines()
{
let line_h = 20.0;
let ( s, moved ) = TextScroll::default().scrolled( 0, 20, 0, 12.0, line_h );
assert!( !moved );
assert_eq!( s.remainder, 12.0 );
let ( s, moved ) = s.scrolled( 0, 20, 0, 12.0, line_h );
assert!( moved );
assert_eq!( ( s.first_line, s.remainder ), ( 1, 4.0 ) );
let ( s, moved ) = s.scrolled( 1, 20, 0, -24.0, line_h );
assert!( moved );
assert_eq!( s.first_line, 0 );
assert_eq!( s.remainder, 0.0 );
}
}