stacking: Tab animations

This commit is contained in:
Victoria Brekenfeld 2023-06-26 21:05:31 +02:00
parent fcc4cf231f
commit 67832f5cad
3 changed files with 146 additions and 21 deletions

View file

@ -590,10 +590,16 @@ impl Program for CosmicStackInternal {
.into(),
CosmicElement::new(
Tabs::new(
windows
.iter()
.enumerate()
.map(|(i, w)| Tab::new(w.title(), w.app_id()).on_close(Message::Close(i))),
windows.iter().enumerate().map(|(i, w)| {
let user_data = w.user_data();
user_data.insert_if_missing(Id::unique);
Tab::new(
w.title(),
w.app_id(),
user_data.get::<Id>().unwrap().clone(),
)
.on_close(Message::Close(i))
}),
active,
windows[active].is_activated(false),
group_focused,

View file

@ -14,7 +14,7 @@ use cosmic::{
widget::{
operation::{Operation, OperationOutputWrapper},
tree::Tree,
Widget,
Id, Widget,
},
Clipboard, Color, Length, Rectangle, Shell, Size,
},
@ -114,6 +114,7 @@ pub trait TabMessage {
}
pub struct Tab<'a, Message: TabMessage> {
id: Id,
app_icon: Icon<'a>,
title: String,
font: Font,
@ -124,8 +125,9 @@ pub struct Tab<'a, Message: TabMessage> {
}
impl<'a, Message: TabMessage> Tab<'a, Message> {
pub fn new(title: impl Into<String>, app_id: impl Into<String>) -> Self {
pub fn new(title: impl Into<String>, app_id: impl Into<String>, id: Id) -> Self {
Tab {
id,
app_icon: icon(app_id.into(), 16),
title: title.into(),
font: cosmic::font::FONT,
@ -223,6 +225,7 @@ impl<'a, Message: TabMessage> Tab<'a, Message> {
);
TabInternal {
id: self.id,
idx,
active: self.active,
background: self.background_theme.into(),
@ -239,6 +242,7 @@ const TEXT_BREAKPOINT: i32 = 44;
const CLOSE_BREAKPOINT: i32 = 125;
pub(super) struct TabInternal<'a, Message: TabMessage, Renderer> {
id: Id,
idx: usize,
active: bool,
background: theme::Container,
@ -251,6 +255,10 @@ where
Renderer::Theme: ContainerStyleSheet<Style = theme::Container>,
Message: TabMessage,
{
fn id(&self) -> Option<Id> {
Some(self.id.clone())
}
fn children(&self) -> Vec<Tree> {
self.elements.iter().map(Tree::new).collect()
}

View file

@ -27,7 +27,10 @@ use cosmic::{
widget::{icon, Icon},
};
use cosmic_time::{Cubic, Ease, Tween};
use std::time::{Duration, Instant};
use std::{
collections::{HashMap, HashSet, VecDeque},
time::{Duration, Instant},
};
pub struct Tabs<'a, Message, Renderer>
where
@ -49,12 +52,21 @@ struct ScrollAnimationState {
end: Offset,
}
#[derive(Debug, Clone)]
struct TabAnimationState {
previous_bounds: HashMap<Id, Rectangle>,
next_bounds: HashMap<Id, Rectangle>,
start_time: Instant,
}
/// The local state of [`Tabs`].
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone)]
pub struct State {
offset_x: Offset,
scroll_animation: Option<ScrollAnimationState>,
scroll_to: Option<usize>,
last_state: Option<HashMap<Id, Rectangle>>,
tab_animations: VecDeque<TabAnimationState>,
}
impl Scrollable for State {
@ -79,6 +91,8 @@ impl Default for State {
offset_x: Offset::Absolute(0.),
scroll_animation: None,
scroll_to: None,
last_state: None,
tab_animations: VecDeque::new(),
}
}
}
@ -98,7 +112,8 @@ impl Offset {
}
}
const ANIMATION_DURATION: Duration = Duration::from_millis(200);
const SCROLL_ANIMATION_DURATION: Duration = Duration::from_millis(200);
const TAB_ANIMATION_DURATION: Duration = Duration::from_millis(150);
impl<'a, Message, Renderer> Tabs<'a, Message, Renderer>
where
@ -234,7 +249,7 @@ impl State {
let percentage = (Instant::now()
.duration_since(animation.start_time)
.as_millis() as f32
/ ANIMATION_DURATION.as_millis() as f32)
/ SCROLL_ANIMATION_DURATION.as_millis() as f32)
.min(1.0);
Ease::Cubic(Cubic::InOut).tween(percentage)
@ -257,10 +272,19 @@ impl State {
pub fn cleanup_old_animations(&mut self) {
if let Some(animation) = self.scroll_animation.as_ref() {
if Instant::now().duration_since(animation.start_time) > ANIMATION_DURATION {
if Instant::now().duration_since(animation.start_time) > SCROLL_ANIMATION_DURATION {
self.scroll_animation.take();
}
}
if let Some(animation) = self.tab_animations.front() {
if Instant::now().duration_since(animation.start_time) > TAB_ANIMATION_DURATION {
self.tab_animations.pop_front();
if let Some(next_animation) = self.tab_animations.front_mut() {
next_animation.start_time = Instant::now();
}
}
}
}
}
@ -503,25 +527,79 @@ where
renderer.with_layer(bounds, |renderer| {
renderer.with_translation(Vector::new(-offset.x, -offset.y), |renderer| {
for ((tab, state), layout) in self.elements[2..self.elements.len() - 3]
let percentage = if let Some(animation) = state.tab_animations.front() {
let percentage = (Instant::now()
.duration_since(animation.start_time)
.as_millis() as f32
/ TAB_ANIMATION_DURATION.as_millis() as f32)
.min(1.0);
Ease::Cubic(Cubic::Out).tween(percentage)
} else {
1.0
};
for ((tab, wstate), layout) in self.elements[2..self.elements.len() - 3]
.iter()
.zip(tree.children.iter().skip(2))
.zip(layout.children().skip(2))
{
let bounds = if let Some(animation) = state.tab_animations.front() {
let id = tab.as_widget().id().unwrap();
let previous =
animation
.previous_bounds
.get(&id)
.copied()
.unwrap_or(Rectangle {
x: layout.position().x,
y: layout.position().y,
width: 0.,
height: layout.bounds().height,
});
let next = animation
.next_bounds
.get(&id)
.copied()
.unwrap_or(Rectangle {
x: layout.position().x,
y: layout.position().y,
width: 0.,
height: layout.bounds().height,
});
Rectangle {
x: previous.x + (next.x - previous.x) * percentage,
y: previous.y + (next.y - previous.y) * percentage,
width: previous.width + (next.width - previous.width) * percentage,
height: next.height,
}
} else {
layout.bounds()
};
let cursor = match cursor {
mouse::Cursor::Available(point) => mouse::Cursor::Available(point + offset),
mouse::Cursor::Unavailable => mouse::Cursor::Unavailable,
};
tab.as_widget().draw(
state,
renderer,
theme,
style,
layout,
cursor,
&offset_viewport,
);
renderer.with_layer(bounds, |renderer| {
renderer.with_translation(
Vector {
x: bounds.x - layout.position().x,
y: bounds.y - layout.position().y,
},
|renderer| {
tab.as_widget().draw(
wstate,
renderer,
theme,
style,
layout,
cursor,
&offset_viewport,
);
},
)
})
}
});
});
@ -610,6 +688,39 @@ where
let state = tree.state.downcast_mut::<State>();
state.cleanup_old_animations();
let current_state = self.elements[2..self.elements.len() - 3]
.iter()
.zip(layout.children().skip(2))
.map(|(element, layout)| (element.as_widget().id().unwrap(), layout.bounds()))
.collect::<HashMap<Id, Rectangle>>();
if state.last_state.is_none() {
state.last_state = Some(current_state.clone());
}
let last_state = state.last_state.as_mut().unwrap();
let unknown_keys = current_state
.keys()
.collect::<HashSet<_>>()
.symmetric_difference(&last_state.keys().collect::<HashSet<_>>())
.next()
.is_some();
if unknown_keys
|| current_state.iter().any(|(a_id, a_bounds)| {
let Some(b_bounds) = last_state.get(a_id) else { return true };
a_bounds != b_bounds
})
{
// new tab_animation
state.tab_animations.push_back(TabAnimationState {
previous_bounds: last_state.clone(),
next_bounds: current_state.clone(),
start_time: Instant::now(),
});
// update last_state
*last_state = current_state;
}
let mut bounds = layout.bounds();
let content_bounds = layout.children().fold(Size::new(0., 0.), |a, b| Size {
width: a.width + b.bounds().width,