feat: animate scrolling workspace viewports
This commit is contained in:
1 parent
f0862348c9
commit
ed529ae900
9 files changed
+623
-52
No files matched your search
+100
-1
@@ -7,7 +7,7 @@ use std::path::PathBuf;
|
||||
|
||||
use ferese_animation::SpringConfig;
|
||||
use ferese_core::LayoutMode;
|
||||
use ferese_layout::ColumnWidth;
|
||||
use ferese_layout::{ColumnWidth, ViewportFocusStrategy};
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
@@ -30,6 +30,8 @@ struct AnimationsConfig {
|
||||
speed: f64,
|
||||
#[serde(default)]
|
||||
spring: SpringSettings,
|
||||
#[serde(default)]
|
||||
viewport_spring: ViewportSpringSettings,
|
||||
}
|
||||
|
||||
impl Default for AnimationsConfig {
|
||||
@@ -39,6 +41,27 @@ impl Default for AnimationsConfig {
|
||||
reduced_motion: false,
|
||||
speed: 1.0,
|
||||
spring: SpringSettings::default(),
|
||||
viewport_spring: ViewportSpringSettings::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ViewportSpringSettings {
|
||||
#[serde(default = "default_viewport_mass")]
|
||||
mass: f64,
|
||||
#[serde(default = "default_viewport_stiffness")]
|
||||
stiffness: f64,
|
||||
#[serde(default = "default_viewport_damping_ratio")]
|
||||
damping_ratio: f64,
|
||||
}
|
||||
|
||||
impl Default for ViewportSpringSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
mass: default_viewport_mass(),
|
||||
stiffness: default_viewport_stiffness(),
|
||||
damping_ratio: default_viewport_damping_ratio(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,10 +101,18 @@ enum LayoutModeValue {
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
struct ScrollingConfig {
|
||||
default_column_width: Option<ColumnWidthValue>,
|
||||
focus_strategy: Option<FocusStrategyValue>,
|
||||
#[serde(default)]
|
||||
width_presets: Vec<ColumnWidthValue>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum FocusStrategyValue {
|
||||
Minimal,
|
||||
CenterOnFocus,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum ColumnWidthValue {
|
||||
@@ -187,6 +218,17 @@ impl Config {
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn scrolling_focus_strategy(&self) -> ViewportFocusStrategy {
|
||||
match self
|
||||
.scrolling
|
||||
.focus_strategy
|
||||
.unwrap_or(FocusStrategyValue::Minimal)
|
||||
{
|
||||
FocusStrategyValue::Minimal => ViewportFocusStrategy::Minimal,
|
||||
FocusStrategyValue::CenterOnFocus => ViewportFocusStrategy::Center,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn animations_enabled(&self) -> bool {
|
||||
self.animations.enabled && !self.animations.reduced_motion
|
||||
}
|
||||
@@ -214,6 +256,26 @@ impl Config {
|
||||
..SpringConfig::default()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn viewport_spring_config(&self) -> Result<SpringConfig, ConfigError> {
|
||||
let mass =
|
||||
positive_animation_value(self.animations.viewport_spring.mass, "viewport_spring.mass")?;
|
||||
let stiffness = positive_animation_value(
|
||||
self.animations.viewport_spring.stiffness,
|
||||
"viewport_spring.stiffness",
|
||||
)?;
|
||||
let damping_ratio = positive_animation_value(
|
||||
self.animations.viewport_spring.damping_ratio,
|
||||
"viewport_spring.damping_ratio",
|
||||
)?;
|
||||
|
||||
Ok(SpringConfig {
|
||||
mass,
|
||||
stiffness,
|
||||
damping: 2.0 * damping_ratio * (stiffness * mass).sqrt(),
|
||||
..SpringConfig::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn positive_animation_value(value: f64, field: &'static str) -> Result<f64, ConfigError> {
|
||||
@@ -244,6 +306,18 @@ const fn default_spring_damping() -> f64 {
|
||||
53.0
|
||||
}
|
||||
|
||||
const fn default_viewport_mass() -> f64 {
|
||||
1.0
|
||||
}
|
||||
|
||||
const fn default_viewport_stiffness() -> f64 {
|
||||
320.0
|
||||
}
|
||||
|
||||
const fn default_viewport_damping_ratio() -> f64 {
|
||||
1.0
|
||||
}
|
||||
|
||||
fn parse_column_width(
|
||||
value: &ColumnWidthValue,
|
||||
field: &'static str,
|
||||
@@ -323,6 +397,21 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_scrolling_focus_strategy() {
|
||||
let minimal = parse("");
|
||||
let centered = parse("[scrolling]\nfocus_strategy = \"center_on_focus\"");
|
||||
|
||||
assert_eq!(
|
||||
minimal.scrolling_focus_strategy(),
|
||||
ViewportFocusStrategy::Minimal
|
||||
);
|
||||
assert_eq!(
|
||||
centered.scrolling_focus_strategy(),
|
||||
ViewportFocusStrategy::Center
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_non_positive_or_unknown_column_widths() {
|
||||
let zero = parse("[scrolling]\ndefault_column_width = 0.0");
|
||||
@@ -359,4 +448,14 @@ mod tests {
|
||||
assert!(speed.animation_speed().is_err());
|
||||
assert!(damping.spring_config().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derives_critical_viewport_damping() {
|
||||
let config = parse("");
|
||||
let spring = config.viewport_spring_config().unwrap();
|
||||
|
||||
assert_eq!(spring.mass, 1.0);
|
||||
assert_eq!(spring.stiffness, 320.0);
|
||||
assert!((spring.damping - 35.777_087_64).abs() < 0.000_001);
|
||||
}
|
||||
}
|
||||
@@ -29,10 +29,12 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
let runtime = RuntimeConfig {
|
||||
layout_mode: config.layout_mode(),
|
||||
default_column_width: config.default_column_width()?,
|
||||
scrolling_focus_strategy: config.scrolling_focus_strategy(),
|
||||
column_width_presets: config.width_presets()?,
|
||||
animations_enabled: config.animations_enabled(),
|
||||
animation_speed: config.animation_speed()?,
|
||||
spring_config: config.spring_config()?,
|
||||
viewport_spring_config: config.viewport_spring_config()?,
|
||||
};
|
||||
let mut state = Ferese::new(&mut event_loop, display, runtime)?;
|
||||
backends::init(launch.backend, &mut event_loop, &mut state)?;
|
||||
|
||||
+94
-6
@@ -6,12 +6,14 @@ use std::{
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use ferese_animation::{ClientSize, SpringConfig, WindowGeometry};
|
||||
use ferese_animation::{AnimatedValue, ClientSize, SpringConfig, WindowGeometry};
|
||||
use ferese_core::{
|
||||
LayoutMode, OutputGeometry, OutputId, OutputWorkspaceMap, WindowPlacement, WorkspaceSet,
|
||||
LayoutMode, OutputGeometry, OutputId, OutputWorkspaceMap, WindowPlacement, WorkspaceId,
|
||||
WorkspaceSet,
|
||||
};
|
||||
use ferese_layout::{
|
||||
Axis, ColumnWidth, Direction, GapConfig, LayoutResult, Rect, SizeConstraints, WindowId,
|
||||
Axis, ColumnWidth, Direction, GapConfig, LayoutResult, Rect, SizeConstraints,
|
||||
ViewportFocusStrategy, WindowId,
|
||||
};
|
||||
|
||||
use smithay::{
|
||||
@@ -84,11 +86,14 @@ pub struct Ferese {
|
||||
output_identity_ids: HashMap<String, OutputId>,
|
||||
pub window_ids: HashMap<Window, WindowId>,
|
||||
pub window_geometry: HashMap<WindowId, WindowGeometry>,
|
||||
viewport_animations: HashMap<WorkspaceId, AnimatedValue>,
|
||||
scrolling_world_x: HashMap<WindowId, (WorkspaceId, AnimatedValue)>,
|
||||
pub focused_window: Option<WindowId>,
|
||||
column_width_presets: Vec<ColumnWidth>,
|
||||
animations_enabled: bool,
|
||||
animation_speed: f64,
|
||||
spring_config: SpringConfig,
|
||||
viewport_spring_config: SpringConfig,
|
||||
pub cursor_status: CursorImageStatus,
|
||||
pub(crate) cursor_theme: xcursor::CursorTheme,
|
||||
pub(crate) named_cursors: HashMap<CursorIcon, crate::cursor::NamedCursor>,
|
||||
@@ -133,10 +138,12 @@ pub struct Ferese {
|
||||
pub struct RuntimeConfig {
|
||||
pub layout_mode: LayoutMode,
|
||||
pub default_column_width: ColumnWidth,
|
||||
pub scrolling_focus_strategy: ViewportFocusStrategy,
|
||||
pub column_width_presets: Vec<ColumnWidth>,
|
||||
pub animations_enabled: bool,
|
||||
pub animation_speed: f64,
|
||||
pub spring_config: SpringConfig,
|
||||
pub viewport_spring_config: SpringConfig,
|
||||
}
|
||||
|
||||
impl Ferese {
|
||||
@@ -342,17 +349,24 @@ impl Ferese {
|
||||
display_handle,
|
||||
loop_signal: event_loop.get_signal(),
|
||||
space: Space::default(),
|
||||
workspaces: WorkspaceSet::new(config.layout_mode, config.default_column_width),
|
||||
workspaces: WorkspaceSet::new(
|
||||
config.layout_mode,
|
||||
config.default_column_width,
|
||||
config.scrolling_focus_strategy,
|
||||
),
|
||||
output_workspaces: OutputWorkspaceMap::default(),
|
||||
output_ids: HashMap::new(),
|
||||
output_identity_ids: HashMap::new(),
|
||||
window_ids: HashMap::new(),
|
||||
window_geometry: HashMap::new(),
|
||||
viewport_animations: HashMap::new(),
|
||||
scrolling_world_x: HashMap::new(),
|
||||
focused_window: None,
|
||||
column_width_presets: config.column_width_presets,
|
||||
animations_enabled: config.animations_enabled,
|
||||
animation_speed: config.animation_speed,
|
||||
spring_config: config.spring_config,
|
||||
viewport_spring_config: config.viewport_spring_config,
|
||||
cursor_status: CursorImageStatus::default_named(),
|
||||
cursor_theme,
|
||||
named_cursors,
|
||||
@@ -639,6 +653,7 @@ impl Ferese {
|
||||
|
||||
self.space.unmap_elem(window);
|
||||
self.window_geometry.remove(&id);
|
||||
self.scrolling_world_x.remove(&id);
|
||||
if let Err(error) = self.workspaces.remove_window(id) {
|
||||
tracing::error!(%error, ?id, "failed to remove window from layout");
|
||||
}
|
||||
@@ -651,6 +666,7 @@ impl Ferese {
|
||||
|
||||
pub fn relayout(&mut self) {
|
||||
self.arrange_layers();
|
||||
let mut previous_scrolling_world_x = std::mem::take(&mut self.scrolling_world_x);
|
||||
let outputs = self.space.outputs().cloned().collect::<Vec<_>>();
|
||||
let constraints = self.window_constraints();
|
||||
let mut visible = HashSet::new();
|
||||
@@ -693,6 +709,18 @@ impl Ferese {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let viewport_target = workspace.layout.viewport_x();
|
||||
let viewport_current = viewport_target.map(|target| {
|
||||
let viewport = self
|
||||
.viewport_animations
|
||||
.entry(workspace_id)
|
||||
.or_insert_with(|| AnimatedValue::new(target));
|
||||
viewport.retarget_preserving_motion(target);
|
||||
if !self.animations_enabled {
|
||||
viewport.snap();
|
||||
}
|
||||
viewport.current
|
||||
});
|
||||
|
||||
for warning in layout.warnings {
|
||||
tracing::warn!(
|
||||
@@ -732,7 +760,22 @@ impl Ferese {
|
||||
);
|
||||
|
||||
visible.insert(*id);
|
||||
placements.push((window.clone(), *id, rect, is_fullscreen, is_floating));
|
||||
let scrolling = if !is_fullscreen && !is_floating {
|
||||
viewport_target
|
||||
.zip(viewport_current)
|
||||
.map(|(target, current)| (workspace_id, rect.x + target, current))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
placements.push((
|
||||
window.clone(),
|
||||
*id,
|
||||
rect,
|
||||
is_fullscreen,
|
||||
is_floating,
|
||||
scrolling,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -749,7 +792,9 @@ impl Ferese {
|
||||
|
||||
let now = self.start_time.elapsed();
|
||||
|
||||
for (window, id, rect, is_fullscreen, is_floating) in placements {
|
||||
let mut scrolling_world_x = HashMap::new();
|
||||
|
||||
for (window, id, rect, is_fullscreen, is_floating, scrolling) in placements {
|
||||
let committed_size = client_size(&window);
|
||||
let geometry = self
|
||||
.window_geometry
|
||||
@@ -759,6 +804,20 @@ impl Ferese {
|
||||
if !self.animations_enabled {
|
||||
geometry.advance(Duration::ZERO, self.spring_config, false);
|
||||
}
|
||||
if let Some((workspace, world_x, viewport_x)) = scrolling {
|
||||
let mut animated_world_x = previous_scrolling_world_x
|
||||
.remove(&id)
|
||||
.filter(|(previous_workspace, _)| *previous_workspace == workspace)
|
||||
.map(|(_, world_x)| world_x)
|
||||
.unwrap_or_else(|| AnimatedValue::new(world_x));
|
||||
animated_world_x.set_target(world_x);
|
||||
if !self.animations_enabled {
|
||||
animated_world_x.snap();
|
||||
}
|
||||
geometry.visual.current.x = animated_world_x.current - viewport_x;
|
||||
geometry.visual.velocity.x = 0.0;
|
||||
scrolling_world_x.insert(id, (workspace, animated_world_x));
|
||||
}
|
||||
let visual = geometry.visual.current;
|
||||
let location = (visual.x.round() as i32, visual.y.round() as i32);
|
||||
|
||||
@@ -804,6 +863,7 @@ impl Ferese {
|
||||
}
|
||||
}
|
||||
}
|
||||
self.scrolling_world_x = scrolling_world_x;
|
||||
|
||||
crate::backends::direct::render_all(self);
|
||||
}
|
||||
@@ -832,6 +892,12 @@ impl Ferese {
|
||||
|
||||
fn advance_animations_by(&mut self, delta: std::time::Duration) -> bool {
|
||||
let delta = delta.mul_f64(self.animation_speed);
|
||||
let visible_workspaces = self
|
||||
.space
|
||||
.outputs()
|
||||
.filter_map(|output| self.output_ids.get(output))
|
||||
.filter_map(|output| self.output_workspaces.active_workspace(*output))
|
||||
.collect::<HashSet<_>>();
|
||||
let windows = self
|
||||
.space
|
||||
.elements()
|
||||
@@ -844,6 +910,17 @@ impl Ferese {
|
||||
.collect::<Vec<_>>();
|
||||
let mut active_animation = false;
|
||||
|
||||
for (workspace, viewport) in &mut self.viewport_animations {
|
||||
if !visible_workspaces.contains(workspace) {
|
||||
continue;
|
||||
}
|
||||
if self.animations_enabled {
|
||||
active_animation |= viewport.advance(delta, self.viewport_spring_config);
|
||||
} else {
|
||||
viewport.snap();
|
||||
}
|
||||
}
|
||||
|
||||
for (window, id) in windows {
|
||||
let Some(geometry) = self.window_geometry.get_mut(&id) else {
|
||||
continue;
|
||||
@@ -851,6 +928,17 @@ impl Ferese {
|
||||
|
||||
active_animation |=
|
||||
geometry.advance(delta, self.spring_config, self.animations_enabled);
|
||||
if let Some((workspace, world_x)) = self.scrolling_world_x.get_mut(&id)
|
||||
&& let Some(viewport) = self.viewport_animations.get(workspace)
|
||||
{
|
||||
if self.animations_enabled {
|
||||
active_animation |= world_x.advance(delta, self.spring_config);
|
||||
} else {
|
||||
world_x.snap();
|
||||
}
|
||||
geometry.visual.current.x = world_x.current - viewport.current;
|
||||
geometry.visual.velocity.x = 0.0;
|
||||
}
|
||||
if let Some(size) = geometry.client.expire_wait(self.start_time.elapsed()) {
|
||||
tracing::warn!(
|
||||
?id,
|
||||
|
||||
Reference in new issue
Block a user