diff --git a/niri-config/src/appearance.rs b/niri-config/src/appearance.rs index bf600feb70..efdeef1d07 100644 --- a/niri-config/src/appearance.rs +++ b/niri-config/src/appearance.rs @@ -238,6 +238,8 @@ pub struct FocusRing { pub active_gradient: Option, pub inactive_gradient: Option, pub urgent_gradient: Option, + pub fade_duration_ms: u64, + pub gradient_spin_speed: f64, } impl Default for FocusRing { @@ -251,6 +253,8 @@ impl Default for FocusRing { active_gradient: None, inactive_gradient: None, urgent_gradient: None, + fade_duration_ms: 0, + gradient_spin_speed: 0., } } } @@ -293,6 +297,8 @@ impl From for FocusRing { active_gradient: value.active_gradient, inactive_gradient: value.inactive_gradient, urgent_gradient: value.urgent_gradient, + fade_duration_ms: 0, + gradient_spin_speed: 0., } } } @@ -332,9 +338,19 @@ impl MergeWith for Border { impl MergeWith for FocusRing { fn merge_with(&mut self, part: &BorderRule) { + let saved_fade = self.fade_duration_ms; + let saved_spin = self.gradient_spin_speed; let mut x = Border::from(*self); x.merge_with(part); *self = FocusRing::from(x); + self.fade_duration_ms = match part.fade_duration_ms { + Some(v) => v.0 as u64, + None => saved_fade, + }; + self.gradient_spin_speed = match part.gradient_spin_speed { + Some(v) => v.0 as f64, + None => saved_spin, + }; } } @@ -642,6 +658,10 @@ pub struct BorderRule { pub inactive_gradient: Option, #[knuffel(child)] pub urgent_gradient: Option, + #[knuffel(child, unwrap(argument))] + pub fade_duration_ms: Option>, + #[knuffel(child, unwrap(argument))] + pub gradient_spin_speed: Option>, } #[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)] diff --git a/src/layout/focus_ring.rs b/src/layout/focus_ring.rs index 5066a33b89..8b79ba41a2 100644 --- a/src/layout/focus_ring.rs +++ b/src/layout/focus_ring.rs @@ -66,6 +66,7 @@ impl FocusRing { radius: CornerRadius, scale: f64, alpha: f32, + gradient_angle_offset: f32, ) { let width = self.config.width; self.full_size = win_size + Size::from((width, width)).upscale(2.); @@ -187,7 +188,7 @@ impl FocusRing { gradient.in_, gradient.from, gradient.to, - ((gradient.angle as f32) - 90.).to_radians(), + ((gradient.angle as f32) - 90. + gradient_angle_offset).to_radians(), Rectangle::new(full_rect.loc - loc, full_rect.size), rounded_corner_border_width, radius, diff --git a/src/layout/insert_hint_element.rs b/src/layout/insert_hint_element.rs index 818aeb7e5e..2d646887b7 100644 --- a/src/layout/insert_hint_element.rs +++ b/src/layout/insert_hint_element.rs @@ -23,6 +23,8 @@ impl InsertHintElement { active_gradient: config.gradient, inactive_gradient: config.gradient, urgent_gradient: config.gradient, + fade_duration_ms: 0, + gradient_spin_speed: 0., }), } } @@ -37,6 +39,8 @@ impl InsertHintElement { active_gradient: config.gradient, inactive_gradient: config.gradient, urgent_gradient: config.gradient, + fade_duration_ms: 0, + gradient_spin_speed: 0., }); } @@ -52,7 +56,7 @@ impl InsertHintElement { scale: f64, ) { self.inner - .update_render_elements(size, true, false, false, view_rect, radius, scale, 1.); + .update_render_elements(size, true, false, false, view_rect, radius, scale, 1., 0.); } pub fn render( diff --git a/src/layout/tile.rs b/src/layout/tile.rs index beaa981fcc..1948934ac9 100644 --- a/src/layout/tile.rs +++ b/src/layout/tile.rs @@ -94,6 +94,12 @@ pub struct Tile { /// The animation of the tile's opacity. pub(super) alpha_animation: Option, + /// The animation of the focus ring fading in/out. + focus_ring_anim: Option, + + /// Whether the tile was focused on the last render update. + was_focus_active: bool, + /// Offset during the initial interactive move rubberband. pub(super) interactive_move_offset: Point, @@ -202,6 +208,8 @@ impl Tile { move_x_animation: None, move_y_animation: None, alpha_animation: None, + focus_ring_anim: None, + was_focus_active: false, interactive_move_offset: Point::from((0., 0.)), unmap_snapshot: None, rounded_corner_damage: Default::default(), @@ -450,6 +458,11 @@ impl Tile { .alpha_animation .as_ref() .is_some_and(|alpha| !alpha.anim.is_done()) + || self + .focus_ring_anim + .as_ref() + .is_some_and(|anim| !anim.is_done()) + || (self.was_focus_active && self.options.layout.focus_ring.gradient_spin_speed > 0.) } pub fn update_render_elements(&mut self, is_active: bool, view_rect: Rectangle) { @@ -487,6 +500,7 @@ impl Tile { radius, self.scale, 1. - expanded_progress as f32, + 0., ); let radius = if self.visual_border_width().is_some() { @@ -511,15 +525,63 @@ impl Tile { false }; let radius = radius.expanded_by(self.focus_ring.width() as f32); + + let fade_ms = self.options.layout.focus_ring.fade_duration_ms; + + // Animate focus ring fade in/out on focus change. + if is_active != self.was_focus_active { + if fade_ms > 0 { + let (from, to) = if is_active { (0., 1.) } else { (1., 0.) }; + self.focus_ring_anim = Some(Animation::ease( + self.clock.clone(), + from, + to, + 0., + fade_ms, + crate::animation::Curve::EaseOutCubic, + )); + } else { + self.focus_ring_anim = None; + } + self.was_focus_active = is_active; + } + + let focus_ring_alpha = if fade_ms > 0 { + match &self.focus_ring_anim { + Some(anim) if !anim.is_done() => { + anim.clamped_value() as f32 + } + _ => { + self.focus_ring_anim = None; + if is_active { 1.0 } else { 0.0 } + } + } + } else { + if is_active { 1.0 } else { 0.0 } + }; + + // During fade-out, keep rendering with active colors. + let ring_is_active = is_active || focus_ring_alpha > 0.0; + + // Rotate gradient while focus ring is visible. + let spin_speed = self.options.layout.focus_ring.gradient_spin_speed as f32; + let gradient_angle_offset = if ring_is_active && spin_speed > 0. { + let secs = self.clock.now().as_secs_f32(); + (secs * spin_speed) % 360. + } else { + 0. + }; + self.focus_ring.update_render_elements( animated_tile_size, - is_active, + ring_is_active, !draw_focus_ring_with_background, self.window.is_urgent(), view_rect, radius, self.scale, - 1. - expanded_progress as f32, + focus_ring_alpha * (1. - expanded_progress as f32), + gradient_angle_offset, ); self.fullscreen_backdrop.resize(animated_tile_size); diff --git a/src/ui/mru.rs b/src/ui/mru.rs index 1b2aca1d6b..076de94269 100644 --- a/src/ui/mru.rs +++ b/src/ui/mru.rs @@ -531,6 +531,7 @@ impl Thumbnail { radius, scale, 0.5, + 0., ); let bg_elems = background .render(renderer, loc) @@ -552,6 +553,7 @@ impl Thumbnail { radius.expanded_by(config.width as f32), scale, 1., + 0., ); let border_elems = border