Add unified window focus opacity and dimming effects
This commit is contained in:
1 parent
02ec7e5199
commit
bbc5b27c25
21 files changed
+553
-342
No files matched your search
@@ -125,14 +125,9 @@ impl Ferese {
|
||||
|
||||
self.windows.records().any(|(&id, record)| {
|
||||
let focus = if selected == Some(id) { 1.0 } else { 0.0 };
|
||||
let dim = crate::dimming::target(
|
||||
self.inactive_dim,
|
||||
self.focused_window,
|
||||
id,
|
||||
self.overview.is_presenting(),
|
||||
);
|
||||
let activation = crate::focus_effect::target(self.focus_effect, self.focused_window, id);
|
||||
|
||||
record_needs_tick(record, focus, dim, || {
|
||||
record_needs_tick(record, focus, activation, || {
|
||||
self.windows
|
||||
.window(id)
|
||||
.is_some_and(|window| self.space.element_location(window).is_some())
|
||||
@@ -196,14 +191,14 @@ impl Ferese {
|
||||
active
|
||||
});
|
||||
|
||||
let dim_settings = self.inactive_dim;
|
||||
let duration = if self.animations_enabled {
|
||||
dim_settings.duration_ms
|
||||
let effect = self.focus_effect;
|
||||
let duration = if self.animations_enabled && effect.enabled {
|
||||
effect.duration_ms
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let mut dim_changed = false;
|
||||
let mut appearance_changed = false;
|
||||
// Include hidden workspace windows: overview can present them too.
|
||||
// Read selection once so these immutable fields can be borrowed alongside
|
||||
// each record without allocating a temporary window-ID vector.
|
||||
@@ -228,7 +223,7 @@ impl Ferese {
|
||||
} else {
|
||||
focus.snap();
|
||||
}
|
||||
dim_changed |= previous != focus.current;
|
||||
appearance_changed |= previous != focus.current;
|
||||
let shadow = record.shadow.get_or_insert_with(|| AnimatedValue::new(target));
|
||||
let previous = shadow.current;
|
||||
let changed = shadow.target != target;
|
||||
@@ -241,24 +236,15 @@ impl Ferese {
|
||||
} else {
|
||||
shadow.snap();
|
||||
}
|
||||
dim_changed |= previous != shadow.current;
|
||||
}
|
||||
appearance_changed |= previous != shadow.current;
|
||||
|
||||
for window in self.space.elements() {
|
||||
let Some(id) = self.windows.ids().get(window).copied() else {
|
||||
continue;
|
||||
};
|
||||
let target = crate::dimming::target(dim_settings, self.focused_window, id, self.overview.is_presenting());
|
||||
let Some(record) = self.windows.record_mut(id) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let dim = record
|
||||
.dimming
|
||||
.get_or_insert_with(|| crate::dimming::DimAnimation::new(target));
|
||||
let previous = dim.current;
|
||||
active_animation |= dim.advance_visual(target, delta, duration);
|
||||
dim_changed |= previous != dim.current;
|
||||
let target = crate::focus_effect::target(effect, self.focused_window, id);
|
||||
let transition = record
|
||||
.focus_transition
|
||||
.get_or_insert_with(|| crate::focus_effect::BoundedFade::new(target));
|
||||
let previous = transition.current;
|
||||
active_animation |= transition.advance_visual(target, delta, duration);
|
||||
appearance_changed |= previous != transition.current;
|
||||
}
|
||||
|
||||
for (_, record) in self.windows.records_mut() {
|
||||
@@ -540,7 +526,7 @@ impl Ferese {
|
||||
self.refresh_workspace_slide_offsets();
|
||||
self.sync_window_stacking();
|
||||
|
||||
active_animation |= dim_changed;
|
||||
active_animation |= appearance_changed;
|
||||
active_animation
|
||||
}
|
||||
|
||||
@@ -640,7 +626,7 @@ pub(super) fn sync_scrolling_coordinates(
|
||||
fn record_needs_tick(
|
||||
record: &super::window_registry::WindowRecord,
|
||||
focus: f64,
|
||||
dim: f64,
|
||||
activation: f64,
|
||||
mapped: impl FnOnce() -> bool,
|
||||
) -> bool {
|
||||
if record.resize.is_some()
|
||||
@@ -652,7 +638,10 @@ fn record_needs_tick(
|
||||
}
|
||||
|
||||
let moving = |value: &AnimatedValue| value.current != value.target || value.velocity != 0.0;
|
||||
let pending = record.dimming.as_ref().is_none_or(|motion| motion.needs_update(dim))
|
||||
let pending = record
|
||||
.focus_transition
|
||||
.as_ref()
|
||||
.is_none_or(|motion| motion.needs_update(activation))
|
||||
|| record.world_x.as_ref().is_some_and(|(_, world)| moving(world))
|
||||
|| record.coupled_width.is_some()
|
||||
|| record.geometry.is_some_and(|geometry| {
|
||||
@@ -665,7 +654,6 @@ fn record_needs_tick(
|
||||
&& geometry.client.last_configured_size != Some(ClientSize::from_rect(geometry.logical)))
|
||||
});
|
||||
|
||||
// Hidden geometry and dimming do not advance in the full path.
|
||||
// A stale hidden target must not keep waking the whole desktop.
|
||||
pending && mapped()
|
||||
}
|
||||
@@ -684,24 +672,24 @@ mod tests {
|
||||
geometry: Some(geometry),
|
||||
focus: Some(AnimatedValue::new(1.0)),
|
||||
shadow: Some(AnimatedValue::new(1.0)),
|
||||
dimming: Some(DimAnimation::new(0.0)),
|
||||
focus_transition: Some(BoundedFade::new(1.0)),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn idle_check_skips_mapping_lookup_but_new_focus_and_dim_targets_wake_it() {
|
||||
fn idle_check_skips_mapping_lookup_but_new_focus_and_activation_targets_wake_it() {
|
||||
let record = settled();
|
||||
|
||||
for _ in 0..100 {
|
||||
assert!(!record_needs_tick(&record, 1.0, 0.0, || panic!(
|
||||
assert!(!record_needs_tick(&record, 1.0, 1.0, || panic!(
|
||||
"settled record needs no mapping lookup"
|
||||
)));
|
||||
}
|
||||
|
||||
assert!(record_needs_tick(&record, 0.0, 0.0, || true));
|
||||
assert!(record_needs_tick(&record, 1.0, 0.15, || true));
|
||||
assert!(!record_needs_tick(&record, 1.0, 0.15, || false));
|
||||
assert!(record_needs_tick(&record, 0.0, 1.0, || true));
|
||||
assert!(record_needs_tick(&record, 1.0, 0.0, || true));
|
||||
assert!(!record_needs_tick(&record, 1.0, 0.0, || false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -710,12 +698,12 @@ mod tests {
|
||||
let shadow = record.shadow.as_mut().unwrap();
|
||||
shadow.current = 0.9;
|
||||
shadow.velocity = 0.2;
|
||||
assert!(record_needs_tick(&record, 1.0, 0.0, || false));
|
||||
assert!(record_needs_tick(&record, 1.0, 1.0, || false));
|
||||
record.shadow.as_mut().unwrap().advance(
|
||||
Duration::from_secs(3),
|
||||
crate::presentation::shadow_spring(SpringConfig::default()),
|
||||
);
|
||||
assert!(!record_needs_tick(&record, 1.0, 0.0, || false));
|
||||
assert!(!record_needs_tick(&record, 1.0, 1.0, || false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -727,8 +715,8 @@ mod tests {
|
||||
.unwrap()
|
||||
.visual
|
||||
.set_target(Rect::new(10.0, 0.0, 400.0, 300.0));
|
||||
assert!(record_needs_tick(&record, 1.0, 0.0, || true));
|
||||
assert!(!record_needs_tick(&record, 1.0, 0.0, || false));
|
||||
assert!(record_needs_tick(&record, 1.0, 1.0, || true));
|
||||
assert!(!record_needs_tick(&record, 1.0, 1.0, || false));
|
||||
|
||||
let mut record = settled();
|
||||
record.geometry.as_mut().unwrap().client.request_size(
|
||||
@@ -738,23 +726,23 @@ mod tests {
|
||||
},
|
||||
Duration::ZERO,
|
||||
);
|
||||
assert!(record_needs_tick(&record, 1.0, 0.0, || true));
|
||||
assert!(record_needs_tick(&record, 1.0, 1.0, || true));
|
||||
record.resize = Some(crate::resize_transaction::ResizeTransaction::new(
|
||||
1.into(),
|
||||
Duration::ZERO,
|
||||
));
|
||||
assert!(record_needs_tick(&record, 1.0, 0.0, || false));
|
||||
assert!(record_needs_tick(&record, 1.0, 1.0, || false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opening_and_coupled_width_cleanup_still_get_their_final_tick() {
|
||||
let mut record = settled();
|
||||
record.opening = Some(AnimatedValue::new(0.0));
|
||||
assert!(record_needs_tick(&record, 1.0, 0.0, || false));
|
||||
assert!(record_needs_tick(&record, 1.0, 1.0, || false));
|
||||
record.opening = None;
|
||||
assert!(!record_needs_tick(&record, 1.0, 0.0, || true));
|
||||
assert!(!record_needs_tick(&record, 1.0, 1.0, || true));
|
||||
record.coupled_width = Some((WorkspaceId(1), AnimatedValue::new(400.0)));
|
||||
assert!(record_needs_tick(&record, 1.0, 0.0, || true));
|
||||
assert!(record_needs_tick(&record, 1.0, 1.0, || true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -570,7 +570,6 @@ fn capture_privacy_pixels_and_policy_transitions() {
|
||||
radius: 0.0,
|
||||
shape: crate::presentation::CornerShape::Continuous,
|
||||
decorations: 0.0,
|
||||
dim: 0.0,
|
||||
fill: None,
|
||||
material: None,
|
||||
});
|
||||
|
||||
@@ -49,7 +49,6 @@ impl Ferese {
|
||||
let record = self.windows.record(id).unwrap();
|
||||
let geometry = record.geometry.unwrap();
|
||||
let decorations = geometry.decorations.clamp(0.0, 1.0);
|
||||
let dim = record.dimming.as_ref().map_or(0.0, |dim| dim.current);
|
||||
let handoff_size = (!presentation.scale_content).then(|| {
|
||||
presentation
|
||||
.native_size
|
||||
@@ -138,7 +137,6 @@ impl Ferese {
|
||||
radius,
|
||||
shape: crate::render::window_corner_shape(window),
|
||||
decorations,
|
||||
dim,
|
||||
fill,
|
||||
material,
|
||||
});
|
||||
|
||||
@@ -13,7 +13,6 @@ pub(crate) struct FrameScene {
|
||||
pub(crate) struct WindowFrame {
|
||||
pub geometry: WindowGeometry,
|
||||
pub presentation: crate::presentation::WindowPresentation,
|
||||
pub dim: f64,
|
||||
}
|
||||
|
||||
impl Ferese {
|
||||
@@ -28,6 +27,26 @@ impl Ferese {
|
||||
))
|
||||
}
|
||||
|
||||
fn window_focus_effect(&self, id: WindowId, delta: Duration, overview_opacity: f32) -> (f32, f64) {
|
||||
let target = crate::focus_effect::target(self.focus_effect, self.focused_window, id);
|
||||
let mut focus = self
|
||||
.windows
|
||||
.record(id)
|
||||
.and_then(|record| record.focus_transition.clone())
|
||||
.unwrap_or_else(|| crate::focus_effect::BoundedFade::new(target));
|
||||
focus.advance_visual(
|
||||
target,
|
||||
delta,
|
||||
if self.animations_enabled && self.focus_effect.enabled {
|
||||
self.focus_effect.duration_ms
|
||||
} else {
|
||||
0.0
|
||||
},
|
||||
);
|
||||
self.focus_effect
|
||||
.resolve(focus.current, self.focused_window.is_some(), overview_opacity)
|
||||
}
|
||||
|
||||
fn compose_window_presentation(
|
||||
&self,
|
||||
id: WindowId,
|
||||
@@ -110,8 +129,11 @@ impl Ferese {
|
||||
);
|
||||
bounds.current = crate::presentation::scaled_visual_rect(bounds.current, presence_scale);
|
||||
bounds.target = bounds.current;
|
||||
let (focus_alpha, dim) = self.window_focus_effect(id, delta, overview.opacity());
|
||||
crate::presentation::WindowPresentation {
|
||||
id,
|
||||
focus_alpha,
|
||||
dim,
|
||||
bounds,
|
||||
opacity,
|
||||
emphasis,
|
||||
@@ -161,13 +183,8 @@ impl Ferese {
|
||||
.shadow
|
||||
.as_ref()
|
||||
.is_some_and(|shadow| shadow.needs_update(if selected == Some(id) { 1.0 } else { 0.0 }))
|
||||
|| record.dimming.as_ref().is_some_and(|dim| {
|
||||
dim.needs_update(crate::dimming::target(
|
||||
self.inactive_dim,
|
||||
self.focused_window,
|
||||
id,
|
||||
self.overview.is_presenting(),
|
||||
))
|
||||
|| record.focus_transition.as_ref().is_some_and(|transition| {
|
||||
transition.needs_update(crate::focus_effect::target(self.focus_effect, self.focused_window, id))
|
||||
}))
|
||||
})
|
||||
}
|
||||
@@ -209,7 +226,10 @@ impl Ferese {
|
||||
|| self.render.snapshot(&id).is_some()
|
||||
|| record.focus.as_ref().is_some_and(|focus| focus.is_animating())
|
||||
|| record.shadow.as_ref().is_some_and(|shadow| shadow.is_animating())
|
||||
|| record.dimming.as_ref().is_some_and(|dim| dim.is_animating())
|
||||
|| record
|
||||
.focus_transition
|
||||
.as_ref()
|
||||
.is_some_and(|transition| transition.is_animating())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -298,18 +318,7 @@ impl Ferese {
|
||||
.and_then(|workspace| slide_offsets.get(&workspace).copied())
|
||||
.unwrap_or_default();
|
||||
let presentation = self.compose_window_presentation(id, geometry, &overview, offset, delta);
|
||||
let mut dim = record.dimming.clone();
|
||||
if let Some(dim) = &mut dim {
|
||||
dim.predict(delta, self.inactive_dim.duration_ms);
|
||||
}
|
||||
windows.insert(
|
||||
id,
|
||||
WindowFrame {
|
||||
geometry,
|
||||
presentation,
|
||||
dim: dim.map_or(0.0, |dim| dim.current),
|
||||
},
|
||||
);
|
||||
windows.insert(id, WindowFrame { geometry, presentation });
|
||||
}
|
||||
|
||||
FrameScene {
|
||||
@@ -375,6 +384,124 @@ fn predict_geometry(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires a private XDG_RUNTIME_DIR and permission to bind test sockets"]
|
||||
fn focus_effect_sampling_preserves_activation_and_output_local_progress() {
|
||||
let runtime = std::path::PathBuf::from(std::env::var_os("XDG_RUNTIME_DIR").unwrap());
|
||||
assert!(runtime.starts_with(std::env::temp_dir()));
|
||||
let mut event_loop = EventLoop::try_new().unwrap();
|
||||
let mut state = Ferese::new(
|
||||
&mut event_loop,
|
||||
Display::new().unwrap(),
|
||||
crate::config::Config::default().runtime_config().unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
state.focus_effect = crate::config::FocusEffectSettings {
|
||||
enabled: true,
|
||||
active_opacity: 0.9,
|
||||
inactive_opacity: 0.6,
|
||||
inactive_dim: 0.2,
|
||||
duration_ms: 150.0,
|
||||
};
|
||||
let mut outputs = Vec::new();
|
||||
for index in 0..2 {
|
||||
let output = Output::new(
|
||||
format!("focus-{index}"),
|
||||
smithay::output::PhysicalProperties {
|
||||
size: (0, 0).into(),
|
||||
subpixel: smithay::output::Subpixel::Unknown,
|
||||
make: "test".into(),
|
||||
model: "test".into(),
|
||||
},
|
||||
);
|
||||
output.change_current_state(
|
||||
Some(smithay::output::Mode {
|
||||
size: (800, 600).into(),
|
||||
refresh: 60_000,
|
||||
}),
|
||||
Some(smithay::utils::Transform::Normal),
|
||||
None,
|
||||
Some((index * 800, 0).into()),
|
||||
);
|
||||
state.space.map_output(&output, (index * 800, 0));
|
||||
state.register_output(&output, format!("focus-{index}"));
|
||||
let workspace = state
|
||||
.output_workspaces
|
||||
.active_workspace(state.output_id(&output).unwrap())
|
||||
.unwrap();
|
||||
let id = WindowId(index as u64 + 1);
|
||||
let rect = Rect::new(index as f64 * 800.0, 0.0, 400.0, 300.0);
|
||||
state
|
||||
.workspaces
|
||||
.insert_floating_window(id, workspace, rect, false)
|
||||
.unwrap();
|
||||
state.windows.records.insert(
|
||||
id,
|
||||
super::super::window_registry::WindowRecord {
|
||||
geometry: Some(WindowGeometry::new(rect, Some(ClientSize::from_rect(rect)))),
|
||||
focus_transition: Some(crate::focus_effect::BoundedFade::new(if index == 0 {
|
||||
1.0
|
||||
} else {
|
||||
0.0
|
||||
})),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
outputs.push(output);
|
||||
}
|
||||
let first = WindowId(1);
|
||||
let second = WindowId(2);
|
||||
state.focused_window = Some(first);
|
||||
assert_eq!(state.current_window_presentation(first).unwrap().alpha(), 0.9);
|
||||
assert_eq!(state.current_window_presentation(second).unwrap().alpha(), 0.6);
|
||||
// Only emphasis follows overview selection; activation and its fade remain unchanged.
|
||||
state.set_overview_active(true);
|
||||
state.overview.select_window(second);
|
||||
assert_eq!(state.focused_window, Some(first));
|
||||
assert_eq!(state.current_window_presentation(first).unwrap().alpha(), 0.9);
|
||||
assert_eq!(state.current_window_presentation(second).unwrap().alpha(), 0.6);
|
||||
state.set_overview_active(false);
|
||||
state.focused_window = Some(second);
|
||||
let transition = state
|
||||
.windows
|
||||
.record_mut(first)
|
||||
.unwrap()
|
||||
.focus_transition
|
||||
.as_mut()
|
||||
.unwrap();
|
||||
transition.advance_visual(0.0, Duration::ZERO, 150.0);
|
||||
let previous = transition.current;
|
||||
assert!(state.output_has_pending_visual_changes(&outputs[0]));
|
||||
let frame = state.sample_frame(&outputs[0], Duration::from_millis(75));
|
||||
assert!((frame.windows[&first].presentation.alpha() - 0.75).abs() < 1e-6);
|
||||
assert!(!frame.windows.contains_key(&second));
|
||||
assert_eq!(
|
||||
state
|
||||
.windows
|
||||
.record(first)
|
||||
.unwrap()
|
||||
.focus_transition
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.current,
|
||||
previous
|
||||
);
|
||||
// A pending client configure must not stop focus presentation from progressing.
|
||||
state.windows.set_transaction(
|
||||
first,
|
||||
crate::resize_transaction::ResizeTransaction::new(9.into(), Duration::ZERO),
|
||||
);
|
||||
let waiting = state.sample_frame(&outputs[0], Duration::from_millis(75));
|
||||
assert_eq!(
|
||||
waiting.windows[&first].presentation.alpha(),
|
||||
frame.windows[&first].presentation.alpha()
|
||||
);
|
||||
assert!(state.windows.transaction(&first).is_some());
|
||||
state.animations_enabled = false;
|
||||
assert_eq!(state.current_window_presentation(first).unwrap().alpha(), 0.6);
|
||||
assert_eq!(state.current_window_presentation(second).unwrap().alpha(), 0.9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires a private XDG_RUNTIME_DIR and permission to bind test sockets"]
|
||||
fn overview_strip_switch_slides_stable_grids_and_keeps_strip_fixed() {
|
||||
|
||||
@@ -23,7 +23,7 @@ pub(crate) struct WindowRecord {
|
||||
pub resize: Option<ResizeTransaction>,
|
||||
pub focus: Option<AnimatedValue>,
|
||||
pub shadow: Option<AnimatedValue>,
|
||||
pub dimming: Option<crate::dimming::DimAnimation>,
|
||||
pub focus_transition: Option<crate::focus_effect::BoundedFade>,
|
||||
pub opening: Option<AnimatedValue>,
|
||||
pub mapped_once: bool,
|
||||
pub world_x: Option<(WorkspaceId, AnimatedValue)>,
|
||||
|
||||
Reference in new issue
Block a user