use std::sync::{ Arc, Mutex, MutexGuard, PoisonError };
use crate::time::Instant;
use crate::types::Rect;
use crate::widget::scroll::offset_into_view;
use super::theme::{ CLOSE_HOLD, SLIDE };
#[ derive( Clone, Default ) ]
pub struct TabMotion( Arc<Mutex<Motion>> );
impl TabMotion
{
pub fn new() -> Self
{
Self::default()
}
pub fn is_animating( &self ) -> bool
{
self.lock().is_animating( Instant::now() )
}
pub( crate ) fn set_origin( &self, origin: ( f32, f32 ) )
{
self.lock().origin = origin;
}
pub fn drag_move( &self, x: f32, y: f32 ) -> bool
{
self.lock().drag_move( x, y, Instant::now() )
}
pub fn drag_end( &self ) -> Option<( usize, usize )>
{
self.lock().drag_end( Instant::now() )
}
pub( crate ) fn arrange( &self, keys: &[ u64 ], width: f32 ) -> Vec<Placed>
{
self.lock().arrange( keys, width, Instant::now() )
}
pub( crate ) fn placed( &self ) -> Vec<Placed>
{
self.lock().placed.clone()
}
pub( crate ) fn lay( &self, rect: Rect, gap: f32 ) -> ( Vec<Laid>, f32 )
{
self.lock().lay( rect, gap, Instant::now() )
}
pub( crate ) fn scroll( &self, selected: Option<u64>, offset: f32, viewport: f32, step: f32 ) -> Option<f32>
{
self.lock().scroll( selected, offset, viewport, step, Instant::now() )
}
pub( crate ) fn lay_cards( &self, keys: &[ u64 ], rect: Rect, columns: usize, cell: ( f32, f32 ), gap: f32 ) -> Vec<Laid>
{
self.lock().lay_cards( keys, rect, columns, cell, gap, Instant::now() )
}
fn lock( &self ) -> MutexGuard<'_, Motion>
{
self.0.lock().unwrap_or_else( PoisonError::into_inner )
}
}
#[ derive( Clone, Copy, Debug, PartialEq ) ]
pub( crate ) struct Placed
{
pub key: Option<u64>,
pub width: f32,
pub presence: f32,
pub shift: ( f32, f32 ),
}
#[ derive( Clone, Copy, Debug, PartialEq ) ]
pub( crate ) struct Laid
{
pub key: u64,
pub rect: Rect,
}
#[ derive( Clone, Copy ) ]
struct Slot
{
key: u64,
gone: bool,
from: f32,
to: f32,
start: Instant,
full: f32,
shift_from: ( f32, f32 ),
shift_start: Instant,
}
fn eased( start: Instant, now: Instant ) -> f32
{
let t = ( now.saturating_duration_since( start ).as_secs_f32() / SLIDE.as_secs_f32() ).min( 1.0 );
1.0 - ( 1.0 - t ).powi( 3 )
}
impl Slot
{
fn new( key: u64, now: Instant ) -> Self
{
Self { key, gone: false, from: 0.0, to: 0.0, start: now, full: 0.0, shift_from: ( 0.0, 0.0 ), shift_start: now }
}
fn width( &self, now: Instant ) -> f32
{
self.from + ( self.to - self.from ) * eased( self.start, now )
}
fn shift( &self, now: Instant ) -> ( f32, f32 )
{
let left = 1.0 - eased( self.shift_start, now );
( self.shift_from.0 * left, self.shift_from.1 * left )
}
fn moving( &self, now: Instant ) -> bool
{
( self.from != self.to && now < self.start + SLIDE ) || ( self.shift_from != ( 0.0, 0.0 ) && now < self.shift_start + SLIDE )
}
fn retarget( &mut self, to: f32, now: Instant )
{
if ( self.to - to ).abs() < 0.5 { return; }
self.from = self.width( now );
self.to = to;
self.start = now;
self.full = to;
}
fn snap( &mut self, to: f32 )
{
self.from = to;
self.to = to;
self.full = to;
}
fn close( &mut self, now: Instant )
{
let width = self.width( now );
self.gone = true;
self.from = width;
self.to = 0.0;
self.start = now;
self.full = width;
}
fn slide( &mut self, by: ( f32, f32 ), now: Instant )
{
self.shift_from = by;
self.shift_start = now;
}
fn placed( &self, now: Instant ) -> Placed
{
let width = self.width( now ).max( 0.0 );
let presence = if self.full > 0.0 { ( width / self.full ).clamp( 0.0, 1.0 ) } else { 0.0 };
Placed { key: ( !self.gone ).then_some( self.key ), width, presence, shift: self.shift( now ) }
}
}
struct Drag
{
key: u64,
grab: ( f32, f32 ),
pointer: ( f32, f32 ),
}
#[ derive( Clone, Copy, Default ) ]
enum Shape
{
#[ default ]
Strip,
Grid { columns: usize, cell: ( f32, f32 ) },
}
#[ derive( Default ) ]
struct Motion
{
slots: Vec<Slot>,
keys: Vec<u64>,
hold: Option<Instant>,
settled: bool,
placed: Vec<Placed>,
drag: Option<Drag>,
rect: Rect,
gap: f32,
shape: Shape,
laid: Vec<Laid>,
revealed: Option<u64>,
revealing: bool,
origin: ( f32, f32 ),
}
fn offsets( placed: &[ Placed ], gap: f32 ) -> Vec<f32>
{
let mut x = 0.0;
placed.iter().enumerate().map( |( i, slot )|
{
if i > 0 { x += gap * placed[ i - 1 ].presence; }
let at = x;
x += slot.width;
at
} ).collect()
}
impl Motion
{
fn is_animating( &self, now: Instant ) -> bool
{
self.drag.is_some() || self.hold.is_some() || self.slots.iter().any( |slot| slot.moving( now ) )
}
fn arrange( &mut self, keys: &[ u64 ], width: f32, now: Instant ) -> Vec<Placed>
{
let changed = keys != self.keys.as_slice();
if changed
{
if self.reconcile( keys, now ) { self.hold = Some( now + CLOSE_HOLD ); }
self.keys = keys.to_vec();
}
self.slots.retain( |slot| !slot.gone || slot.moving( now ) );
let holding = self.hold.is_some_and( |until| now < until );
let release = self.hold.is_some() && !holding;
if release { self.hold = None; }
let held = self.slots.iter().find( |slot| !slot.gone && slot.to > 0.0 ).map_or( width, |slot| slot.to );
for slot in self.slots.iter_mut().filter( |slot| !slot.gone )
{
let fresh = slot.to == 0.0;
if !self.settled
{
slot.snap( width );
} else if fresh {
slot.retarget( if holding { held } else { width }, now );
} else if holding {
continue;
} else if changed || release || slot.moving( now ) {
slot.retarget( width, now );
} else {
slot.snap( width );
}
}
self.settled = true;
self.refresh( now );
self.placed.clone()
}
fn refresh( &mut self, now: Instant )
{
self.placed = self.slots.iter().map( |slot| slot.placed( now ) ).collect();
}
fn reconcile( &mut self, keys: &[ u64 ], now: Instant ) -> bool
{
let mut closed = false;
for slot in self.slots.iter_mut().filter( |slot| !slot.gone && !keys.contains( &slot.key ) )
{
slot.close( now );
closed = true;
}
if self.drag.as_ref().is_some_and( |drag| !keys.contains( &drag.key ) ) { self.drag = None; }
let present: Vec<u64> = self.slots.iter().filter( |slot| !slot.gone ).map( |slot| slot.key ).collect();
if !keys.iter().filter( |key| present.contains( key ) ).eq( present.iter() )
{
self.slots.retain( |slot| !slot.gone );
self.slots.sort_by_key( |slot| keys.iter().position( |&key| key == slot.key ) );
}
for ( idx, &key ) in keys.iter().enumerate()
{
if self.slots.iter().any( |slot| !slot.gone && slot.key == key ) { continue; }
let at = match idx.checked_sub( 1 ).map( |before| keys[ before ] )
{
Some( before ) => self.slots.iter().position( |slot| !slot.gone && slot.key == before ).map_or( self.slots.len(), |at| at + 1 ),
None => 0,
};
self.slots.insert( at, Slot::new( key, now ) );
}
closed
}
fn natural( &self, placed: &[ Placed ] ) -> Vec<( f32, f32 )>
{
match self.shape
{
Shape::Strip => offsets( placed, self.gap ).into_iter().map( |x| ( x, 0.0 ) ).collect(),
Shape::Grid { columns, cell } =>
{
let mut n = 0;
placed.iter().map( |slot|
{
let at = ( ( n % columns ) as f32 * ( cell.0 + self.gap ), ( n / columns ) as f32 * ( cell.1 + self.gap ) );
if slot.key.is_some() { n += 1; }
at
} ).collect()
}
}
}
fn place( &mut self, size: impl Fn( &Placed ) -> ( f32, f32 ), bounds: Rect ) -> Vec<Laid>
{
let natural = self.natural( &self.placed );
let mut laid = Vec::new();
let mut lifted = None;
for ( slot, at ) in self.placed.iter().zip( natural )
{
let Some( key ) = slot.key else { continue };
let ( width, height ) = size( slot );
match &self.drag
{
Some( drag ) if drag.key == key =>
{
let x = ( drag.pointer.0 - drag.grab.0 ).min( bounds.x + bounds.width - width ).max( bounds.x );
let y = ( drag.pointer.1 - drag.grab.1 ).min( bounds.y + bounds.height - height ).max( bounds.y );
lifted = Some( Laid { key, rect: Rect { x, y, width, height } } );
}
_ =>
{
let rect = Rect { x: self.rect.x + at.0 + slot.shift.0, y: self.rect.y + at.1 + slot.shift.1, width, height };
laid.push( Laid { key, rect } );
}
}
}
laid.extend( lifted );
self.laid = laid.clone();
laid
}
fn lay( &mut self, rect: Rect, gap: f32, now: Instant ) -> ( Vec<Laid>, f32 )
{
self.rect = rect;
self.gap = gap;
self.shape = Shape::Strip;
if let Some( ( key, at ) ) = self.drag.as_ref().map( |drag| ( drag.key, ( drag.pointer.0 - drag.grab.0, drag.pointer.1 - drag.grab.1 ) ) )
{
self.follow( key, at, now );
}
let end = offsets( &self.placed, gap ).last().zip( self.placed.last() ).map_or( 0.0, |( at, slot )| at + slot.width );
let laid = self.place( |slot| ( slot.width, rect.height ), Rect { width: end, ..rect } );
( laid, rect.x + end )
}
fn lay_cards( &mut self, keys: &[ u64 ], rect: Rect, columns: usize, cell: ( f32, f32 ), gap: f32, now: Instant ) -> Vec<Laid>
{
if keys != self.keys.as_slice()
{
self.reconcile( keys, now );
self.keys = keys.to_vec();
}
self.slots.retain( |slot| !slot.gone );
self.rect = rect;
self.gap = gap;
self.shape = Shape::Grid { columns, cell };
self.settled = false;
self.refresh( now );
let rows = keys.len().div_ceil( columns ) as f32;
let height = rows * cell.1 + ( rows - 1.0 ).max( 0.0 ) * gap;
self.place( |_| cell, Rect { height, ..rect } )
}
fn scroll( &mut self, selected: Option<u64>, offset: f32, viewport: f32, step: f32, now: Instant ) -> Option<f32>
{
if let Some( drag ) = &self.drag
{
let lifted = self.laid.iter().find( |laid| laid.key == drag.key )?.rect;
if lifted.x + lifted.width >= viewport - 0.5 { return Some( offset + step ); }
if lifted.x <= 0.5 && offset > 0.0 { return Some( ( offset - step ).max( 0.0 ) ); }
return None;
}
if selected != self.revealed
{
self.revealed = selected;
self.revealing = true;
}
if !self.revealing { return None; }
if !self.is_animating( now ) { self.revealing = false; }
let at = self.placed.iter().position( |slot| slot.key.is_some() && slot.key == selected )?;
let x = offsets( &self.placed, self.gap )[ at ];
Some( offset_into_view( offset, x, self.placed[ at ].width, viewport ) )
}
fn drag_move( &mut self, x: f32, y: f32, now: Instant ) -> bool
{
let ( x, y ) = ( x - self.origin.0, y - self.origin.1 );
if self.drag.is_none()
{
let hit = self.laid.iter().find( |laid| x >= laid.rect.x && x < laid.rect.x + laid.rect.width && y >= laid.rect.y && y < laid.rect.y + laid.rect.height );
let Some( hit ) = hit else { return false };
self.drag = Some( Drag { key: hit.key, grab: ( x - hit.rect.x, y - hit.rect.y ), pointer: ( x, y ) } );
}
let Some( drag ) = self.drag.as_mut() else { return false };
drag.pointer = ( x, y );
let ( key, at ) = ( drag.key, ( x - drag.grab.0, y - drag.grab.1 ) );
self.follow( key, at, now );
true
}
fn follow( &mut self, key: u64, at: ( f32, f32 ), now: Instant )
{
let Some( from ) = self.slots.iter().position( |slot| !slot.gone && slot.key == key ) else { return };
let placed = self.slots.iter().map( |slot| slot.placed( now ) ).collect::<Vec<_>>();
let before = self.natural( &placed );
let to = self.target( from, at, &placed, &before );
if to == from { return; }
let was: Vec<( u64, ( f32, f32 ) )> = self.slots.iter().zip( before.iter().zip( &placed ) )
.map( |( slot, ( at, p ) )| ( slot.key, ( at.0 + p.shift.0, at.1 + p.shift.1 ) ) )
.collect();
let slot = self.slots.remove( from );
self.slots.insert( to, slot );
let after = self.natural( &self.slots.iter().map( |slot| slot.placed( now ) ).collect::<Vec<_>>() );
for ( slot, at ) in self.slots.iter_mut().zip( after )
{
if slot.key == key || slot.gone { continue; }
if let Some( &( _, old ) ) = was.iter().find( |( k, _ )| *k == slot.key ) { slot.slide( ( old.0 - at.0, old.1 - at.1 ), now ); }
}
self.refresh( now );
}
fn target( &self, from: usize, at: ( f32, f32 ), placed: &[ Placed ], natural: &[ ( f32, f32 ) ] ) -> usize
{
match self.shape
{
Shape::Strip =>
{
let centre = at.0 - self.rect.x + placed[ from ].width / 2.0;
let mut to = 0;
for ( j, i ) in ( 0..placed.len() ).filter( |&i| i != from ).enumerate()
{
if placed[ i ].key.is_some() && natural[ i ].0 + placed[ i ].width / 2.0 < centre { to = j + 1; }
}
to
}
Shape::Grid { columns, cell } =>
{
let rows = placed.len().div_ceil( columns );
let column = ( ( at.0 - self.rect.x + cell.0 / 2.0 ) / ( cell.0 + self.gap ) ).floor().clamp( 0.0, ( columns - 1 ) as f32 ) as usize;
let row = ( ( at.1 - self.rect.y + cell.1 / 2.0 ) / ( cell.1 + self.gap ) ).floor().clamp( 0.0, rows.saturating_sub( 1 ) as f32 ) as usize;
( row * columns + column ).min( placed.len() - 1 )
}
}
}
fn drag_end( &mut self, now: Instant ) -> Option<( usize, usize )>
{
let drag = self.drag.take()?;
let placed = self.slots.iter().map( |slot| slot.placed( now ) ).collect::<Vec<_>>();
let natural = self.natural( &placed );
let shown = self.laid.iter().find( |laid| laid.key == drag.key ).map( |laid| laid.rect );
let origin = ( self.rect.x, self.rect.y );
if let Some( ( slot, at ) ) = self.slots.iter_mut().zip( natural ).find( |( slot, _ )| !slot.gone && slot.key == drag.key )
{
if let Some( rect ) = shown { slot.slide( ( rect.x - origin.0 - at.0, rect.y - origin.1 - at.1 ), now ); }
}
self.refresh( now );
let to = self.slots.iter().filter( |slot| !slot.gone ).position( |slot| slot.key == drag.key )?;
let from = self.keys.iter().position( |&key| key == drag.key )?;
let key = self.keys.remove( from );
self.keys.insert( to, key );
( from != to ).then_some( ( from, to ) )
}
}
#[ cfg( test ) ]
mod tests
{
use std::time::Duration;
use super::*;
fn widths( placed: &[ Placed ] ) -> Vec<( Option<u64>, f32 )>
{
placed.iter().map( |p| ( p.key, p.width.round() ) ).collect()
}
#[ test ]
fn the_first_layout_does_not_move()
{
let mut motion = Motion::default();
let now = Instant::now();
assert_eq!( widths( &motion.arrange( &[ 1, 2 ], 100.0, now ) ), [ ( Some( 1 ), 100.0 ), ( Some( 2 ), 100.0 ) ] );
assert!( !motion.is_animating( now ) );
}
#[ test ]
fn a_new_tab_grows_and_the_others_narrow()
{
let mut motion = Motion::default();
let start = Instant::now();
motion.arrange( &[ 1 ], 200.0, start );
let placed = motion.arrange( &[ 1, 2 ], 100.0, start );
assert_eq!( widths( &placed ), [ ( Some( 1 ), 200.0 ), ( Some( 2 ), 0.0 ) ] );
assert!( motion.is_animating( start ) );
let end = start + SLIDE;
assert_eq!( widths( &motion.arrange( &[ 1, 2 ], 100.0, end ) ), [ ( Some( 1 ), 100.0 ), ( Some( 2 ), 100.0 ) ] );
}
#[ test ]
fn a_closed_tab_leaves_a_shrinking_gap_and_the_others_wait()
{
let mut motion = Motion::default();
let start = Instant::now();
motion.arrange( &[ 1, 2, 3 ], 100.0, start );
let placed = motion.arrange( &[ 1, 3 ], 150.0, start );
assert_eq!( widths( &placed ), [ ( Some( 1 ), 100.0 ), ( None, 100.0 ), ( Some( 3 ), 100.0 ) ] );
let gone = start + SLIDE;
assert_eq!( widths( &motion.arrange( &[ 1, 3 ], 150.0, gone ) ), [ ( Some( 1 ), 100.0 ), ( Some( 3 ), 100.0 ) ] );
assert!( motion.is_animating( gone ) );
let released = start + CLOSE_HOLD;
motion.arrange( &[ 1, 3 ], 150.0, released );
let after = released + SLIDE;
assert_eq!( widths( &motion.arrange( &[ 1, 3 ], 150.0, after ) ), [ ( Some( 1 ), 150.0 ), ( Some( 3 ), 150.0 ) ] );
assert!( !motion.is_animating( after + Duration::from_millis( 1 ) ) );
}
#[ test ]
fn another_close_restarts_the_wait()
{
let mut motion = Motion::default();
let start = Instant::now();
motion.arrange( &[ 1, 2, 3 ], 100.0, start );
motion.arrange( &[ 1, 3 ], 150.0, start );
let second = start + Duration::from_millis( 300 );
motion.arrange( &[ 1 ], 300.0, second );
let still = start + CLOSE_HOLD + SLIDE;
assert_eq!( motion.arrange( &[ 1 ], 300.0, still ).iter().find( |p| p.key == Some( 1 ) ).map( |p| p.width.round() ), Some( 100.0 ) );
}
#[ test ]
fn a_resize_alone_does_not_animate()
{
let mut motion = Motion::default();
let now = Instant::now();
motion.arrange( &[ 1, 2 ], 100.0, now );
assert_eq!( widths( &motion.arrange( &[ 1, 2 ], 120.0, now ) ), [ ( Some( 1 ), 120.0 ), ( Some( 2 ), 120.0 ) ] );
assert!( !motion.is_animating( now ) );
}
#[ test ]
fn a_dragged_tab_moves_where_its_centre_goes()
{
let mut motion = Motion::default();
let now = Instant::now();
let strip = Rect { x: 0.0, y: 0.0, width: 400.0, height: 30.0 };
motion.arrange( &[ 1, 2, 3 ], 100.0, now );
motion.lay( strip, 0.0, now );
assert!( !motion.drag_move( 50.0, 40.0, now ) );
assert!( motion.drag_move( 50.0, 10.0, now ) );
assert!( motion.drag_move( 260.0, 10.0, now ) );
let order: Vec<u64> = motion.lay( strip, 0.0, now ).0.iter().map( |laid| laid.key ).collect();
assert_eq!( order, [ 2, 3, 1 ] );
assert_eq!( motion.drag_end( now ), Some( ( 0, 2 ) ) );
assert!( motion.arrange( &[ 2, 3, 1 ], 100.0, now ).iter().all( |p| p.width == 100.0 ) );
}
#[ test ]
fn cards_fill_the_rows_in_order()
{
let mut motion = Motion::default();
let laid = motion.lay_cards( &[ 1, 2, 3 ], Rect { x: 0.0, y: 0.0, width: 210.0, height: 0.0 }, 2, ( 100.0, 80.0 ), 10.0, Instant::now() );
let corners: Vec<( u64, f32, f32 )> = laid.iter().map( |laid| ( laid.key, laid.rect.x, laid.rect.y ) ).collect();
assert_eq!( corners, [ ( 1, 0.0, 0.0 ), ( 2, 110.0, 0.0 ), ( 3, 0.0, 90.0 ) ] );
}
#[ test ]
fn a_dragged_card_takes_the_cell_under_its_centre()
{
let mut motion = Motion::default();
let now = Instant::now();
let grid = Rect { x: 0.0, y: 0.0, width: 210.0, height: 0.0 };
motion.lay_cards( &[ 1, 2, 3, 4 ], grid, 2, ( 100.0, 80.0 ), 10.0, now );
assert!( motion.drag_move( 20.0, 20.0, now ) );
assert!( motion.drag_move( 130.0, 110.0, now ) );
let order: Vec<u64> = motion.lay_cards( &[ 1, 2, 3, 4 ], grid, 2, ( 100.0, 80.0 ), 10.0, now ).iter().map( |laid| laid.key ).collect();
assert_eq!( order, [ 2, 3, 4, 1 ] );
assert_eq!( motion.drag_end( now ), Some( ( 0, 3 ) ) );
}
#[ test ]
fn the_selected_tab_is_scrolled_into_view_once()
{
let mut motion = Motion::default();
let now = Instant::now();
let keys: Vec<u64> = ( 1..=10 ).collect();
motion.arrange( &keys, 100.0, now );
motion.lay( Rect { x: 0.0, y: 0.0, width: 1000.0, height: 30.0 }, 0.0, now );
assert_eq!( motion.scroll( Some( 8 ), 0.0, 300.0, 10.0, now ), Some( 500.0 ) );
assert_eq!( motion.scroll( Some( 8 ), 0.0, 300.0, 10.0, now ), None );
assert_eq!( motion.scroll( Some( 2 ), 500.0, 300.0, 10.0, now ), Some( 100.0 ) );
}
#[ test ]
fn a_tab_dragged_against_the_edge_scrolls_the_strip()
{
let mut motion = Motion::default();
let now = Instant::now();
let keys: Vec<u64> = ( 1..=10 ).collect();
motion.arrange( &keys, 100.0, now );
motion.lay( Rect { x: 0.0, y: 0.0, width: 1000.0, height: 30.0 }, 0.0, now );
assert!( motion.drag_move( 250.0, 10.0, now ) );
assert!( motion.drag_move( 290.0, 10.0, now ) );
motion.lay( Rect { x: 0.0, y: 0.0, width: 1000.0, height: 30.0 }, 0.0, now );
assert_eq!( motion.scroll( None, 0.0, 300.0, 10.0, now ), Some( 10.0 ) );
}
}