Implement Continuum compositor and shell motion
This commit is contained in:
1 parent
259f7e93b6
commit
f8bc17e913
42 files changed
+1873
-647
No files matched your search
@@ -1058,17 +1058,6 @@ fn cursor_affected_outputs(state: &Ferese) -> Vec<Output> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub(crate) fn render_window(state: &mut Ferese, window: ferese_layout::WindowId) {
|
||||
let outputs = state
|
||||
.space
|
||||
.outputs()
|
||||
.filter(|output| state.window_belongs_to_output(window, output))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
render_on(state, &outputs);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub(crate) fn render_surface(
|
||||
state: &mut Ferese,
|
||||
|
||||
+11
-28
@@ -346,34 +346,6 @@ const fn default_repeat_delay() -> i32 {
|
||||
600
|
||||
}
|
||||
|
||||
const fn default_animation_speed() -> f64 {
|
||||
1.0
|
||||
}
|
||||
|
||||
const fn default_spring_mass() -> f64 {
|
||||
1.0
|
||||
}
|
||||
|
||||
const fn default_spring_stiffness() -> f64 {
|
||||
700.0
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
const fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
@@ -384,6 +356,17 @@ pub(crate) fn config_path() -> Option<PathBuf> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn rejects_undamped_overshoot_before_publication() {
|
||||
for property in ["spring", "viewport-spring"] {
|
||||
for coefficient in ["damping", "damping-ratio"] {
|
||||
let source = format!("animations {{ {property} {{ {coefficient} 0; overshoot #true; }}; }}");
|
||||
let error = Config::parse_source(&source).unwrap().runtime_config().err().unwrap().to_string();
|
||||
assert!(error.contains("overshoot requires positive damping"), "{error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -44,7 +44,7 @@ impl Config {
|
||||
}
|
||||
|
||||
pub fn animations_enabled(&self) -> bool {
|
||||
self.animations.enabled && !self.animations.reduced_motion
|
||||
self.animations.motion_enabled()
|
||||
}
|
||||
|
||||
pub fn animation_speed(&self) -> Result<f64, ConfigError> {
|
||||
@@ -52,39 +52,14 @@ impl Config {
|
||||
}
|
||||
|
||||
pub fn spring_config(&self) -> Result<SpringConfig, ConfigError> {
|
||||
let mass = positive_animation_value(self.animations.spring.mass, "spring.mass")?;
|
||||
let stiffness = positive_animation_value(self.animations.spring.stiffness, "spring.stiffness")?;
|
||||
let damping = self.animations.spring.damping;
|
||||
if !damping.is_finite() || damping < 0.0 {
|
||||
return Err(ConfigError::AnimationValue {
|
||||
field: "spring.damping",
|
||||
value: damping,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(SpringConfig {
|
||||
mass,
|
||||
stiffness,
|
||||
damping,
|
||||
..SpringConfig::default()
|
||||
})
|
||||
self.animations
|
||||
.spring
|
||||
.resolve(SpringConfig::default())
|
||||
.map_err(ConfigError::Binding)
|
||||
}
|
||||
|
||||
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()
|
||||
})
|
||||
self.animations.viewport_config().map_err(ConfigError::Binding)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,68 +153,4 @@ impl Default for TouchpadConfig {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(super) struct AnimationsConfig {
|
||||
#[serde(default = "enabled_by_default")]
|
||||
pub(super) enabled: bool,
|
||||
#[serde(default)]
|
||||
pub(super) reduced_motion: bool,
|
||||
#[serde(default = "default_animation_speed")]
|
||||
pub(super) speed: f64,
|
||||
#[serde(default)]
|
||||
pub(super) spring: SpringSettings,
|
||||
#[serde(default)]
|
||||
pub(super) viewport_spring: ViewportSpringSettings,
|
||||
}
|
||||
|
||||
impl Default for AnimationsConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: true,
|
||||
reduced_motion: false,
|
||||
speed: 1.0,
|
||||
spring: SpringSettings::default(),
|
||||
viewport_spring: ViewportSpringSettings::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(super) struct ViewportSpringSettings {
|
||||
#[serde(default = "default_viewport_mass")]
|
||||
pub(super) mass: f64,
|
||||
#[serde(default = "default_viewport_stiffness")]
|
||||
pub(super) stiffness: f64,
|
||||
#[serde(default = "default_viewport_damping_ratio")]
|
||||
pub(super) 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(super) struct SpringSettings {
|
||||
#[serde(default = "default_spring_mass")]
|
||||
pub(super) mass: f64,
|
||||
#[serde(default = "default_spring_stiffness")]
|
||||
pub(super) stiffness: f64,
|
||||
#[serde(default = "default_spring_damping")]
|
||||
pub(super) damping: f64,
|
||||
}
|
||||
|
||||
impl Default for SpringSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
mass: default_spring_mass(),
|
||||
stiffness: default_spring_stiffness(),
|
||||
damping: default_spring_damping(),
|
||||
}
|
||||
}
|
||||
}
|
||||
pub(super) type AnimationsConfig = ferese_animation::MotionSettings;
|
||||
+110
-3
@@ -1,5 +1,3 @@
|
||||
//! Navigation previews follow finger motion; actions commit once on release.
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
#[repr(u32)]
|
||||
pub(crate) enum SwipeDirection {
|
||||
@@ -12,10 +10,16 @@ pub(crate) enum SwipeDirection {
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Swipe {
|
||||
delta: Option<(f64, f64)>,
|
||||
tracker_x: ferese_animation::gesture::VelocityTracker,
|
||||
tracker_y: ferese_animation::gesture::VelocityTracker,
|
||||
pub release_velocity: f64,
|
||||
pub unbounded_release_velocity: f64,
|
||||
|
||||
threshold: f64,
|
||||
fingers: u32,
|
||||
direction: Option<SwipeDirection>,
|
||||
preview_started: bool,
|
||||
momentum_navigation: bool,
|
||||
}
|
||||
|
||||
impl Swipe {
|
||||
@@ -25,6 +29,59 @@ impl Swipe {
|
||||
self.fingers = fingers;
|
||||
self.direction = None;
|
||||
self.preview_started = false;
|
||||
self.momentum_navigation = false;
|
||||
self.tracker_x = Default::default();
|
||||
self.tracker_y = Default::default();
|
||||
self.release_velocity = 0.0;
|
||||
self.unbounded_release_velocity = 0.0;
|
||||
}
|
||||
|
||||
pub fn start_time(&mut self, time: std::time::Duration) {
|
||||
self.tracker_x.push(time, 0.0);
|
||||
self.tracker_y.push(time, 0.0);
|
||||
}
|
||||
|
||||
pub fn update_at(&mut self, dx: f64, dy: f64, time: std::time::Duration) {
|
||||
self.update(dx, dy);
|
||||
if let Some((x, y)) = self.delta {
|
||||
self.tracker_x.push(time, x);
|
||||
self.tracker_y.push(time, y);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn finish_at(&mut self, cancelled: bool, time: std::time::Duration) -> Option<SwipeDirection> {
|
||||
let Some(direction) = self.direction else {
|
||||
return self.finish(cancelled);
|
||||
};
|
||||
let v = match direction {
|
||||
SwipeDirection::Left => -self.tracker_x.velocity(time),
|
||||
SwipeDirection::Right => self.tracker_x.velocity(time),
|
||||
SwipeDirection::Up => -self.tracker_y.velocity(time),
|
||||
SwipeDirection::Down => self.tracker_y.velocity(time),
|
||||
};
|
||||
self.release_velocity = if cancelled {
|
||||
0.0
|
||||
} else {
|
||||
v / (2.0 * self.threshold.max(1.0))
|
||||
};
|
||||
self.unbounded_release_velocity = self.release_velocity;
|
||||
if !self.preview_started && !self.momentum_navigation {
|
||||
return self.finish(cancelled);
|
||||
}
|
||||
let Some((_, progress)) = self.preview() else {
|
||||
self.release_velocity = 0.0;
|
||||
self.unbounded_release_velocity = 0.0;
|
||||
self.delta = None;
|
||||
return None;
|
||||
};
|
||||
// The preview is bounded. Its visible velocity is zero when pushing
|
||||
// farther beyond either endpoint, even if the fingers keep moving.
|
||||
if (progress == 0.0 && self.release_velocity < 0.0) || (progress == 1.0 && self.release_velocity > 0.0) {
|
||||
self.release_velocity = 0.0;
|
||||
}
|
||||
self.delta = None;
|
||||
(!cancelled && ferese_animation::gesture::project(progress, self.release_velocity, 0.997) >= 0.5)
|
||||
.then_some(direction)
|
||||
}
|
||||
|
||||
pub fn fingers(&self) -> u32 {
|
||||
@@ -49,11 +106,20 @@ impl Swipe {
|
||||
self.preview_started = true;
|
||||
}
|
||||
|
||||
pub fn mark_momentum_navigation(&mut self) {
|
||||
self.momentum_navigation = true;
|
||||
}
|
||||
|
||||
pub fn preview_started(&self) -> bool {
|
||||
self.preview_started
|
||||
}
|
||||
|
||||
pub fn preview(&self) -> Option<(SwipeDirection, f64)> {
|
||||
self.unbounded_preview()
|
||||
.map(|(direction, progress)| (direction, progress.clamp(0.0, 1.0)))
|
||||
}
|
||||
|
||||
pub fn unbounded_preview(&self) -> Option<(SwipeDirection, f64)> {
|
||||
let (x, y) = self.delta?;
|
||||
if !x.is_finite() || !y.is_finite() {
|
||||
return None;
|
||||
@@ -65,7 +131,7 @@ impl Swipe {
|
||||
SwipeDirection::Up => -y,
|
||||
SwipeDirection::Down => y,
|
||||
};
|
||||
Some((direction, (distance / (self.threshold.max(1.0) * 2.0)).clamp(0.0, 1.0)))
|
||||
Some((direction, distance / (self.threshold.max(1.0) * 2.0)))
|
||||
}
|
||||
|
||||
pub fn finish(&mut self, cancelled: bool) -> Option<SwipeDirection> {
|
||||
@@ -103,6 +169,47 @@ fn dominant_direction(x: f64, y: f64, threshold: f64) -> Option<SwipeDirection>
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn release_uses_momentum_and_a_pause_removes_it() {
|
||||
use std::time::Duration;
|
||||
let swipe = || {
|
||||
let mut swipe = Swipe::default();
|
||||
swipe.begin(3, false, 80);
|
||||
swipe.start_time(Duration::ZERO);
|
||||
swipe.update_at(0.0, -20.0, Duration::from_millis(10));
|
||||
swipe.update_at(0.0, -20.0, Duration::from_millis(20));
|
||||
swipe.mark_preview_started();
|
||||
swipe
|
||||
};
|
||||
let mut flick = swipe();
|
||||
assert_eq!(flick.preview(), Some((SwipeDirection::Up, 0.25)));
|
||||
assert_eq!(
|
||||
flick.finish_at(false, Duration::from_millis(20)),
|
||||
Some(SwipeDirection::Up)
|
||||
);
|
||||
assert!((flick.release_velocity - 12.5).abs() < 1e-9);
|
||||
let mut paused = swipe();
|
||||
assert_eq!(paused.finish_at(false, Duration::from_millis(80)), None);
|
||||
assert_eq!(paused.release_velocity, 0.0);
|
||||
let mut cancelled = swipe();
|
||||
assert_eq!(cancelled.finish_at(true, Duration::from_millis(20)), None);
|
||||
assert_eq!(cancelled.release_velocity, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reversing_momentum_can_cancel_past_halfway() {
|
||||
use std::time::Duration;
|
||||
let mut swipe = Swipe::default();
|
||||
swipe.begin(3, false, 80);
|
||||
swipe.start_time(Duration::ZERO);
|
||||
swipe.update_at(0.0, -120.0, Duration::from_millis(200));
|
||||
swipe.update_at(0.0, 20.0, Duration::from_millis(210));
|
||||
swipe.mark_preview_started();
|
||||
assert_eq!(swipe.preview(), Some((SwipeDirection::Up, 0.625)));
|
||||
assert_eq!(swipe.finish_at(false, Duration::from_millis(210)), None);
|
||||
assert!(swipe.release_velocity < 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cancellation_and_new_gestures_reset_preview_state() {
|
||||
let mut swipe = Swipe::default();
|
||||
|
||||
@@ -37,6 +37,7 @@ impl CompositorHandler for Ferese {
|
||||
fn commit(&mut self, surface: &WlSurface) {
|
||||
self.update_surface_preferences(surface);
|
||||
self.capture_resize_before_commit(surface);
|
||||
self.capture_close_before_commit(surface);
|
||||
on_commit_buffer_handler::<Self>(surface);
|
||||
if !is_sync_subsurface(surface) {
|
||||
let mut root = surface.clone();
|
||||
|
||||
+13
-3
@@ -170,6 +170,7 @@ impl Ferese {
|
||||
.any(|binding| binding.swipe_fingers(event.fingers())),
|
||||
self.input_settings.touchpad.swipe_threshold,
|
||||
);
|
||||
self.swipe.start_time(std::time::Duration::from_micros(event.time()));
|
||||
if self.swipe.active() {
|
||||
self.focus_output_at(pointer.current_location());
|
||||
} else {
|
||||
@@ -185,7 +186,11 @@ impl Ferese {
|
||||
}
|
||||
InputEvent::GestureSwipeUpdate { event } => {
|
||||
if self.swipe.active() {
|
||||
self.swipe.update(event.delta_x(), event.delta_y());
|
||||
self.swipe.update_at(
|
||||
event.delta_x(),
|
||||
event.delta_y(),
|
||||
std::time::Duration::from_micros(event.time()),
|
||||
);
|
||||
if self.swipe_navigation_blocked() {
|
||||
self.finish_workspace_swipe(None);
|
||||
self.finish_focus_swipe(None);
|
||||
@@ -198,10 +203,13 @@ impl Ferese {
|
||||
{
|
||||
match action {
|
||||
BindingAction::SwitchRelativeWorkspace(next) => {
|
||||
self.swipe.mark_momentum_navigation();
|
||||
self.preview_workspace_swipe(next, direction, progress);
|
||||
}
|
||||
BindingAction::Focus(focus @ (Direction::Left | Direction::Right)) => {
|
||||
self.preview_focus_swipe(focus, direction, progress);
|
||||
self.swipe.mark_momentum_navigation();
|
||||
let raw = self.swipe.unbounded_preview().map_or(progress, |(_, value)| value);
|
||||
self.preview_focus_swipe(focus, direction, raw);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -220,7 +228,9 @@ impl Ferese {
|
||||
if self.swipe.active() {
|
||||
let fingers = self.swipe.fingers();
|
||||
let cancelled = event.cancelled() || self.swipe_navigation_blocked();
|
||||
let direction = self.swipe.finish(cancelled);
|
||||
let direction = self
|
||||
.swipe
|
||||
.finish_at(cancelled, std::time::Duration::from_micros(event.time()));
|
||||
let workspace_handled = self.finish_workspace_swipe(direction);
|
||||
let focus_handled = self.finish_focus_swipe(direction);
|
||||
let handled = workspace_handled || focus_handled || self.swipe.preview_started();
|
||||
|
||||
+50
-58
@@ -17,7 +17,6 @@ const OVERVIEW_MARGIN: f64 = 48.0;
|
||||
const OVERVIEW_GAP: f64 = 40.0;
|
||||
// Applied after the configured animation speed, in both live and predicted motion.
|
||||
const OVERVIEW_MOTION_SPEED: f64 = 0.6;
|
||||
const EXIT_DURATION: f64 = 0.18;
|
||||
const WORKSPACE_CARD_GAP: f64 = 8.0;
|
||||
const MAX_PREVIEW_SCALE: f64 = 0.82;
|
||||
|
||||
@@ -117,7 +116,6 @@ pub(crate) struct OverviewMotion {
|
||||
active: bool,
|
||||
presentations: HashMap<WindowId, AnimatedRect>,
|
||||
opacity: AnimatedValue,
|
||||
exit_transition: Option<(f64, f64)>,
|
||||
}
|
||||
|
||||
impl Default for OverviewState {
|
||||
@@ -179,8 +177,7 @@ fn centered_row_start(strip: Rect, count: usize, card_width: f64) -> f64 {
|
||||
|
||||
impl OverviewState {
|
||||
pub(crate) fn needs_tick(&self) -> bool {
|
||||
self.motion.exit_transition.is_some()
|
||||
|| self.motion.opacity.current != self.motion.opacity.target
|
||||
self.motion.opacity.current != self.motion.opacity.target
|
||||
|| self.motion.opacity.velocity != 0.0
|
||||
|| self
|
||||
.motion
|
||||
@@ -366,25 +363,22 @@ impl OverviewState {
|
||||
self.motion.presented_rect(id, normal)
|
||||
}
|
||||
|
||||
pub(crate) fn presented_velocity(&self, id: WindowId) -> Option<ferese_animation::RectVelocity> {
|
||||
let velocity = self.motion.presentations.get(&id)?.velocity;
|
||||
Some(ferese_animation::RectVelocity {
|
||||
x: velocity.x * OVERVIEW_MOTION_SPEED,
|
||||
y: velocity.y * OVERVIEW_MOTION_SPEED,
|
||||
width: velocity.width * OVERVIEW_MOTION_SPEED,
|
||||
height: velocity.height * OVERVIEW_MOTION_SPEED,
|
||||
})
|
||||
}
|
||||
|
||||
fn enter(
|
||||
&mut self,
|
||||
targets: HashMap<WindowId, (Rect, Rect)>,
|
||||
selected: Option<WindowId>,
|
||||
animations_enabled: bool,
|
||||
) {
|
||||
// Reverse a dismissal from the currently visible geometry, not the old grid.
|
||||
if self.motion.exit_transition.is_some() {
|
||||
let visible: Vec<_> = targets
|
||||
.iter()
|
||||
.map(|(id, (normal, _))| (*id, self.presented_rect(*id, *normal)))
|
||||
.collect();
|
||||
for (id, rect) in visible {
|
||||
if let Some(presentation) = self.motion.presentations.get_mut(&id) {
|
||||
*presentation = AnimatedRect::new(rect);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.motion.exit_transition = None;
|
||||
self.selection_workspace = None;
|
||||
self.motion.active = true;
|
||||
self.motion.opacity.retarget_preserving_motion(1.0);
|
||||
@@ -423,7 +417,6 @@ impl OverviewState {
|
||||
|
||||
fn exit(&mut self, normal: HashMap<WindowId, Rect>, animations_enabled: bool) {
|
||||
self.motion.active = false;
|
||||
self.motion.exit_transition = animations_enabled.then_some((0.0, self.motion.opacity.current));
|
||||
self.motion.opacity.retarget_preserving_motion(0.0);
|
||||
if !animations_enabled {
|
||||
self.motion.opacity.snap();
|
||||
@@ -605,6 +598,17 @@ impl Ferese {
|
||||
|
||||
pub(crate) fn retarget_overview(&mut self) {
|
||||
if !self.overview.is_active() {
|
||||
if self.overview.is_presenting() {
|
||||
let normal = self.normal_window_rects();
|
||||
self.overview.motion.presentations.retain(|id, presentation| {
|
||||
if let Some(target) = normal.get(id) {
|
||||
presentation.set_target(*target);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -643,17 +647,26 @@ impl Ferese {
|
||||
presented.x += x;
|
||||
presented.y += y;
|
||||
}
|
||||
Some(presented)
|
||||
let record = self.windows.record(id)?;
|
||||
let opening = record.opening.map_or(1.0, |value| value.current);
|
||||
Some(crate::presentation::scaled_visual_rect(
|
||||
presented,
|
||||
0.97 + 0.03 * opening,
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn inverse_presented_window_point(&self, id: WindowId, x: f64, y: f64) -> Option<(f64, f64)> {
|
||||
let geometry = self.windows.geometry(&id)?;
|
||||
if !self.overview.is_presenting() {
|
||||
if !self.overview.is_presenting() && self.windows.record(id)?.opening.is_none() {
|
||||
let presented = self.presented_window_rect(id)?;
|
||||
return Some((x - presented.x, y - presented.y));
|
||||
}
|
||||
let source = geometry.client.committed_size?;
|
||||
let presented = self.overview.presented_rect(id, geometry.visual.current);
|
||||
let source = if self.overview.is_presenting() {
|
||||
geometry.client.committed_size?
|
||||
} else {
|
||||
ferese_animation::ClientSize::from_rect(geometry.visual.current)
|
||||
};
|
||||
let presented = self.presented_window_rect(id)?;
|
||||
|
||||
if source.width <= 0 || source.height <= 0 || presented.width <= 0.0 || presented.height <= 0.0 {
|
||||
return None;
|
||||
@@ -989,15 +1002,13 @@ impl Default for OverviewMotion {
|
||||
active: false,
|
||||
presentations: HashMap::new(),
|
||||
opacity: AnimatedValue::new(0.0),
|
||||
exit_transition: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OverviewMotion {
|
||||
pub(crate) fn is_animating(&self, spring: SpringConfig) -> bool {
|
||||
self.exit_transition.is_some()
|
||||
|| (self.opacity.current - self.opacity.target).abs() > 0.001
|
||||
(self.opacity.current - self.opacity.target).abs() > 0.001
|
||||
|| self.opacity.velocity.abs() > 0.005
|
||||
|| self.presentations.values().any(|rect| !rect.is_settled(spring))
|
||||
}
|
||||
@@ -1012,43 +1023,17 @@ impl OverviewMotion {
|
||||
|
||||
pub(crate) fn presented_rect(&self, id: WindowId, normal: Rect) -> Rect {
|
||||
let preview = self.presentations.get(&id).map_or(normal, |p| p.current);
|
||||
if let Some((elapsed, _)) = self.exit_transition {
|
||||
let t = 1.0 - (1.0 - (elapsed / EXIT_DURATION).clamp(0.0, 1.0)).powi(3);
|
||||
return Rect::new(
|
||||
preview.x + (normal.x - preview.x) * t,
|
||||
preview.y + (normal.y - preview.y) * t,
|
||||
preview.width + (normal.width - preview.width) * t,
|
||||
preview.height + (normal.height - preview.height) * t,
|
||||
);
|
||||
}
|
||||
preview
|
||||
}
|
||||
|
||||
pub(crate) fn advance(&mut self, delta: Duration, spring: SpringConfig, animations_enabled: bool) -> bool {
|
||||
let delta = delta.mul_f64(OVERVIEW_MOTION_SPEED);
|
||||
|
||||
if let Some((elapsed, initial_opacity)) = self.exit_transition {
|
||||
let elapsed = if animations_enabled {
|
||||
elapsed + delta.as_secs_f64()
|
||||
} else {
|
||||
EXIT_DURATION
|
||||
};
|
||||
let remaining = (1.0 - elapsed / EXIT_DURATION).clamp(0.0, 1.0);
|
||||
self.opacity.current = initial_opacity * remaining.powi(3);
|
||||
if elapsed >= EXIT_DURATION {
|
||||
self.exit_transition = None;
|
||||
self.presentations.clear();
|
||||
self.opacity.snap();
|
||||
return false;
|
||||
}
|
||||
self.exit_transition = Some((elapsed, initial_opacity));
|
||||
return true;
|
||||
}
|
||||
let mut active_animation = false;
|
||||
|
||||
for presentation in self.presentations.values_mut() {
|
||||
if animations_enabled {
|
||||
active_animation |= presentation.advance_with_policy(delta, spring, CrossingPolicy::NoCrossing);
|
||||
active_animation |= presentation.advance(delta, spring);
|
||||
} else {
|
||||
presentation.snap();
|
||||
}
|
||||
@@ -1197,7 +1182,7 @@ mod tests {
|
||||
let before = overview.presented_rect(id, normal);
|
||||
let opacity = overview.opacity();
|
||||
|
||||
let finished = overview.sample(Duration::from_millis(24), SpringConfig::default());
|
||||
let finished = overview.sample(Duration::from_secs(2), SpringConfig::default());
|
||||
let earlier = overview.sample(Duration::from_millis(4), SpringConfig::default());
|
||||
|
||||
assert!(!finished.is_presenting());
|
||||
@@ -1351,7 +1336,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exiting_overview_tracks_a_moving_layout_and_finishes_in_300ms() {
|
||||
fn exiting_overview_retargets_without_resetting_motion() {
|
||||
let id = WindowId(1);
|
||||
let initial = Rect::new(-400.0, 0.0, 800.0, 600.0);
|
||||
let preview = Rect::new(100.0, 100.0, 400.0, 300.0);
|
||||
@@ -1359,10 +1344,15 @@ mod tests {
|
||||
let mut overview = OverviewState::default();
|
||||
overview.enter(HashMap::from([(id, (initial, preview))]), Some(id), false);
|
||||
overview.exit(HashMap::from([(id, initial)]), true);
|
||||
overview.advance(Duration::from_millis(150), SpringConfig::default(), true);
|
||||
let shown = overview.presented_rect(id, moved);
|
||||
assert!((shown.x - 187.5).abs() < 0.001);
|
||||
overview.advance(Duration::from_millis(150), SpringConfig::default(), true);
|
||||
overview.advance(Duration::from_millis(60), SpringConfig::default(), true);
|
||||
let before = overview.motion.presentations[&id];
|
||||
overview.exit(HashMap::from([(id, moved)]), true);
|
||||
let after = overview.motion.presentations[&id];
|
||||
assert_eq!(before.current, after.current);
|
||||
assert_eq!(before.velocity, after.velocity);
|
||||
for _ in 0..300 {
|
||||
overview.advance(Duration::from_millis(8), SpringConfig::default(), true);
|
||||
}
|
||||
assert!(!overview.is_presenting());
|
||||
assert_eq!(overview.presented_rect(id, moved), moved);
|
||||
}
|
||||
@@ -1378,7 +1368,9 @@ mod tests {
|
||||
overview.exit(HashMap::from([(id, normal)]), true);
|
||||
overview.advance(Duration::from_millis(60), SpringConfig::default(), true);
|
||||
let shown = overview.presented_rect(id, normal);
|
||||
let velocity = overview.motion.presentations[&id].velocity;
|
||||
overview.enter(targets, Some(id), true);
|
||||
assert_eq!(overview.motion.presentations[&id].velocity, velocity);
|
||||
assert_eq!(overview.presented_rect(id, normal), shown);
|
||||
}
|
||||
|
||||
|
||||
@@ -523,3 +523,63 @@ mod tests {
|
||||
assert!(handoff_alpha(Duration::from_millis(1)) > 0.99);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn scaled_visual_rect(rect: ferese_layout::Rect, scale: f64) -> ferese_layout::Rect {
|
||||
let scale = scale.max(0.0);
|
||||
let width = rect.width * scale;
|
||||
let height = rect.height * scale;
|
||||
|
||||
ferese_layout::Rect::new(
|
||||
rect.x + (rect.width - width) / 2.0,
|
||||
rect.y + (rect.height - height) / 2.0,
|
||||
width,
|
||||
height,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn scaled_visual_velocity(
|
||||
rect: ferese_layout::Rect,
|
||||
velocity: ferese_animation::RectVelocity,
|
||||
scale: f64,
|
||||
scale_velocity: f64,
|
||||
) -> ferese_animation::RectVelocity {
|
||||
ferese_animation::RectVelocity {
|
||||
x: velocity.x + ((1.0 - scale) * velocity.width - rect.width * scale_velocity) * 0.5,
|
||||
y: velocity.y + ((1.0 - scale) * velocity.height - rect.height * scale_velocity) * 0.5,
|
||||
width: velocity.width * scale + rect.width * scale_velocity,
|
||||
height: velocity.height * scale + rect.height * scale_velocity,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod motion_tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn center_scale_velocity_matches_the_visible_rectangle() {
|
||||
let rect = ferese_layout::Rect::new(12.25, 18.75, 400.5, 300.25);
|
||||
let velocity = ferese_animation::RectVelocity {
|
||||
x: 80.0,
|
||||
y: -40.0,
|
||||
width: 100.0,
|
||||
height: 20.0,
|
||||
};
|
||||
let v = scaled_visual_velocity(rect, velocity, 0.98, 0.07);
|
||||
let before = scaled_visual_rect(rect, 0.98);
|
||||
let dt = 1e-5;
|
||||
let next = ferese_layout::Rect::new(
|
||||
rect.x + velocity.x * dt,
|
||||
rect.y + velocity.y * dt,
|
||||
rect.width + velocity.width * dt,
|
||||
rect.height + velocity.height * dt,
|
||||
);
|
||||
let after = scaled_visual_rect(next, 0.98 + 0.07 * dt);
|
||||
for (measured, expected) in [
|
||||
((after.x - before.x) / dt, v.x),
|
||||
((after.y - before.y) / dt, v.y),
|
||||
((after.width - before.width) / dt, v.width),
|
||||
((after.height - before.height) / dt, v.height),
|
||||
] {
|
||||
assert!((measured - expected).abs() < 0.0001);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
use super::*;
|
||||
use ferese_animation::{AnimatedRect, AnimatedValue, CrossingPolicy, SpringConfig};
|
||||
use ferese_core::OutputId;
|
||||
use ferese_layout::WindowId;
|
||||
|
||||
/// Detached presentation only: no live window, input, focus, or layout entry.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ClosedWindow {
|
||||
pub id: WindowId,
|
||||
pub output: OutputId,
|
||||
pub below: Option<WindowId>,
|
||||
pub bounds: AnimatedRect,
|
||||
pub opacity: AnimatedValue,
|
||||
pub snapshot: ResizeSnapshot,
|
||||
pub source: Rectangle<f64, Buffer>,
|
||||
pub radius: f64,
|
||||
pub shape: CornerShape,
|
||||
pub focus: f64,
|
||||
pub decorations: f64,
|
||||
pub dim: f64,
|
||||
pub fill: Option<[f32; 4]>,
|
||||
pub material: Option<(
|
||||
smithay::reexports::wayland_server::protocol::wl_surface::WlSurface,
|
||||
crate::effects::SemanticRole,
|
||||
u64,
|
||||
f32,
|
||||
)>,
|
||||
}
|
||||
|
||||
impl ClosedWindow {
|
||||
pub fn advance(&mut self, delta: Duration, spring: SpringConfig) -> bool {
|
||||
self.bounds.advance(delta, spring);
|
||||
let opacity = self.opacity.advance_with_policy(
|
||||
delta,
|
||||
SpringConfig {
|
||||
position_tolerance: 0.001,
|
||||
velocity_tolerance: 0.001,
|
||||
..spring
|
||||
},
|
||||
CrossingPolicy::NoCrossing,
|
||||
);
|
||||
self.snapshot.commit.increment();
|
||||
// Once invisible there is no reason to keep a texture (including with
|
||||
// intentionally undamped, overshooting geometry configurations).
|
||||
opacity
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn elements(
|
||||
state: &mut Ferese,
|
||||
renderer: &mut GlesRenderer,
|
||||
output: &Output,
|
||||
below: Option<WindowId>,
|
||||
live: &std::collections::HashSet<WindowId>,
|
||||
delta: Duration,
|
||||
) -> Vec<AnimatedWindowRenderElement> {
|
||||
if state.session_lock.active() {
|
||||
return Vec::new();
|
||||
}
|
||||
let Some(output_id) = state.output_id(output) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let Some(output_geometry) = state.space.output_geometry(output) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let scale = output.current_scale().fractional_scale();
|
||||
let windows: Vec<_> = state
|
||||
.render
|
||||
.closing
|
||||
.iter()
|
||||
.filter(|window| {
|
||||
window.output == output_id
|
||||
&& window.below.filter(|id| live.contains(id)) == below
|
||||
&& window.snapshot.context == renderer.context_id().erased()
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
let mut elements = Vec::new();
|
||||
for mut window in windows {
|
||||
if !delta.is_zero() {
|
||||
window.advance(delta, state.spring_config);
|
||||
}
|
||||
let visual = window.bounds.current;
|
||||
let corners = RoundedRect::new(visual, output_geometry.loc, scale, window.radius).with_shape(window.shape);
|
||||
let constrain = rounded_visual_rect(visual, output_geometry.loc);
|
||||
let Some(programs) = corner_program(&mut state.render, renderer, window.shape) else {
|
||||
continue;
|
||||
};
|
||||
let alpha = window.opacity.current.clamp(0.0, 1.0) as f32;
|
||||
if let Some(dim) = window_tint_element(
|
||||
&mut state.render,
|
||||
renderer,
|
||||
window.id,
|
||||
constrain,
|
||||
corners,
|
||||
[0.0, 0.0, 0.0, window.dim as f32 * alpha],
|
||||
false,
|
||||
output,
|
||||
) {
|
||||
elements.push(dim.into());
|
||||
}
|
||||
let theme = &state.theme_settings;
|
||||
if let Some(border) = window_border_element(
|
||||
&mut state.render,
|
||||
theme,
|
||||
renderer,
|
||||
window.id,
|
||||
constrain,
|
||||
corners,
|
||||
scale,
|
||||
theme.border_width + (theme.focus_ring_width - theme.border_width) * window.focus,
|
||||
theme.border_color.0,
|
||||
theme.border_gradient,
|
||||
window.focus as f32,
|
||||
alpha * window.decorations as f32,
|
||||
output,
|
||||
&programs,
|
||||
) {
|
||||
elements.push(border.into());
|
||||
}
|
||||
let clip = framebuffer_clip_rect(
|
||||
corners.rect,
|
||||
output.current_mode().unwrap().size,
|
||||
output.current_transform().invert(),
|
||||
);
|
||||
elements.push(
|
||||
NativeTextureElement {
|
||||
id: window.snapshot.id.clone(),
|
||||
commit: window.snapshot.commit,
|
||||
texture: window.snapshot.texture.clone(),
|
||||
geometry: corners.rect,
|
||||
source: window.source,
|
||||
alpha,
|
||||
program: Some(programs.texture.clone()),
|
||||
uniforms: vec![Uniform::new("clip_rect", clip), Uniform::new("radius", corners.radius)],
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
if let Some(mut fill) = window.fill {
|
||||
fill[3] = alpha;
|
||||
if let Some(fill) = window_tint_element(
|
||||
&mut state.render,
|
||||
renderer,
|
||||
window.id,
|
||||
constrain,
|
||||
corners,
|
||||
fill,
|
||||
true,
|
||||
output,
|
||||
) {
|
||||
elements.push(fill.into());
|
||||
}
|
||||
}
|
||||
if let Some((surface, role, generation, opacity)) = &window.material {
|
||||
if let Some((material, _)) = material_element_with_role(
|
||||
state,
|
||||
renderer,
|
||||
output,
|
||||
surface,
|
||||
MaterialSurface {
|
||||
geometry: constrain,
|
||||
corners,
|
||||
index: 0,
|
||||
capture_geometry: constrain,
|
||||
alpha,
|
||||
},
|
||||
(*role, *generation, *opacity),
|
||||
) {
|
||||
elements.push(material);
|
||||
}
|
||||
}
|
||||
let theme = &state.theme_settings;
|
||||
if let Some(shadow) = window_shadow_element(
|
||||
&mut state.render,
|
||||
renderer,
|
||||
window.id,
|
||||
constrain,
|
||||
corners,
|
||||
scale,
|
||||
theme.shadow_offset_y,
|
||||
theme.shadow_blur,
|
||||
theme.shadow_opacity * f64::from(alpha) * window.decorations,
|
||||
theme.shadow_color.0,
|
||||
output,
|
||||
&programs,
|
||||
) {
|
||||
elements.push(shadow.into());
|
||||
}
|
||||
}
|
||||
elements
|
||||
}
|
||||
@@ -380,6 +380,7 @@ pub(super) fn rounded_window_elements(
|
||||
output: &Output,
|
||||
programs: RoundedClipPrograms,
|
||||
behavior: ConstrainScaleBehavior,
|
||||
native_size: Option<ferese_animation::ClientSize>,
|
||||
) -> Vec<AnimatedWindowRenderElement> {
|
||||
let Some(toplevel) = window.toplevel() else {
|
||||
return Vec::new();
|
||||
@@ -390,7 +391,13 @@ pub(super) fn rounded_window_elements(
|
||||
|
||||
let physical_constrain = corners.rect;
|
||||
let geometry = window.geometry();
|
||||
let reference = geometry.to_physical_precise_round(scale);
|
||||
let mut reference = geometry.to_physical_precise_round(scale);
|
||||
let behavior = if let Some(size) = native_size {
|
||||
reference.size = Size::<i32, Logical>::from((size.width, size.height)).to_physical_precise_round(scale);
|
||||
ConstrainScaleBehavior::Stretch
|
||||
} else {
|
||||
behavior
|
||||
};
|
||||
let location = physical_constrain.loc - geometry.loc.to_physical_precise_round(scale);
|
||||
let clip = framebuffer_clip_rect(physical_constrain, mode.size, output.current_transform().invert());
|
||||
let radius = corners.radius;
|
||||
@@ -462,18 +469,8 @@ pub(super) fn resize_content_behavior(intentional_scale: bool) -> ConstrainScale
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn scaled_visual_rect(rect: ferese_layout::Rect, scale: f64) -> ferese_layout::Rect {
|
||||
let scale = scale.clamp(0.0, 1.0);
|
||||
let width = rect.width * scale;
|
||||
let height = rect.height * scale;
|
||||
|
||||
ferese_layout::Rect::new(
|
||||
rect.x + (rect.width - width) / 2.0,
|
||||
rect.y + (rect.height - height) / 2.0,
|
||||
width,
|
||||
height,
|
||||
)
|
||||
}
|
||||
#[cfg(test)]
|
||||
pub(super) use crate::presentation::scaled_visual_rect;
|
||||
|
||||
pub(super) fn color_with_alpha(mut color: [f32; 4], alpha: f32) -> [f32; 4] {
|
||||
color[3] *= alpha;
|
||||
|
||||
@@ -128,6 +128,25 @@ pub(super) fn material_element(
|
||||
output: &Output,
|
||||
surface: &smithay::reexports::wayland_server::protocol::wl_surface::WlSurface,
|
||||
surface_geometry: MaterialSurface,
|
||||
) -> Option<(AnimatedWindowRenderElement, AnimatedWindowRenderElement)> {
|
||||
let (role, generation) = crate::effects::surface_role(surface)?;
|
||||
material_element_with_role(
|
||||
state,
|
||||
renderer,
|
||||
output,
|
||||
surface,
|
||||
surface_geometry,
|
||||
(role, generation, crate::effects::surface_opacity(surface)),
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn material_element_with_role(
|
||||
state: &mut Ferese,
|
||||
renderer: &mut GlesRenderer,
|
||||
output: &Output,
|
||||
surface: &smithay::reexports::wayland_server::protocol::wl_surface::WlSurface,
|
||||
surface_geometry: MaterialSurface,
|
||||
(role, generation, opacity): (crate::effects::SemanticRole, u64, f32),
|
||||
) -> Option<(AnimatedWindowRenderElement, AnimatedWindowRenderElement)> {
|
||||
let MaterialSurface {
|
||||
geometry,
|
||||
@@ -136,13 +155,12 @@ pub(super) fn material_element(
|
||||
capture_geometry,
|
||||
alpha,
|
||||
} = surface_geometry;
|
||||
let (role, generation) = crate::effects::surface_role(surface)?;
|
||||
let material = crate::effects::resolve_material(
|
||||
role,
|
||||
state.theme_settings.material_style,
|
||||
state.theme_settings.shell_opacity as f32,
|
||||
);
|
||||
let presentation_alpha = crate::effects::surface_opacity(surface) * alpha;
|
||||
let presentation_alpha = opacity * alpha;
|
||||
let mode = output.current_mode()?;
|
||||
let scale = output.current_scale().fractional_scale();
|
||||
let transform = output.current_transform().invert();
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
mod backdrop;
|
||||
mod capture;
|
||||
mod closing;
|
||||
pub(crate) use closing::ClosedWindow;
|
||||
#[cfg(test)]
|
||||
mod corner_tests;
|
||||
mod decorations;
|
||||
@@ -78,7 +80,7 @@ render_elements! {
|
||||
Native=NativeTextureElement,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct ResizeSnapshot {
|
||||
pub texture: GlesTexture,
|
||||
pub context: ErasedContextId,
|
||||
@@ -134,7 +136,7 @@ fn corner_shader_for(source: &str, shape: CornerShape) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn window_corner_shape(window: &smithay::desktop::Window) -> CornerShape {
|
||||
pub(crate) fn window_corner_shape(window: &smithay::desktop::Window) -> CornerShape {
|
||||
// Semantic shell surfaces keep their client-matching circular outlines.
|
||||
corner_shape_for_role(
|
||||
window
|
||||
|
||||
@@ -241,6 +241,7 @@ pub(super) fn overview_strip_elements(
|
||||
output,
|
||||
programs.clone(),
|
||||
ConstrainScaleBehavior::Stretch,
|
||||
None,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ use super::*;
|
||||
/// context. Desktop policy and animation values do not live in these caches.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct RenderResources {
|
||||
pub closing: Vec<ClosedWindow>,
|
||||
pub(super) windows: HashMap<WindowId, WindowBuffers>,
|
||||
pub(super) contexts: HashMap<ErasedContextId, ContextPrograms>,
|
||||
pub(super) outputs: HashMap<OutputId, OverviewScrim>,
|
||||
@@ -71,6 +72,16 @@ impl RenderResources {
|
||||
}
|
||||
|
||||
pub fn remove_output(&mut self, id: OutputId) {
|
||||
let removed: Vec<_> = self
|
||||
.closing
|
||||
.iter()
|
||||
.filter(|window| window.output == id)
|
||||
.map(|window| window.id)
|
||||
.collect();
|
||||
self.closing.retain(|window| window.output != id);
|
||||
for id in removed {
|
||||
self.remove_window(id);
|
||||
}
|
||||
self.outputs.remove(&id);
|
||||
|
||||
for window in self.windows.values_mut() {
|
||||
@@ -124,6 +135,16 @@ impl RenderResources {
|
||||
}
|
||||
|
||||
pub fn forget_context(&mut self, context: &ErasedContextId) {
|
||||
let removed: Vec<_> = self
|
||||
.closing
|
||||
.iter()
|
||||
.filter(|window| &window.snapshot.context == context)
|
||||
.map(|window| window.id)
|
||||
.collect();
|
||||
self.closing.retain(|window| &window.snapshot.context != context);
|
||||
for id in removed {
|
||||
self.remove_window(id);
|
||||
}
|
||||
self.contexts.remove(context);
|
||||
|
||||
for window in self.windows.values_mut() {
|
||||
|
||||
@@ -124,8 +124,8 @@ pub(crate) fn sampled_output_elements(
|
||||
return None;
|
||||
}
|
||||
let sample = frame.windows.get(&id)?;
|
||||
let visual = scaled_visual_rect(sample.rect, sample.close_scale);
|
||||
let close_alpha = sample.close_alpha;
|
||||
let visual = sample.rect;
|
||||
let close_alpha = sample.alpha;
|
||||
let decoration_progress = sample.geometry.decorations.clamp(0.0, 1.0);
|
||||
|
||||
Some((window.clone(), id, sample, visual, decoration_progress, close_alpha))
|
||||
@@ -145,7 +145,16 @@ pub(crate) fn sampled_output_elements(
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
let live = windows.iter().map(|(_, id, ..)| *id).collect();
|
||||
for (window, id, sample, visual, decoration_progress, close_alpha) in windows {
|
||||
elements.extend(super::closing::elements(
|
||||
state,
|
||||
renderer,
|
||||
output,
|
||||
Some(id),
|
||||
&live,
|
||||
frame.delta,
|
||||
));
|
||||
let constrain = rounded_visual_rect(visual, output_geometry.loc);
|
||||
let material_surface = window
|
||||
.toplevel()
|
||||
@@ -165,7 +174,7 @@ pub(crate) fn sampled_output_elements(
|
||||
let rounded_clip_program = corner_program(&mut state.render, renderer, shape);
|
||||
let pixels = corners.rect;
|
||||
// Only overview/close intentionally scale the complete application.
|
||||
let scale_content = frame.overview.is_presenting() || sample.close_scale != 1.0;
|
||||
let scale_content = sample.scale_content;
|
||||
let behavior = resize_content_behavior(scale_content);
|
||||
|
||||
let dim = sample.dim;
|
||||
@@ -227,13 +236,33 @@ pub(crate) fn sampled_output_elements(
|
||||
output,
|
||||
&programs,
|
||||
);
|
||||
if !scale_content
|
||||
&& let Some(snapshot) = state.render.snapshot(&id)
|
||||
if let Some(snapshot) = state.render.snapshot(&id)
|
||||
&& snapshot.context == renderer.context_id().erased()
|
||||
&& (snapshot.scale - scale).abs() < 0.001
|
||||
{
|
||||
let size = snapshot.texture.size();
|
||||
let visible = Rectangle::new(pixels.loc, (size.w, size.h).into()).intersection(pixels);
|
||||
let (sx, sy) = if scale_content {
|
||||
(
|
||||
f64::from(pixels.size.w) / f64::from(size.w),
|
||||
f64::from(pixels.size.h) / f64::from(size.h),
|
||||
)
|
||||
} else if let Some(native) = sample.native_size {
|
||||
(
|
||||
f64::from(pixels.size.w) / (f64::from(native.width) * scale).round().max(1.0),
|
||||
f64::from(pixels.size.h) / (f64::from(native.height) * scale).round().max(1.0),
|
||||
)
|
||||
} else {
|
||||
(1.0, 1.0)
|
||||
};
|
||||
let visible = Rectangle::new(
|
||||
pixels.loc,
|
||||
(
|
||||
(f64::from(size.w) * sx).round() as i32,
|
||||
(f64::from(size.h) * sy).round() as i32,
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
.intersection(pixels);
|
||||
if let Some(visible) = visible {
|
||||
let clip = framebuffer_clip_rect(
|
||||
pixels,
|
||||
@@ -247,8 +276,8 @@ pub(crate) fn sampled_output_elements(
|
||||
texture: snapshot.texture.clone(),
|
||||
geometry: visible,
|
||||
source: Rectangle::from_size(Size::from((
|
||||
f64::from(visible.size.w),
|
||||
f64::from(visible.size.h),
|
||||
f64::from(visible.size.w) / sx,
|
||||
f64::from(visible.size.h) / sy,
|
||||
))),
|
||||
alpha: crate::presentation::handoff_alpha(snapshot.elapsed) * close_alpha,
|
||||
program: Some(programs.texture.clone()),
|
||||
@@ -268,6 +297,7 @@ pub(crate) fn sampled_output_elements(
|
||||
output,
|
||||
programs.clone(),
|
||||
behavior,
|
||||
sample.native_size,
|
||||
));
|
||||
let source = window.geometry().size;
|
||||
if material_surface.is_none()
|
||||
@@ -322,6 +352,14 @@ pub(crate) fn sampled_output_elements(
|
||||
));
|
||||
}
|
||||
}
|
||||
elements.extend(super::closing::elements(
|
||||
state,
|
||||
renderer,
|
||||
output,
|
||||
None,
|
||||
&live,
|
||||
frame.delta,
|
||||
));
|
||||
elements.extend(layer_elements(
|
||||
state,
|
||||
renderer,
|
||||
@@ -374,7 +412,14 @@ pub(super) fn layer_elements(
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
state.render.surfaces.retain(|surface, _| surface.is_alive());
|
||||
state.render.surfaces.retain(|surface, _| {
|
||||
surface.is_alive()
|
||||
|| state
|
||||
.render
|
||||
.closing
|
||||
.iter()
|
||||
.any(|window| window.material.as_ref().is_some_and(|(key, ..)| key == surface))
|
||||
});
|
||||
|
||||
let mut elements = Vec::new();
|
||||
for (geometry, layer) in layers {
|
||||
|
||||
+172
-97
@@ -9,6 +9,7 @@ mod reconciliation;
|
||||
pub(crate) use reconciliation::{DesktopChanges, DesktopOutput, DesktopTransition};
|
||||
mod prediction;
|
||||
pub(crate) use prediction::FrameScene;
|
||||
mod lifecycle;
|
||||
mod window_registry;
|
||||
mod windows;
|
||||
|
||||
@@ -19,7 +20,7 @@ use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use calloop::LoopHandle;
|
||||
use ferese_animation::{AnimatedValue, ClientSize, CrossingPolicy, PresentationMode, SpringConfig, WindowGeometry};
|
||||
use ferese_animation::{AnimatedValue, ClientSize, PresentationMode, SpringConfig, WindowGeometry};
|
||||
use ferese_core::{
|
||||
LayoutMode, OutputGeometry, OutputId, OutputWorkspaceMap, WindowPlacement, WorkspaceId, WorkspaceLayout,
|
||||
WorkspaceSet,
|
||||
@@ -98,9 +99,11 @@ use crate::wallpaper::{WallpaperConfig, WallpaperState};
|
||||
use crate::window_rules::{WindowRule, resolve as resolve_window_rules};
|
||||
use crate::winit::NestedBackend;
|
||||
|
||||
const CLOSE_ANIMATION_DURATION: Duration = Duration::from_millis(140);
|
||||
const WORKSPACE_SLIDE_DURATION: Duration = Duration::from_millis(220);
|
||||
const WORKSPACE_SLIDE_FIRST_FRAME: Duration = Duration::from_millis(8);
|
||||
#[cfg(test)]
|
||||
use ferese_animation::CrossingPolicy;
|
||||
|
||||
#[cfg(test)]
|
||||
const SLIDE_SETTLE_TEST_BUDGET: Duration = Duration::from_secs(2);
|
||||
|
||||
fn workspace_swipe_is_current(
|
||||
outputs: &OutputWorkspaceMap,
|
||||
@@ -153,7 +156,7 @@ struct FocusSwipe {
|
||||
}
|
||||
|
||||
impl FocusSwipe {
|
||||
fn position(&self) -> f64 {
|
||||
fn distance(&self) -> f64 {
|
||||
let distance = self.destination - self.start;
|
||||
// When both columns already fit, give bounded drag feedback and settle
|
||||
// back to the unchanged viewport after selecting the next window.
|
||||
@@ -166,7 +169,29 @@ impl FocusSwipe {
|
||||
} else {
|
||||
distance
|
||||
};
|
||||
self.start + distance * self.progress
|
||||
distance
|
||||
}
|
||||
|
||||
fn position(&self) -> f64 {
|
||||
let progress = if self.to == self.from {
|
||||
ferese_animation::gesture::rubber_band(self.progress.max(0.0), 1.0, 0.55).0
|
||||
} else {
|
||||
self.progress.clamp(0.0, 1.0)
|
||||
};
|
||||
self.start + self.distance() * progress
|
||||
}
|
||||
|
||||
fn release_velocity(&self, bounded: f64, unbounded: f64) -> f64 {
|
||||
let velocity = if self.to == self.from {
|
||||
if self.progress <= 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
unbounded * ferese_animation::gesture::rubber_band(self.progress, 1.0, 0.55).1
|
||||
}
|
||||
} else {
|
||||
bounded
|
||||
};
|
||||
self.distance() * velocity
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,12 +200,13 @@ struct WorkspaceSlideItem {
|
||||
workspace: WorkspaceId,
|
||||
start: SlideOffset,
|
||||
target: SlideOffset,
|
||||
velocity: SlideOffset,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct WorkspaceSlide {
|
||||
items: Vec<WorkspaceSlideItem>,
|
||||
elapsed: Duration,
|
||||
spring: SpringConfig,
|
||||
held_progress: Option<f64>,
|
||||
gesture: Option<(WorkspaceId, WorkspaceId, SwipeDirection)>,
|
||||
}
|
||||
@@ -190,12 +216,14 @@ impl WorkspaceSlide {
|
||||
let movement = SlideOffset::for_swipe(direction);
|
||||
let mut items = previous
|
||||
.map(|slide| {
|
||||
let progress = slide.progress();
|
||||
let progress = slide.held_progress;
|
||||
slide
|
||||
.items
|
||||
.into_iter()
|
||||
.map(|mut item| {
|
||||
item.start = item.start.between(item.target, progress);
|
||||
if let Some(progress) = progress {
|
||||
item.start = item.start.between(item.target, progress);
|
||||
}
|
||||
item
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
@@ -209,6 +237,7 @@ impl WorkspaceSlide {
|
||||
workspace: from,
|
||||
start: SlideOffset::default(),
|
||||
target: movement,
|
||||
velocity: SlideOffset::default(),
|
||||
});
|
||||
}
|
||||
if let Some(item) = items.iter_mut().find(|item| item.workspace == to) {
|
||||
@@ -218,30 +247,64 @@ impl WorkspaceSlide {
|
||||
workspace: to,
|
||||
start: movement.opposite(),
|
||||
target: SlideOffset::default(),
|
||||
velocity: SlideOffset::default(),
|
||||
});
|
||||
}
|
||||
|
||||
Self {
|
||||
items,
|
||||
elapsed: WORKSPACE_SLIDE_FIRST_FRAME,
|
||||
spring: SpringConfig {
|
||||
stiffness: 320.0,
|
||||
damping: 2.0 * 320.0_f64.sqrt(),
|
||||
position_tolerance: 0.00001,
|
||||
velocity_tolerance: 0.00001,
|
||||
..SpringConfig::default()
|
||||
},
|
||||
held_progress: None,
|
||||
gesture: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn progress(&self) -> f64 {
|
||||
self.progress_at(Duration::ZERO)
|
||||
fn position(&self, item: &WorkspaceSlideItem, delta: Duration) -> SlideOffset {
|
||||
if let Some(progress) = self.held_progress {
|
||||
return item.start.between(item.target, progress);
|
||||
}
|
||||
let mut x = AnimatedValue {
|
||||
current: item.start.x,
|
||||
target: item.target.x,
|
||||
velocity: item.velocity.x,
|
||||
};
|
||||
let mut y = AnimatedValue {
|
||||
current: item.start.y,
|
||||
target: item.target.y,
|
||||
velocity: item.velocity.y,
|
||||
};
|
||||
x.advance(delta, self.spring);
|
||||
y.advance(delta, self.spring);
|
||||
SlideOffset {
|
||||
x: x.current,
|
||||
y: y.current,
|
||||
}
|
||||
}
|
||||
|
||||
fn progress_at(&self, delta: Duration) -> f64 {
|
||||
self.held_progress.unwrap_or_else(|| {
|
||||
let elapsed = self.elapsed.saturating_add(delta).min(WORKSPACE_SLIDE_DURATION);
|
||||
smoothstep(elapsed.as_secs_f64() / WORKSPACE_SLIDE_DURATION.as_secs_f64())
|
||||
})
|
||||
fn moving(&self) -> bool {
|
||||
self.items
|
||||
.iter()
|
||||
.any(|item| item.start != item.target || item.velocity != SlideOffset::default())
|
||||
}
|
||||
|
||||
fn release_with_velocity(&mut self, committed: bool, velocity: f64) {
|
||||
for item in &mut self.items {
|
||||
item.velocity = SlideOffset {
|
||||
x: (item.target.x - item.start.x) * velocity,
|
||||
y: (item.target.y - item.start.y) * velocity,
|
||||
};
|
||||
}
|
||||
self.release(committed);
|
||||
}
|
||||
|
||||
fn release(&mut self, committed: bool) {
|
||||
let progress = self.progress();
|
||||
let progress = self.held_progress.unwrap_or(0.0);
|
||||
for item in &mut self.items {
|
||||
let position = item.start.between(item.target, progress);
|
||||
if !committed && let Some((from, to, direction)) = self.gesture {
|
||||
@@ -255,12 +318,36 @@ impl WorkspaceSlide {
|
||||
}
|
||||
self.held_progress = None;
|
||||
self.gesture = None;
|
||||
self.elapsed = Duration::ZERO;
|
||||
}
|
||||
|
||||
fn advance(&mut self, delta: Duration) -> bool {
|
||||
self.elapsed = (self.elapsed + delta).min(WORKSPACE_SLIDE_DURATION);
|
||||
self.elapsed < WORKSPACE_SLIDE_DURATION
|
||||
if self.held_progress.is_some() {
|
||||
return true;
|
||||
}
|
||||
let mut moving = false;
|
||||
for item in &mut self.items {
|
||||
let mut x = AnimatedValue {
|
||||
current: item.start.x,
|
||||
target: item.target.x,
|
||||
velocity: item.velocity.x,
|
||||
};
|
||||
let mut y = AnimatedValue {
|
||||
current: item.start.y,
|
||||
target: item.target.y,
|
||||
velocity: item.velocity.y,
|
||||
};
|
||||
moving |= x.advance(delta, self.spring);
|
||||
moving |= y.advance(delta, self.spring);
|
||||
item.start = SlideOffset {
|
||||
x: x.current,
|
||||
y: y.current,
|
||||
};
|
||||
item.velocity = SlideOffset {
|
||||
x: x.velocity,
|
||||
y: y.velocity,
|
||||
};
|
||||
}
|
||||
moving
|
||||
}
|
||||
|
||||
fn contains(&self, workspace: WorkspaceId) -> bool {
|
||||
@@ -269,43 +356,16 @@ impl WorkspaceSlide {
|
||||
|
||||
#[cfg(test)]
|
||||
fn offset(&self, workspace: WorkspaceId, width: f64, height: f64) -> (f64, f64) {
|
||||
let progress = self.progress();
|
||||
let position = self
|
||||
.items
|
||||
.iter()
|
||||
.find(|item| item.workspace == workspace)
|
||||
.map(|item| item.start.between(item.target, progress))
|
||||
.map(|item| self.position(item, Duration::ZERO))
|
||||
.unwrap_or_default();
|
||||
(position.x * width, position.y * height)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
struct ClosingAnimation {
|
||||
progress: f64,
|
||||
close_sent: bool,
|
||||
}
|
||||
|
||||
impl ClosingAnimation {
|
||||
fn advance(&mut self, delta: Duration, animations_enabled: bool) -> bool {
|
||||
if self.close_sent {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.progress = if animations_enabled {
|
||||
(self.progress + delta.as_secs_f64() / CLOSE_ANIMATION_DURATION.as_secs_f64()).min(1.0)
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
if self.progress < 1.0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.close_sent = true;
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fn scaled_animation_duration(duration: Duration, enabled: bool, speed: f64) -> Duration {
|
||||
if enabled {
|
||||
duration.div_f64(speed)
|
||||
@@ -373,7 +433,7 @@ pub struct Ferese {
|
||||
pub(crate) inactive_dim: crate::config::InactiveDimSettings,
|
||||
animations_enabled: bool,
|
||||
animation_speed: f64,
|
||||
spring_config: SpringConfig,
|
||||
pub(crate) spring_config: SpringConfig,
|
||||
viewport_spring_config: SpringConfig,
|
||||
pub(crate) output_profiles: Vec<OutputProfile>,
|
||||
pub(crate) autostart: Vec<DaemonConfig>,
|
||||
@@ -784,6 +844,13 @@ impl Ferese {
|
||||
self.animation_speed = config.animation_speed;
|
||||
self.spring_config = config.spring_config;
|
||||
self.viewport_spring_config = config.viewport_spring_config;
|
||||
for slide in self.workspace_slides.values_mut() {
|
||||
slide.spring = SpringConfig {
|
||||
position_tolerance: 0.00001,
|
||||
velocity_tolerance: 0.00001,
|
||||
..self.viewport_spring_config
|
||||
};
|
||||
}
|
||||
self.output_profiles = config.output_profiles;
|
||||
self.autostart = config.autostart;
|
||||
if daemons_changed && let Some(runner) = self.daemons.clone() {
|
||||
@@ -940,12 +1007,6 @@ fn natural_floating_rect(bounds: Rect, size: ClientSize) -> Rect {
|
||||
)
|
||||
}
|
||||
|
||||
fn smoothstep(progress: f64) -> f64 {
|
||||
let progress = progress.clamp(0.0, 1.0);
|
||||
|
||||
progress * progress * (3.0 - 2.0 * progress)
|
||||
}
|
||||
|
||||
fn restored_scrolling_world_x(had_geometry: bool, visual_x: f64, viewport_x: f64, target_world_x: f64) -> f64 {
|
||||
if had_geometry {
|
||||
visual_x + viewport_x
|
||||
@@ -1044,20 +1105,60 @@ impl ClientData for ClientState {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn workspace_reversal_keeps_position_and_velocity() {
|
||||
let from = WorkspaceId(1);
|
||||
let to = WorkspaceId(2);
|
||||
let mut slide = WorkspaceSlide::new(None, from, to, SwipeDirection::Up);
|
||||
slide.advance(Duration::from_millis(40));
|
||||
let before: Vec<_> = slide
|
||||
.items
|
||||
.iter()
|
||||
.map(|item| (item.workspace, item.start, item.velocity))
|
||||
.collect();
|
||||
let reversed = WorkspaceSlide::new(Some(slide), to, from, SwipeDirection::Down);
|
||||
for (id, position, velocity) in before {
|
||||
let item = reversed.items.iter().find(|item| item.workspace == id).unwrap();
|
||||
assert_eq!(item.start, position);
|
||||
assert_eq!(item.velocity, velocity);
|
||||
assert!(velocity.y < 0.0);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn viewport_limit_uses_resisted_position_and_release_velocity() {
|
||||
let swipe = FocusSwipe {
|
||||
workspace: WorkspaceId(1),
|
||||
from: WindowId(1),
|
||||
to: WindowId(1),
|
||||
direction: Direction::Right,
|
||||
gesture_direction: SwipeDirection::Left,
|
||||
start: 20.0,
|
||||
destination: 20.0,
|
||||
progress: 2.0,
|
||||
};
|
||||
assert!(swipe.position() > 20.0 && swipe.position() < 84.0);
|
||||
assert!(swipe.release_velocity(0.0, 3.0) > 0.0);
|
||||
let mut released = AnimatedValue {
|
||||
current: swipe.position(),
|
||||
target: swipe.start,
|
||||
velocity: swipe.release_velocity(0.0, 3.0),
|
||||
};
|
||||
let initial = released;
|
||||
released.advance(Duration::from_micros(1), SpringConfig::default());
|
||||
assert!((released.current - initial.current - initial.velocity * 1e-6).abs() < 1e-6);
|
||||
released.advance(Duration::from_secs(2), SpringConfig::default());
|
||||
assert_eq!(released.current, swipe.start);
|
||||
}
|
||||
#[test]
|
||||
fn sampling_slide_progress_matches_advancing_without_mutating_the_slide() {
|
||||
let slide = WorkspaceSlide::new(None, WorkspaceId(1), WorkspaceId(2), SwipeDirection::Left);
|
||||
|
||||
for delta in [Duration::ZERO, Duration::from_millis(8), Duration::from_secs(1)] {
|
||||
for delta in [Duration::ZERO, Duration::from_millis(16), Duration::from_millis(100)] {
|
||||
let mut advanced = slide.clone();
|
||||
advanced.advance(delta);
|
||||
assert_eq!(slide.progress_at(delta), advanced.progress());
|
||||
assert_eq!(slide.elapsed, WORKSPACE_SLIDE_FIRST_FRAME);
|
||||
for (before, after) in slide.items.iter().zip(&advanced.items) {
|
||||
assert_eq!(slide.position(before, delta), after.start);
|
||||
}
|
||||
}
|
||||
|
||||
let mut held = slide;
|
||||
held.held_progress = Some(0.37);
|
||||
assert_eq!(held.progress_at(Duration::from_secs(1)), 0.37);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1080,15 +1181,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disabling_motion_finishes_an_in_progress_close_once() {
|
||||
let mut animation = ClosingAnimation::default();
|
||||
assert!(!animation.advance(Duration::from_millis(35), true));
|
||||
assert!(animation.advance(Duration::ZERO, false));
|
||||
assert_eq!(animation.progress, 1.0);
|
||||
assert!(!animation.advance(Duration::ZERO, false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_drag_cancels_when_focus_or_target_ownership_changes() {
|
||||
let mut outputs = OutputWorkspaceMap::default();
|
||||
@@ -1241,7 +1333,7 @@ mod tests {
|
||||
slide.release(committed);
|
||||
assert_eq!(slide.offset(from, 1000.0, 1000.0), (0.0, -400.0));
|
||||
assert_eq!(slide.offset(to, 1000.0, 1000.0), (0.0, 600.0));
|
||||
assert!(!slide.advance(WORKSPACE_SLIDE_DURATION));
|
||||
assert!(!slide.advance(SLIDE_SETTLE_TEST_BUDGET));
|
||||
let active = if committed { to } else { from };
|
||||
assert_eq!(slide.offset(active, 1000.0, 1000.0), (0.0, 0.0));
|
||||
}
|
||||
@@ -1253,14 +1345,14 @@ mod tests {
|
||||
let second = WorkspaceId(2);
|
||||
let third = WorkspaceId(3);
|
||||
let mut previous = WorkspaceSlide::new(None, first, second, SwipeDirection::Up);
|
||||
previous.advance(WORKSPACE_SLIDE_DURATION / 3);
|
||||
previous.advance(Duration::from_millis(40));
|
||||
let mut slide = WorkspaceSlide::new(Some(previous), second, third, SwipeDirection::Up);
|
||||
slide.gesture = Some((second, third, SwipeDirection::Up));
|
||||
slide.held_progress = Some(0.25);
|
||||
let position = slide.offset(second, 1000.0, 1000.0);
|
||||
slide.release(false);
|
||||
assert_eq!(slide.offset(second, 1000.0, 1000.0), position);
|
||||
slide.advance(WORKSPACE_SLIDE_DURATION);
|
||||
slide.advance(SLIDE_SETTLE_TEST_BUDGET);
|
||||
assert_eq!(slide.offset(second, 1000.0, 1000.0), (0.0, 0.0));
|
||||
}
|
||||
|
||||
@@ -1279,13 +1371,13 @@ mod tests {
|
||||
let mut slide = WorkspaceSlide::new(None, from, to, direction);
|
||||
assert!(slide.contains(from));
|
||||
assert!(slide.contains(to));
|
||||
assert!(slide.advance(WORKSPACE_SLIDE_DURATION / 2));
|
||||
assert!(slide.advance(Duration::from_millis(50)));
|
||||
let outgoing = slide.offset(from, width, height);
|
||||
let incoming = slide.offset(to, width, height);
|
||||
assert!((incoming.0 - outgoing.0).abs() == outgoing_target.0.abs());
|
||||
assert!((incoming.1 - outgoing.1).abs() == outgoing_target.1.abs());
|
||||
|
||||
assert!(!slide.advance(WORKSPACE_SLIDE_DURATION));
|
||||
assert!(!slide.advance(SLIDE_SETTLE_TEST_BUDGET));
|
||||
assert_eq!(slide.offset(from, width, height), outgoing_target);
|
||||
assert_eq!(slide.offset(to, width, height), (0.0, 0.0));
|
||||
}
|
||||
@@ -1297,7 +1389,7 @@ mod tests {
|
||||
let second = WorkspaceId(2);
|
||||
let third = WorkspaceId(3);
|
||||
let mut slide = WorkspaceSlide::new(None, first, second, SwipeDirection::Up);
|
||||
slide.advance(WORKSPACE_SLIDE_DURATION / 3);
|
||||
slide.advance(Duration::from_millis(40));
|
||||
let first_position = slide.offset(first, 1600.0, 900.0);
|
||||
let second_position = slide.offset(second, 1600.0, 900.0);
|
||||
|
||||
@@ -1404,24 +1496,6 @@ mod tests {
|
||||
assert_eq!(transient, Rect::new(280.0, 210.0, 640.0, 480.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn close_easing_is_bounded_and_symmetric() {
|
||||
assert_eq!(smoothstep(-1.0), 0.0);
|
||||
assert_eq!(smoothstep(0.5), 0.5);
|
||||
assert_eq!(smoothstep(2.0), 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn close_animation_delays_the_protocol_close_until_it_finishes() {
|
||||
let mut animation = ClosingAnimation::default();
|
||||
|
||||
assert!(!animation.advance(Duration::from_millis(70), true));
|
||||
assert_eq!(animation.progress, 0.5);
|
||||
assert!(animation.advance(Duration::from_millis(70), true));
|
||||
assert!(animation.close_sent);
|
||||
assert!(!animation.advance(Duration::from_secs(1), true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coupled_right_column_width_keeps_its_right_edge_stable() {
|
||||
let spring = SpringConfig {
|
||||
@@ -1430,6 +1504,7 @@ mod tests {
|
||||
damping: 2.0 * 320.0_f64.sqrt(),
|
||||
position_tolerance: 0.1,
|
||||
velocity_tolerance: 0.1,
|
||||
..SpringConfig::default()
|
||||
};
|
||||
let mut width = AnimatedValue::new(980.0);
|
||||
let mut viewport = AnimatedValue::new(495.0);
|
||||
|
||||
@@ -82,6 +82,7 @@ impl Ferese {
|
||||
if self.focus_swipe.is_some()
|
||||
|| !self.paused_workspaces.is_empty()
|
||||
|| !self.workspace_slides.is_empty()
|
||||
|| !self.render.closing.is_empty()
|
||||
|| !self.dismissing_popups.is_empty()
|
||||
|| self.render.snapshots().next().is_some()
|
||||
|| self.overview.needs_tick()
|
||||
@@ -192,11 +193,22 @@ impl Ferese {
|
||||
for (&id, record) in self.windows.records_mut() {
|
||||
let selected = selected_window == Some(id);
|
||||
let target = if selected { 1.0 } else { 0.0 };
|
||||
let focus = record
|
||||
.focus
|
||||
.get_or_insert_with(|| crate::dimming::DimAnimation::new(target));
|
||||
let focus = record.focus.get_or_insert_with(|| AnimatedValue::new(target));
|
||||
let previous = focus.current;
|
||||
active_animation |= focus.advance_visual(target, delta, duration);
|
||||
let changed = focus.target != target;
|
||||
focus.set_target(target);
|
||||
if self.animations_enabled {
|
||||
active_animation |= focus.advance(
|
||||
if changed { Duration::ZERO } else { delta },
|
||||
SpringConfig {
|
||||
position_tolerance: 0.001,
|
||||
velocity_tolerance: 0.001,
|
||||
..self.spring_config
|
||||
},
|
||||
);
|
||||
} else {
|
||||
focus.snap();
|
||||
}
|
||||
dim_changed |= previous != focus.current;
|
||||
}
|
||||
|
||||
@@ -217,19 +229,40 @@ impl Ferese {
|
||||
dim_changed |= previous != dim.current;
|
||||
}
|
||||
|
||||
let mut ready_to_close = Vec::new();
|
||||
for (id, record) in self.windows.records_mut() {
|
||||
let Some(animation) = &mut record.closing else { continue };
|
||||
|
||||
if animation.advance(delta, self.animations_enabled) {
|
||||
ready_to_close.push(*id);
|
||||
} else if !animation.close_sent {
|
||||
active_animation = true;
|
||||
for (_, record) in self.windows.records_mut() {
|
||||
if let Some(opening) = &mut record.opening {
|
||||
let active = if self.animations_enabled {
|
||||
opening.advance(
|
||||
delta,
|
||||
SpringConfig {
|
||||
position_tolerance: 0.00001,
|
||||
velocity_tolerance: 0.00001,
|
||||
..self.spring_config
|
||||
},
|
||||
)
|
||||
} else {
|
||||
opening.snap();
|
||||
false
|
||||
};
|
||||
active_animation |= active;
|
||||
if !active {
|
||||
record.opening = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for id in ready_to_close {
|
||||
self.send_window_close(id);
|
||||
let mut finished = Vec::new();
|
||||
self.render.closing.retain_mut(|window| {
|
||||
let active =
|
||||
self.animations_enabled && !self.session_lock.active() && window.advance(delta, self.spring_config);
|
||||
if !active {
|
||||
finished.push(window.id);
|
||||
}
|
||||
active_animation |= active;
|
||||
active
|
||||
});
|
||||
for id in finished {
|
||||
tracing::debug!(?id, "released close presentation");
|
||||
self.render.remove_window(id);
|
||||
}
|
||||
|
||||
// Include transactions expiring on this dispatch: their waiting time
|
||||
@@ -325,8 +358,7 @@ impl Ferese {
|
||||
viewport.current = swipe.position();
|
||||
viewport.velocity = 0.0;
|
||||
} else if self.animations_enabled {
|
||||
active_animation |=
|
||||
viewport.advance_with_policy(delta, self.viewport_spring_config, CrossingPolicy::NoCrossing);
|
||||
active_animation |= viewport.advance(delta, self.viewport_spring_config);
|
||||
} else {
|
||||
viewport.snap();
|
||||
}
|
||||
@@ -379,8 +411,7 @@ impl Ferese {
|
||||
if zooming {
|
||||
sync_scrolling_coordinates(geometry, world_x, viewport, true);
|
||||
} else if self.animations_enabled {
|
||||
active_animation |=
|
||||
world_x.advance_with_policy(delta, self.spring_config, CrossingPolicy::NoCrossing);
|
||||
active_animation |= world_x.advance(delta, self.spring_config);
|
||||
} else {
|
||||
world_x.snap();
|
||||
}
|
||||
@@ -392,7 +423,7 @@ impl Ferese {
|
||||
|
||||
if let Some((_, width)) = record.coupled_width.as_mut() {
|
||||
let width_active = if self.animations_enabled {
|
||||
width.advance_with_policy(delta, self.viewport_spring_config, CrossingPolicy::NoCrossing)
|
||||
width.advance(delta, self.viewport_spring_config)
|
||||
} else {
|
||||
width.snap();
|
||||
false
|
||||
@@ -462,6 +493,9 @@ impl Ferese {
|
||||
self.send_shell_snapshots();
|
||||
active_animation = true;
|
||||
}
|
||||
if !self.overview.is_active() {
|
||||
self.retarget_overview();
|
||||
}
|
||||
active_animation |= self
|
||||
.overview
|
||||
.advance(delta, self.spring_config, self.animations_enabled);
|
||||
@@ -525,10 +559,9 @@ impl Ferese {
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let progress = slide.progress_at(delta);
|
||||
|
||||
for item in &slide.items {
|
||||
let position = item.start.between(item.target, progress);
|
||||
let position = slide.position(item, delta);
|
||||
offsets.insert(
|
||||
item.workspace,
|
||||
(position.x * f64::from(size.w), position.y * f64::from(size.h)),
|
||||
@@ -573,7 +606,7 @@ fn record_needs_tick(
|
||||
mapped: impl FnOnce() -> bool,
|
||||
) -> bool {
|
||||
if record.resize.is_some()
|
||||
|| record.closing.as_ref().is_some_and(|close| !close.close_sent)
|
||||
|| record.opening.is_some()
|
||||
|| record.focus.as_ref().is_none_or(|motion| motion.needs_update(focus))
|
||||
{
|
||||
return true;
|
||||
@@ -610,7 +643,7 @@ mod tests {
|
||||
|
||||
WindowRecord {
|
||||
geometry: Some(geometry),
|
||||
focus: Some(DimAnimation::new(1.0)),
|
||||
focus: Some(AnimatedValue::new(1.0)),
|
||||
dimming: Some(DimAnimation::new(0.0)),
|
||||
..Default::default()
|
||||
}
|
||||
@@ -660,11 +693,11 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn close_requests_and_coupled_width_cleanup_still_get_their_final_tick() {
|
||||
fn opening_and_coupled_width_cleanup_still_get_their_final_tick() {
|
||||
let mut record = settled();
|
||||
record.closing = Some(ClosingAnimation::default());
|
||||
record.opening = Some(AnimatedValue::new(0.0));
|
||||
assert!(record_needs_tick(&record, 1.0, 0.0, || false));
|
||||
record.closing.as_mut().unwrap().close_sent = true;
|
||||
record.opening = None;
|
||||
assert!(!record_needs_tick(&record, 1.0, 0.0, || true));
|
||||
record.coupled_width = Some((WorkspaceId(1), AnimatedValue::new(400.0)));
|
||||
assert!(record_needs_tick(&record, 1.0, 0.0, || true));
|
||||
@@ -871,7 +904,7 @@ mod tests {
|
||||
// A settled transaction still has to flush its resume guard; otherwise
|
||||
// the next unrelated animation would inherit an old paused workspace.
|
||||
state.viewport_animations.get_mut(&workspace).unwrap().snap();
|
||||
state.windows.record_mut(id).unwrap().focus = Some(DimAnimation::new(0.0));
|
||||
state.windows.record_mut(id).unwrap().focus = Some(AnimatedValue::new(0.0));
|
||||
state.windows.set_transaction(
|
||||
id,
|
||||
crate::resize_transaction::ResizeTransaction::new(3.into(), Duration::from_millis(800)),
|
||||
|
||||
@@ -10,17 +10,13 @@ impl Ferese {
|
||||
(window.toplevel().is_some()
|
||||
&& self.window_content_ready(window)
|
||||
&& self.windows.geometry(id).is_some()
|
||||
&& self.output_workspaces.focused_output().is_some()
|
||||
&& self.windows.record(*id).is_none_or(|record| record.closing.is_none()))
|
||||
&& self.output_workspaces.focused_output().is_some())
|
||||
.then_some(*id)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn focus_preview_output(&self, id: WindowId) -> Option<OutputId> {
|
||||
if self.windows.record(id).is_some_and(|record| record.closing.is_some()) {
|
||||
return None;
|
||||
}
|
||||
self.workspaces
|
||||
.workspace_for_window(id)
|
||||
.and_then(|workspace| self.output_workspaces.output_for_workspace(workspace))
|
||||
|
||||
@@ -202,12 +202,16 @@ impl Ferese {
|
||||
}
|
||||
|
||||
pub fn visual_scale_for_window(&self, window: &Window) -> Option<(f64, f64)> {
|
||||
if !self.overview.is_presenting() {
|
||||
let id = self.windows.ids().get(window)?;
|
||||
if !self.overview.is_presenting() && self.windows.record(*id)?.opening.is_none() {
|
||||
return Some((1.0, 1.0));
|
||||
}
|
||||
let id = self.windows.ids().get(window)?;
|
||||
let geometry = self.windows.geometry(id)?;
|
||||
let source = geometry.client.committed_size?;
|
||||
let source = if self.overview.is_presenting() {
|
||||
geometry.client.committed_size?
|
||||
} else {
|
||||
ClientSize::from_rect(geometry.visual.current)
|
||||
};
|
||||
let presented = self.presented_window_rect(*id)?;
|
||||
|
||||
if source.width <= 0 || source.height <= 0 {
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
use super::*;
|
||||
use ferese_animation::AnimatedRect;
|
||||
use smithay::backend::renderer::Texture;
|
||||
use smithay::wayland::compositor::{BufferAssignment, SurfaceAttributes};
|
||||
|
||||
impl Ferese {
|
||||
pub(crate) fn capture_close_before_commit(&mut self, surface: &WlSurface) {
|
||||
// At this point cached attributes contain the committed removal, but
|
||||
// on_commit_buffer_handler has not reset the previous renderer state.
|
||||
let removed = with_states(surface, |states| {
|
||||
matches!(
|
||||
states.cached_state.get::<SurfaceAttributes>().current().buffer,
|
||||
Some(BufferAssignment::Removed)
|
||||
)
|
||||
});
|
||||
if removed
|
||||
&& let Some(window) = self
|
||||
.windows
|
||||
.id_for_surface(surface)
|
||||
.and_then(|id| self.windows.window(id))
|
||||
.cloned()
|
||||
{
|
||||
self.retain_closed_window(&window);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn retain_closed_window(&mut self, window: &Window) {
|
||||
if !self.animations_enabled || self.session_lock.active() || self.render.closing.len() >= 64 {
|
||||
return;
|
||||
}
|
||||
let Some(&id) = self.windows.ids().get(window) else {
|
||||
return;
|
||||
};
|
||||
if self.render.closing.iter().any(|window| window.id == id) || !self.window_content_ready(window) {
|
||||
return;
|
||||
}
|
||||
let Some(output) = self
|
||||
.space
|
||||
.outputs()
|
||||
.find(|output| self.window_belongs_to_output(id, output))
|
||||
.cloned()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some(output_id) = self.output_id(&output) else {
|
||||
return;
|
||||
};
|
||||
let Some(rect) = self.presented_window_rect(id) else {
|
||||
return;
|
||||
};
|
||||
let record = self.windows.record(id).unwrap();
|
||||
let geometry = record.geometry.unwrap();
|
||||
let decorations = geometry.decorations.clamp(0.0, 1.0);
|
||||
let focus = record.focus.as_ref().map_or(0.0, |focus| focus.current.clamp(0.0, 1.0));
|
||||
let dim = record.dimming.as_ref().map_or(0.0, |dim| dim.current);
|
||||
let mut opacity = record.opening.unwrap_or_else(|| AnimatedValue::new(1.0));
|
||||
opacity.set_target(0.0);
|
||||
let mut bounds = AnimatedRect::new(rect);
|
||||
bounds.velocity = self.overview.presented_velocity(id).unwrap_or(geometry.visual.velocity);
|
||||
if !self.overview.is_presenting()
|
||||
&& let Some(slide) = self.workspace_slides.get(&output_id)
|
||||
&& let Some(workspace) = self.workspaces.workspace_for_window(id)
|
||||
&& let Some(item) = slide.items.iter().find(|item| item.workspace == workspace)
|
||||
&& let Some(area) = self.space.output_geometry(&output)
|
||||
{
|
||||
bounds.velocity.x += item.velocity.x * f64::from(area.size.w);
|
||||
bounds.velocity.y += item.velocity.y * f64::from(area.size.h);
|
||||
}
|
||||
if let Some(opening) = record.opening {
|
||||
bounds.velocity = crate::presentation::scaled_visual_velocity(
|
||||
self.overview.presented_rect(id, geometry.visual.current),
|
||||
bounds.velocity,
|
||||
0.97 + 0.03 * opening.current,
|
||||
0.03 * opening.velocity,
|
||||
);
|
||||
}
|
||||
bounds.set_target(crate::presentation::scaled_visual_rect(rect, 0.98));
|
||||
let mut source_geometry = window.geometry();
|
||||
if !self.overview.is_presenting() {
|
||||
let size = ClientSize::from_rect(geometry.visual.current);
|
||||
source_geometry.size = (size.width, size.height).into();
|
||||
}
|
||||
let order: Vec<_> = if self.overview.is_active() {
|
||||
self.windows
|
||||
.overview_windows()
|
||||
.filter_map(|window| self.windows.ids().get(window).copied())
|
||||
.collect()
|
||||
} else {
|
||||
self.space
|
||||
.elements()
|
||||
.rev()
|
||||
.filter_map(|window| self.windows.ids().get(window).copied())
|
||||
.collect()
|
||||
};
|
||||
let below = order
|
||||
.iter()
|
||||
.position(|candidate| *candidate == id)
|
||||
.and_then(|index| order.get(index + 1))
|
||||
.copied();
|
||||
let used = self.render.snapshots().map(|snapshot| snapshot.bytes()).sum::<usize>()
|
||||
+ self
|
||||
.render
|
||||
.closing
|
||||
.iter()
|
||||
.map(|window| window.snapshot.bytes())
|
||||
.sum::<usize>();
|
||||
let remaining = crate::presentation::SNAPSHOT_BUDGET.saturating_sub(used);
|
||||
let scale = output.current_scale().fractional_scale();
|
||||
let result = if let Some(backend) = &self.nested_backend {
|
||||
match backend.try_borrow_mut() {
|
||||
Ok(mut backend) => crate::render::capture_resize_snapshot(
|
||||
backend.renderer(),
|
||||
window,
|
||||
source_geometry,
|
||||
scale,
|
||||
remaining,
|
||||
),
|
||||
Err(_) => return,
|
||||
}
|
||||
} else if let Some(backend) = &mut self.direct_backend {
|
||||
backend.capture_resize_snapshot(window, source_geometry, &output, remaining)
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
match result {
|
||||
Ok(Some(snapshot)) => {
|
||||
tracing::debug!(?id, bytes = snapshot.bytes(), "retained close presentation");
|
||||
let size = snapshot.texture.size();
|
||||
let material = window.toplevel().and_then(|toplevel| {
|
||||
let surface = toplevel.wl_surface();
|
||||
let (role, generation) = crate::effects::surface_role(surface)?;
|
||||
(role == crate::effects::SemanticRole::Modal).then(|| {
|
||||
(
|
||||
surface.clone(),
|
||||
role,
|
||||
generation,
|
||||
crate::effects::surface_opacity(surface),
|
||||
)
|
||||
})
|
||||
});
|
||||
let radius = if material.is_some() {
|
||||
self.theme_settings.material_radius
|
||||
} else {
|
||||
self.theme_settings.window_radius
|
||||
} * decorations;
|
||||
let client = window.geometry().size;
|
||||
let fill = (!self.overview.is_presenting()
|
||||
&& material.is_none()
|
||||
&& (client.w < source_geometry.size.w || client.h < source_geometry.size.h))
|
||||
.then_some(self.theme_settings.surface_base_color.0);
|
||||
self.render.closing.push(crate::render::ClosedWindow {
|
||||
id,
|
||||
output: output_id,
|
||||
below,
|
||||
bounds,
|
||||
opacity,
|
||||
snapshot,
|
||||
source: Rectangle::from_size((f64::from(size.w), f64::from(size.h)).into()),
|
||||
radius,
|
||||
shape: crate::render::window_corner_shape(window),
|
||||
focus,
|
||||
decorations,
|
||||
dim,
|
||||
fill,
|
||||
material,
|
||||
});
|
||||
self.last_animation_tick = Instant::now();
|
||||
self.defer_output_redraw(output);
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(error) => tracing::debug!(%error, ?id, "close snapshot unavailable"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,9 +34,10 @@ impl Ferese {
|
||||
if workspace.fullscreen.is_some() || !matches!(workspace.layout, WorkspaceLayout::Scrolling(_)) {
|
||||
return;
|
||||
}
|
||||
let Ok(Some(to)) = workspace.layout.directional_neighbor(from, direction, bounds) else {
|
||||
let Ok(neighbor) = workspace.layout.directional_neighbor(from, direction, bounds) else {
|
||||
return;
|
||||
};
|
||||
let to = neighbor.unwrap_or(from);
|
||||
let workspace_id = workspace.id;
|
||||
let start = self
|
||||
.viewport_animations
|
||||
@@ -45,7 +46,8 @@ impl Ferese {
|
||||
.or_else(|| workspace.layout.viewport_x())
|
||||
.unwrap_or(0.0);
|
||||
let mut candidate = workspace.layout.clone();
|
||||
if let WorkspaceLayout::Scrolling(layout) = &mut candidate
|
||||
if to != from
|
||||
&& let WorkspaceLayout::Scrolling(layout) = &mut candidate
|
||||
&& layout.slide_focus_from(from, to).is_err()
|
||||
{
|
||||
return;
|
||||
@@ -63,7 +65,11 @@ impl Ferese {
|
||||
direction,
|
||||
gesture_direction,
|
||||
start,
|
||||
destination: candidate.viewport_x().unwrap_or(start),
|
||||
destination: if to == from {
|
||||
start
|
||||
} else {
|
||||
candidate.viewport_x().unwrap_or(start)
|
||||
},
|
||||
progress,
|
||||
});
|
||||
self.swipe.mark_preview_started();
|
||||
@@ -91,7 +97,9 @@ impl Ferese {
|
||||
// Release from the last input position even if no frame rendered that update.
|
||||
if current && let Some(viewport) = self.viewport_animations.get_mut(&swipe.workspace) {
|
||||
viewport.current = swipe.position();
|
||||
viewport.velocity = 0.0;
|
||||
viewport.velocity = swipe
|
||||
.release_velocity(self.swipe.release_velocity, self.swipe.unbounded_release_velocity)
|
||||
/ self.animation_speed;
|
||||
}
|
||||
let neighbor = self.output_bounds().and_then(|bounds| {
|
||||
self.workspaces
|
||||
@@ -460,6 +468,11 @@ impl Ferese {
|
||||
};
|
||||
let previous = self.workspace_slides.remove(&output);
|
||||
let mut slide = WorkspaceSlide::new(previous, from, to, direction);
|
||||
slide.spring = SpringConfig {
|
||||
position_tolerance: 0.00001,
|
||||
velocity_tolerance: 0.00001,
|
||||
..self.viewport_spring_config
|
||||
};
|
||||
slide.held_progress = Some(progress);
|
||||
slide.gesture = Some((from, to, direction));
|
||||
self.workspace_slides.insert(output, slide);
|
||||
@@ -481,7 +494,7 @@ impl Ferese {
|
||||
&& self.activate_managed_workspace_from_swipe(to, expected);
|
||||
if !committed {
|
||||
if let Some(slide) = self.workspace_slides.get_mut(&output) {
|
||||
slide.release(false);
|
||||
slide.release_with_velocity(false, self.swipe.release_velocity / self.animation_speed);
|
||||
}
|
||||
self.last_animation_tick = Instant::now();
|
||||
self.relayout();
|
||||
@@ -497,26 +510,55 @@ impl Ferese {
|
||||
workspace: WorkspaceId,
|
||||
slide_direction: Option<SwipeDirection>,
|
||||
) {
|
||||
if owner != requested_output {
|
||||
return;
|
||||
}
|
||||
let mut previous_slide = self.workspace_slides.remove(&owner);
|
||||
if previous == Some(workspace) {
|
||||
if let Some(slide) = previous_slide {
|
||||
self.workspace_slides.insert(owner, slide);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if owner == requested_output
|
||||
&& let Some(slide) = previous_slide.as_mut()
|
||||
&& slide
|
||||
.gesture
|
||||
.is_some_and(|(_, to, direction)| to == workspace && Some(direction) == slide_direction)
|
||||
{
|
||||
slide.release(true);
|
||||
slide.release_with_velocity(true, self.swipe.release_velocity / self.animation_speed);
|
||||
self.last_animation_tick = Instant::now();
|
||||
self.workspace_slides.insert(owner, previous_slide.unwrap());
|
||||
return;
|
||||
}
|
||||
let slide_direction = slide_direction.or_else(|| {
|
||||
let from = previous?;
|
||||
let ids = self
|
||||
.output_workspaces
|
||||
.ordered_workspace_ids(&self.workspaces, owner)
|
||||
.collect::<Vec<_>>();
|
||||
let from = ids.iter().position(|id| *id == from)?;
|
||||
let to = ids.iter().position(|id| *id == workspace)?;
|
||||
Some(if to > from {
|
||||
SwipeDirection::Up
|
||||
} else {
|
||||
SwipeDirection::Down
|
||||
})
|
||||
});
|
||||
if owner == requested_output
|
||||
&& let (Some(from), Some(direction)) = (previous, slide_direction)
|
||||
&& from != workspace
|
||||
&& self.animations_enabled
|
||||
&& !self.overview.is_presenting()
|
||||
{
|
||||
self.last_animation_tick = Instant::now();
|
||||
self.workspace_slides
|
||||
.insert(owner, WorkspaceSlide::new(previous_slide, from, workspace, direction));
|
||||
let mut slide = WorkspaceSlide::new(previous_slide, from, workspace, direction);
|
||||
slide.spring = SpringConfig {
|
||||
position_tolerance: 0.00001,
|
||||
velocity_tolerance: 0.00001,
|
||||
..self.viewport_spring_config
|
||||
};
|
||||
self.workspace_slides.insert(owner, slide);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,13 +7,15 @@ pub(crate) struct FrameScene {
|
||||
pub overview: crate::overview::OverviewMotion,
|
||||
// Plane selection and scheduling reuse the activity sampled with this scene.
|
||||
pub animating: bool,
|
||||
pub delta: Duration,
|
||||
}
|
||||
|
||||
pub(crate) struct WindowFrame {
|
||||
pub geometry: WindowGeometry,
|
||||
pub rect: Rect,
|
||||
pub close_scale: f64,
|
||||
pub close_alpha: f32,
|
||||
pub scale_content: bool,
|
||||
pub native_size: Option<ClientSize>,
|
||||
pub alpha: f32,
|
||||
pub focus: f64,
|
||||
pub dim: f64,
|
||||
}
|
||||
@@ -68,7 +70,12 @@ impl Ferese {
|
||||
}
|
||||
|
||||
pub(crate) fn output_has_animations(&self, output: &Output) -> bool {
|
||||
if self.overview.is_animating(self.spring_config)
|
||||
if self
|
||||
.render
|
||||
.closing
|
||||
.iter()
|
||||
.any(|window| Some(window.output) == self.output_id(output))
|
||||
|| self.overview.is_animating(self.spring_config)
|
||||
|| self
|
||||
.dismissing_popups
|
||||
.iter()
|
||||
@@ -80,7 +87,7 @@ impl Ferese {
|
||||
if self
|
||||
.output_id(output)
|
||||
.and_then(|id| self.workspace_slides.get(&id))
|
||||
.is_some_and(|slide| slide.held_progress.is_none() && slide.elapsed < WORKSPACE_SLIDE_DURATION)
|
||||
.is_some_and(|slide| slide.held_progress.is_none() && slide.moving())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -95,8 +102,8 @@ impl Ferese {
|
||||
}
|
||||
|
||||
if record.resize.is_some()
|
||||
|| record.opening.is_some()
|
||||
|| self.render.snapshot(&id).is_some()
|
||||
|| record.closing.as_ref().is_some_and(|close| !close.close_sent)
|
||||
|| record.focus.as_ref().is_some_and(|focus| focus.is_animating())
|
||||
|| record.dimming.as_ref().is_some_and(|dim| dim.is_animating())
|
||||
{
|
||||
@@ -117,15 +124,10 @@ impl Ferese {
|
||||
.focus_swipe
|
||||
.as_ref()
|
||||
.is_some_and(|swipe| swipe.workspace == workspace);
|
||||
!held
|
||||
&& viewport.advance_with_policy(
|
||||
Duration::ZERO,
|
||||
self.viewport_spring_config,
|
||||
CrossingPolicy::NoCrossing,
|
||||
)
|
||||
!held && viewport.advance(Duration::ZERO, self.viewport_spring_config)
|
||||
}) || record.coupled_width.as_ref().is_some_and(|(_, width)| {
|
||||
let mut width = *width;
|
||||
width.advance_with_policy(Duration::ZERO, self.viewport_spring_config, CrossingPolicy::NoCrossing)
|
||||
width.advance(Duration::ZERO, self.viewport_spring_config)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -187,12 +189,19 @@ impl Ferese {
|
||||
rect.y += y;
|
||||
}
|
||||
|
||||
let mut close = record.closing.as_ref().copied().unwrap_or_default();
|
||||
if record.closing.is_some() && !delta.is_zero() {
|
||||
close.advance(delta, true);
|
||||
let mut opening = record.opening.unwrap_or_else(|| AnimatedValue::new(1.0));
|
||||
if !self.animations_enabled {
|
||||
opening.snap();
|
||||
} else if !delta.is_zero() {
|
||||
opening.advance(
|
||||
delta,
|
||||
SpringConfig {
|
||||
position_tolerance: 0.00001,
|
||||
velocity_tolerance: 0.00001,
|
||||
..self.spring_config
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let eased = smoothstep(close.progress);
|
||||
let focused = if self.overview.is_active() {
|
||||
self.overview_selected(id)
|
||||
} else {
|
||||
@@ -211,14 +220,31 @@ impl Ferese {
|
||||
motion.current
|
||||
};
|
||||
|
||||
let mut focus = record
|
||||
.focus
|
||||
.unwrap_or_else(|| AnimatedValue::new(if focused { 1.0 } else { 0.0 }));
|
||||
if !delta.is_zero() {
|
||||
focus.advance(
|
||||
delta,
|
||||
SpringConfig {
|
||||
position_tolerance: 0.001,
|
||||
velocity_tolerance: 0.001,
|
||||
..self.spring_config
|
||||
},
|
||||
);
|
||||
}
|
||||
let presence_scale = 0.97 + 0.03 * opening.current;
|
||||
let native_size = (!overview.is_presenting() && presence_scale != 1.0).then(|| ClientSize::from_rect(rect));
|
||||
let rect = crate::presentation::scaled_visual_rect(rect, presence_scale);
|
||||
windows.insert(
|
||||
id,
|
||||
WindowFrame {
|
||||
geometry,
|
||||
rect,
|
||||
close_scale: 1.0 - eased * 0.02,
|
||||
close_alpha: (1.0 - eased) as f32,
|
||||
focus: sample_dim(record.focus.as_ref(), if focused { 1.0 } else { 0.0 }),
|
||||
scale_content: overview.is_presenting(),
|
||||
native_size,
|
||||
alpha: opening.current.clamp(0.0, 1.0) as f32,
|
||||
focus: focus.current.clamp(0.0, 1.0),
|
||||
dim: sample_dim(record.dimming.as_ref(), 0.0),
|
||||
},
|
||||
);
|
||||
@@ -228,6 +254,7 @@ impl Ferese {
|
||||
windows,
|
||||
overview,
|
||||
animating,
|
||||
delta,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -252,9 +279,9 @@ fn predict_geometry(
|
||||
if let Some((mut world, mut viewport, held)) = world
|
||||
&& !zooming
|
||||
{
|
||||
world.advance_with_policy(delta, spring, CrossingPolicy::NoCrossing);
|
||||
world.advance(delta, spring);
|
||||
if !held {
|
||||
viewport.advance_with_policy(delta, viewport_spring, CrossingPolicy::NoCrossing);
|
||||
viewport.advance(delta, viewport_spring);
|
||||
}
|
||||
|
||||
predicted.visual.current.x = world.current - viewport.current;
|
||||
@@ -262,7 +289,7 @@ fn predict_geometry(
|
||||
}
|
||||
|
||||
if let Some(mut width) = width {
|
||||
width.advance_with_policy(delta, viewport_spring, CrossingPolicy::NoCrossing);
|
||||
width.advance(delta, viewport_spring);
|
||||
predicted.visual.current.width = width.current;
|
||||
predicted.visual.velocity.width = width.velocity;
|
||||
}
|
||||
@@ -454,7 +481,7 @@ mod tests {
|
||||
state.focused_window = Some(id);
|
||||
state
|
||||
.windows
|
||||
.update(id, |record| record.focus = Some(crate::dimming::DimAnimation::new(1.0)));
|
||||
.update(id, |record| record.focus = Some(AnimatedValue::new(1.0)));
|
||||
state.focused_window = Some(other_id);
|
||||
assert!(state.output_has_pending_visual_changes(&output));
|
||||
state.animations_enabled = false;
|
||||
|
||||
@@ -21,9 +21,10 @@ pub(crate) struct WindowRecord {
|
||||
pub title: String,
|
||||
pub metadata_revision: u64,
|
||||
pub resize: Option<ResizeTransaction>,
|
||||
pub focus: Option<crate::dimming::DimAnimation>,
|
||||
pub focus: Option<AnimatedValue>,
|
||||
pub dimming: Option<crate::dimming::DimAnimation>,
|
||||
pub(super) closing: Option<super::ClosingAnimation>,
|
||||
pub opening: Option<AnimatedValue>,
|
||||
pub mapped_once: bool,
|
||||
pub world_x: Option<(WorkspaceId, AnimatedValue)>,
|
||||
pub coupled_width: Option<(WorkspaceId, AnimatedValue)>,
|
||||
pub maximized: bool,
|
||||
@@ -302,8 +303,8 @@ mod tests {
|
||||
record.resize_anchor = Some((true, false, Rect::new(0., 0., 800., 600.)));
|
||||
record.column_width_pending = true;
|
||||
record.rules_applied = true;
|
||||
record.focus = Some(crate::dimming::DimAnimation::new(1.0));
|
||||
record.closing = Some(super::super::ClosingAnimation::default());
|
||||
record.focus = Some(AnimatedValue::new(1.0));
|
||||
record.opening = Some(AnimatedValue::new(0.0));
|
||||
record.world_x = Some((WorkspaceId(1), AnimatedValue::new(50.0)));
|
||||
record.coupled_width = Some((WorkspaceId(1), AnimatedValue::new(800.0)));
|
||||
});
|
||||
|
||||
@@ -417,6 +417,7 @@ impl Ferese {
|
||||
}
|
||||
|
||||
pub fn remove_tiled_window(&mut self, window: &Window) {
|
||||
self.retain_closed_window(window);
|
||||
// Uncommitted toplevels have not entered a workspace yet.
|
||||
self.space.unmap_elem(window);
|
||||
let Some(id) = self.windows.remove(window) else {
|
||||
@@ -493,7 +494,13 @@ impl Ferese {
|
||||
return;
|
||||
}
|
||||
|
||||
let used: usize = self.render.snapshots().map(|snapshot| snapshot.bytes()).sum();
|
||||
let used: usize = self.render.snapshots().map(|snapshot| snapshot.bytes()).sum::<usize>()
|
||||
+ self
|
||||
.render
|
||||
.closing
|
||||
.iter()
|
||||
.map(|window| window.snapshot.bytes())
|
||||
.sum::<usize>();
|
||||
let remaining = crate::presentation::SNAPSHOT_BUDGET.saturating_sub(used);
|
||||
let result = if let Some(backend) = &self.nested_backend {
|
||||
// Commit dispatch does not run inside the Winit event callback;
|
||||
@@ -557,6 +564,20 @@ impl Ferese {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(record) = self.windows.record_mut(id)
|
||||
&& !record.mapped_once
|
||||
{
|
||||
record.mapped_once = true;
|
||||
if self.animations_enabled {
|
||||
record.opening = Some(AnimatedValue {
|
||||
current: 0.0,
|
||||
target: 1.0,
|
||||
velocity: 0.0,
|
||||
});
|
||||
self.last_animation_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
|
||||
let fullscreen = self
|
||||
.workspaces
|
||||
.workspace_for_window(id)
|
||||
@@ -648,35 +669,16 @@ impl Ferese {
|
||||
}
|
||||
|
||||
pub fn close_focused_window(&mut self) {
|
||||
let Some(focused) = self.focused_window else {
|
||||
return;
|
||||
};
|
||||
|
||||
if !self.animations_enabled {
|
||||
if let Some(focused) = self.focused_window {
|
||||
self.send_window_close(focused);
|
||||
return;
|
||||
}
|
||||
|
||||
self.windows.update(focused, |record| {
|
||||
record.closing.get_or_insert_default();
|
||||
});
|
||||
crate::backends::direct::render_window(self, focused);
|
||||
}
|
||||
|
||||
pub(crate) fn close_managed_window(&mut self, id: WindowId) -> bool {
|
||||
if self.windows.window(id).is_none() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.animations_enabled {
|
||||
self.windows.update(id, |record| {
|
||||
record.closing.get_or_insert_default();
|
||||
});
|
||||
crate::backends::direct::render_window(self, id);
|
||||
} else {
|
||||
self.send_window_close(id);
|
||||
}
|
||||
|
||||
self.send_window_close(id);
|
||||
true
|
||||
}
|
||||
|
||||
|
||||
@@ -95,6 +95,7 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
|
||||
let mut backend = backend.borrow_mut();
|
||||
match event {
|
||||
WinitEvent::Resized { size, scale_factor } => {
|
||||
tracing::debug!(target: "ferese::nested_input", ?size, scale_factor, "host resized nested output");
|
||||
let scale = normalized_scale(scale_factor);
|
||||
let rate = backend
|
||||
.window()
|
||||
@@ -115,7 +116,13 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
|
||||
}
|
||||
backend.window().request_redraw();
|
||||
}
|
||||
WinitEvent::Input(event) => state.process_input_event(event),
|
||||
WinitEvent::Input(event) => {
|
||||
let kind = std::mem::discriminant(&event);
|
||||
state.process_input_event(event);
|
||||
tracing::debug!(target: "ferese::nested_input", ?kind,
|
||||
pointer = ?state.seat.get_pointer().map(|pointer| pointer.current_location()),
|
||||
"host input in nested output");
|
||||
}
|
||||
WinitEvent::Redraw => {
|
||||
state.advance_animations(Instant::now());
|
||||
let age = backend.buffer_age().unwrap_or(0);
|
||||
|
||||
Reference in new issue
Block a user