208 lines
7.5 KiB
Rust
208 lines
7.5 KiB
Rust
//! Interpolated chrome geometry is shared by painting and pointer hit testing.
|
|
use super::*;
|
|
use std::time::{Duration, Instant};
|
|
|
|
#[derive(Clone, Default)]
|
|
pub struct Visual {
|
|
pub rects: Vec<(u64, Rect)>,
|
|
pub exiting: Vec<(Tab, Rect)>,
|
|
pub viewport: Rect,
|
|
pub plus: Rect,
|
|
pub hovers: Vec<(u64, f32, f32)>,
|
|
pub plus_hover: f32,
|
|
}
|
|
impl Visual {
|
|
pub fn hover(&self, id: u64) -> f32 {
|
|
self.hovers
|
|
.iter()
|
|
.find(|entry| entry.0 == id)
|
|
.map_or(0.0, |entry| entry.1)
|
|
}
|
|
pub fn close_hover(&self, id: u64) -> f32 {
|
|
self.hovers
|
|
.iter()
|
|
.find(|entry| entry.0 == id)
|
|
.map_or(0.0, |entry| entry.2)
|
|
}
|
|
pub fn rect(&self, id: u64, fallback: Rect) -> Rect {
|
|
self.rects
|
|
.iter()
|
|
.find(|(key, _)| *key == id)
|
|
.map_or(fallback, |(_, rect)| *rect)
|
|
}
|
|
pub fn hit(&self, point: Point) -> Option<(u64, bool)> {
|
|
if !self.viewport.contains(point) {
|
|
return None;
|
|
}
|
|
self.rects
|
|
.iter()
|
|
.rev()
|
|
.find(|(_, rect)| pill_rect(*rect).contains(point))
|
|
.map(|(id, rect)| (*id, close_rect(*rect).contains(point)))
|
|
}
|
|
}
|
|
#[derive(Default)]
|
|
pub struct Motion {
|
|
hover: super::hover::Hover,
|
|
target: Visual,
|
|
from: Visual,
|
|
current: Visual,
|
|
published: Vec<Tab>,
|
|
initialized: bool,
|
|
direct_scroll: bool,
|
|
started: Option<Instant>,
|
|
pub offset: Option<f32>,
|
|
pub next_frame: Option<Instant>,
|
|
}
|
|
fn blend(a: Rect, b: Rect, t: f32) -> Rect {
|
|
Rect::new(
|
|
a.x + (b.x - a.x) * t,
|
|
a.y + (b.y - a.y) * t,
|
|
a.width + (b.width - a.width) * t,
|
|
a.height + (b.height - a.height) * t,
|
|
)
|
|
}
|
|
impl Motion {
|
|
pub fn sample_hover(&mut self, visual: &mut Visual, tabs: &[Tab], pointer: Option<Point>) {
|
|
let now = Instant::now();
|
|
if self.hover.sample(visual, tabs, pointer, now) {
|
|
let next = now + Duration::from_millis(16);
|
|
self.next_frame = Some(self.next_frame.map_or(next, |old| old.min(next)));
|
|
}
|
|
}
|
|
pub fn scroll_to(&mut self, offset: f32) {
|
|
self.offset = Some(offset);
|
|
self.direct_scroll = true;
|
|
}
|
|
|
|
fn advance(&mut self, now: Instant) {
|
|
let t = self.started.map_or(1.0, |start| {
|
|
(now.duration_since(start).as_secs_f32() / 0.22).min(1.0)
|
|
});
|
|
let eased = 1.0 - (1.0 - t).powi(3);
|
|
self.current = Visual {
|
|
rects: self
|
|
.target
|
|
.rects
|
|
.iter()
|
|
.map(|(id, target)| (*id, blend(self.from.rect(*id, *target), *target, eased)))
|
|
.collect(),
|
|
exiting: self
|
|
.target
|
|
.exiting
|
|
.iter()
|
|
.filter(|_| t < 1.0)
|
|
.map(|(tab, target)| {
|
|
let from = self
|
|
.from
|
|
.exiting
|
|
.iter()
|
|
.find(|(old, _)| old.id == tab.id)
|
|
.map_or(*target, |(_, rect)| *rect);
|
|
(tab.clone(), blend(from, *target, eased))
|
|
})
|
|
.collect(),
|
|
viewport: blend(self.from.viewport, self.target.viewport, eased),
|
|
plus: blend(self.from.plus, self.target.plus, eased),
|
|
..Visual::default()
|
|
};
|
|
self.next_frame = (t < 1.0).then(|| now + Duration::from_millis(16));
|
|
}
|
|
|
|
pub fn sample(&mut self, tabs: &[Tab], offset: Option<f32>, width: f32) -> Visual {
|
|
let now = Instant::now();
|
|
// Retarget from the geometry at this instant, even during rapid repeated clicks.
|
|
self.advance(now);
|
|
let mut offset = scroll_offset(tabs, width, offset.or(self.offset));
|
|
let selection_changed = self
|
|
.published
|
|
.iter()
|
|
.find(|tab| tab.active)
|
|
.map(|tab| tab.id)
|
|
!= tabs.iter().find(|tab| tab.active).map(|tab| tab.id);
|
|
if selection_changed {
|
|
let active = tabs.iter().position(|tab| tab.active).unwrap_or(0) as f32;
|
|
let tab_width = tab_width(tabs, width);
|
|
let left = active * tab_width;
|
|
let right = left + tab_width;
|
|
offset = offset.min(left).max(right - viewport(width).width).max(0.0);
|
|
}
|
|
self.offset = Some(offset);
|
|
let mut target = Visual {
|
|
rects: tab_rects(Some(offset), tabs, width),
|
|
viewport: viewport(width),
|
|
plus: plus_rect(Some(offset), tabs, width),
|
|
..Visual::default()
|
|
};
|
|
if !self.initialized || self.direct_scroll {
|
|
self.direct_scroll = false;
|
|
self.started = None;
|
|
self.next_frame = None;
|
|
// Startup is already laid out; it is not an insertion of every restored tab.
|
|
self.initialized = true;
|
|
self.current = target.clone();
|
|
self.from = target.clone();
|
|
self.target = target;
|
|
self.published = tabs.to_vec();
|
|
return self.current.clone();
|
|
}
|
|
if self.target.rects != target.rects
|
|
|| self.target.plus != target.plus
|
|
|| self.target.viewport != target.viewport
|
|
{
|
|
self.from = self.current.clone();
|
|
for (index, (id, rect)) in target.rects.iter().enumerate() {
|
|
if !self.from.rects.iter().any(|(old, _)| old == id) {
|
|
// Grow out of the previous trailing edge, not the final compressed x.
|
|
let x = target.rects[..index]
|
|
.iter()
|
|
.rev()
|
|
.find_map(|(previous, _)| {
|
|
self.from
|
|
.rects
|
|
.iter()
|
|
.find(|(old, _)| old == previous)
|
|
.map(|(_, previous)| previous.x + previous.width)
|
|
})
|
|
.unwrap_or(rect.x);
|
|
self.from
|
|
.rects
|
|
.push((*id, Rect::new(x, rect.y, 0.0, rect.height)));
|
|
}
|
|
}
|
|
for tab in &self.published {
|
|
if !tabs.iter().any(|live| live.id == tab.id) {
|
|
self.from
|
|
.exiting
|
|
.push((tab.clone(), self.current.rect(tab.id, Rect::default())));
|
|
}
|
|
}
|
|
// A closed tab shrinks with the gap; it never remains an interactive target.
|
|
target.exiting = self
|
|
.from
|
|
.exiting
|
|
.iter()
|
|
.filter(|(tab, _)| !tabs.iter().any(|live| live.id == tab.id))
|
|
.map(|(tab, rect)| {
|
|
let next = self
|
|
.published
|
|
.iter()
|
|
.position(|old| old.id == tab.id)
|
|
.and_then(|index| {
|
|
self.published[index + 1..]
|
|
.iter()
|
|
.find(|old| tabs.iter().any(|live| live.id == old.id))
|
|
});
|
|
let x = next.map_or(target.plus.x - 4.0, |next| target.rect(next.id, *rect).x);
|
|
(tab.clone(), Rect::new(x, rect.y, 0.0, rect.height))
|
|
})
|
|
.collect();
|
|
self.target = target;
|
|
self.started = Some(now);
|
|
}
|
|
self.published = tabs.to_vec();
|
|
self.advance(now);
|
|
self.current.clone()
|
|
}
|
|
}
|