diff options
| author | Ivan Molodetskikh <yalterz@gmail.com> | 2025-08-22 08:50:52 +0300 |
|---|---|---|
| committer | Ivan Molodetskikh <yalterz@gmail.com> | 2025-08-22 08:57:08 +0300 |
| commit | 210d5e90fe00ae9add5d841e1752b7f8c4a639a7 (patch) | |
| tree | ef1431ecb1744984105e7b30520581cb5a1c5219 | |
| parent | 9d3beb49315ae4cde3d8edfb979fccda52af3fae (diff) | |
| download | niri-210d5e90fe00ae9add5d841e1752b7f8c4a639a7.tar.gz niri-210d5e90fe00ae9add5d841e1752b7f8c4a639a7.tar.bz2 niri-210d5e90fe00ae9add5d841e1752b7f8c4a639a7.zip | |
exit_confirm_dialog: Add open/close animation
| -rw-r--r-- | docs/wiki/Configuration:-Animations.md | 20 | ||||
| -rw-r--r-- | niri-config/src/lib.rs | 46 | ||||
| -rw-r--r-- | src/niri.rs | 4 | ||||
| -rw-r--r-- | src/ui/exit_confirm_dialog.rs | 110 |
4 files changed, 159 insertions, 21 deletions
diff --git a/docs/wiki/Configuration:-Animations.md b/docs/wiki/Configuration:-Animations.md index 2400f568..0012aa83 100644 --- a/docs/wiki/Configuration:-Animations.md +++ b/docs/wiki/Configuration:-Animations.md @@ -46,6 +46,10 @@ animations { spring damping-ratio=0.6 stiffness=1000 epsilon=0.001 } + exit-confirmation-open-close { + spring damping-ratio=0.6 stiffness=500 epsilon=0.01 + } + screenshot-ui-open { duration-ms 200 curve "ease-out-quad" @@ -363,6 +367,22 @@ animations { } ``` +#### `exit-confirmation-open-close` + +<sup>Since: next release</sup> + +The open/close animation of the exit confirmation dialog. + +This one uses an underdamped spring by default (`damping-ratio=0.6`) which causes a slight oscillation in the end. + +```kdl +animations { + exit-confirmation-open-close { + spring damping-ratio=0.6 stiffness=500 epsilon=0.01 + } +} +``` + #### `screenshot-ui-open` <sup>Since: 0.1.8</sup> diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index bd8a9854..691672f5 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -1109,6 +1109,8 @@ pub struct Animations { #[knuffel(child, default)] pub config_notification_open_close: ConfigNotificationOpenCloseAnim, #[knuffel(child, default)] + pub exit_confirmation_open_close: ExitConfirmationOpenCloseAnim, + #[knuffel(child, default)] pub screenshot_ui_open: ScreenshotUiOpenAnim, #[knuffel(child, default)] pub overview_open_close: OverviewOpenCloseAnim, @@ -1126,6 +1128,7 @@ impl Default for Animations { window_close: Default::default(), window_resize: Default::default(), config_notification_open_close: Default::default(), + exit_confirmation_open_close: Default::default(), screenshot_ui_open: Default::default(), overview_open_close: Default::default(), } @@ -1261,6 +1264,22 @@ impl Default for ConfigNotificationOpenCloseAnim { } #[derive(Debug, Clone, Copy, PartialEq)] +pub struct ExitConfirmationOpenCloseAnim(pub Animation); + +impl Default for ExitConfirmationOpenCloseAnim { + fn default() -> Self { + Self(Animation { + off: false, + kind: AnimationKind::Spring(SpringParams { + damping_ratio: 0.6, + stiffness: 500, + epsilon: 0.01, + }), + }) + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] pub struct ScreenshotUiOpenAnim(pub Animation); impl Default for ScreenshotUiOpenAnim { @@ -3338,6 +3357,21 @@ where } } +impl<S> knuffel::Decode<S> for ExitConfirmationOpenCloseAnim +where + S: knuffel::traits::ErrorSpan, +{ + fn decode_node( + node: &knuffel::ast::SpannedNode<S>, + ctx: &mut knuffel::decode::Context<S>, + ) -> Result<Self, DecodeError<S>> { + let default = Self::default().0; + Ok(Self(Animation::decode_node(node, ctx, default, |_, _| { + Ok(false) + })?)) + } +} + impl<S> knuffel::Decode<S> for ScreenshotUiOpenAnim where S: knuffel::traits::ErrorSpan, @@ -5148,6 +5182,18 @@ mod tests { ), }, ), + exit_confirmation_open_close: ExitConfirmationOpenCloseAnim( + Animation { + off: false, + kind: Spring( + SpringParams { + damping_ratio: 0.6, + stiffness: 500, + epsilon: 0.01, + }, + ), + }, + ), screenshot_ui_open: ScreenshotUiOpenAnim( Animation { off: false, diff --git a/src/niri.rs b/src/niri.rs index b4b4a665..c97153f1 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -2460,7 +2460,7 @@ impl Niri { hotkey_overlay.show(); } - let exit_confirm_dialog = ExitConfirmDialog::new(); + let exit_confirm_dialog = ExitConfirmDialog::new(animation_clock.clone(), config.clone()); event_loop .insert_source( @@ -4050,6 +4050,7 @@ impl Niri { self.layout.advance_animations(); self.config_error_notification.advance_animations(); + self.exit_confirm_dialog.advance_animations(); self.screenshot_ui.advance_animations(); for state in self.output_state.values_mut() { @@ -4400,6 +4401,7 @@ impl Niri { state.unfinished_animations_remain = self.layout.are_animations_ongoing(Some(output)); state.unfinished_animations_remain |= self.config_error_notification.are_animations_ongoing(); + state.unfinished_animations_remain |= self.exit_confirm_dialog.are_animations_ongoing(); state.unfinished_animations_remain |= self.screenshot_ui.are_animations_ongoing(); state.unfinished_animations_remain |= state.screen_transition.is_some(); diff --git a/src/ui/exit_confirm_dialog.rs b/src/ui/exit_confirm_dialog.rs index 9162118b..e0cc9398 100644 --- a/src/ui/exit_confirm_dialog.rs +++ b/src/ui/exit_confirm_dialog.rs @@ -1,16 +1,20 @@ use std::cell::RefCell; use std::collections::HashMap; +use std::rc::Rc; use std::sync::Mutex; use arrayvec::ArrayVec; +use niri_config::Config; use ordered_float::NotNan; use pangocairo::cairo::{self, ImageSurface}; use pangocairo::pango::{Alignment, FontDescription}; +use smithay::backend::renderer::element::utils::RescaleRenderElement; use smithay::backend::renderer::element::Kind; use smithay::output::Output; use smithay::reexports::gbm::Format as Fourcc; use smithay::utils::{Point, Transform}; +use crate::animation::{Animation, Clock}; use crate::niri_render_elements; use crate::render_helpers::memory::MemoryBuffer; use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement; @@ -27,13 +31,16 @@ const BORDER: i32 = 8; const BACKDROP_COLOR: [f32; 4] = [0., 0., 0., 0.4]; pub struct ExitConfirmDialog { - is_open: bool, + state: State, buffers: RefCell<HashMap<NotNan<f64>, Option<MemoryBuffer>>>, + + clock: Clock, + config: Rc<RefCell<Config>>, } niri_render_elements! { ExitConfirmDialogRenderElement => { - Texture = PrimaryGpuTextureRenderElement, + Texture = RescaleRenderElement<PrimaryGpuTextureRenderElement>, SolidColor = SolidColorRenderElement, } } @@ -42,8 +49,15 @@ struct OutputData { backdrop: SolidColorBuffer, } +enum State { + Hidden, + Showing(Animation), + Visible, + Hiding(Animation), +} + impl ExitConfirmDialog { - pub fn new() -> Self { + pub fn new(clock: Clock, config: Rc<RefCell<Config>>) -> Self { let buffer = match render(1.) { Ok(x) => Some(x), Err(err) => { @@ -53,8 +67,10 @@ impl ExitConfirmDialog { }; Self { - is_open: false, + state: State::Hidden, buffers: RefCell::new(HashMap::from([(NotNan::new(1.).unwrap(), buffer)])), + clock, + config, } } @@ -64,26 +80,72 @@ impl ExitConfirmDialog { fallback.is_some() } + fn animation(&self, from: f64, to: f64) -> Animation { + let c = self.config.borrow(); + Animation::new( + self.clock.clone(), + from, + to, + 0., + c.animations.exit_confirmation_open_close.0, + ) + } + + fn value(&self) -> f64 { + match &self.state { + State::Hidden => 0., + State::Showing(anim) | State::Hiding(anim) => anim.value(), + State::Visible => 1., + } + } + + /// Returns true if the dialog will be shown (even if it is already shown). pub fn show(&mut self) -> bool { if !self.can_show() { return false; } - self.is_open = true; + if self.is_open() { + return true; + } + + self.state = State::Showing(self.animation(self.value(), 1.)); true } + /// Returns true if started the hide animation. pub fn hide(&mut self) -> bool { - if self.is_open { - self.is_open = false; - true - } else { - false + if !self.is_open() { + return false; } + + self.state = State::Hiding(self.animation(self.value(), 0.)); + true } pub fn is_open(&self) -> bool { - self.is_open + matches!(self.state, State::Showing(_) | State::Visible) + } + + pub fn advance_animations(&mut self) { + match &mut self.state { + State::Hidden => (), + State::Showing(anim) => { + if anim.is_done() { + self.state = State::Visible; + } + } + State::Visible => (), + State::Hiding(anim) => { + if anim.is_clamped_done() { + self.state = State::Hidden; + } + } + } + } + + pub fn are_animations_ongoing(&self) -> bool { + matches!(self.state, State::Showing(_) | State::Hiding(_)) } pub fn render<R: NiriRenderer>( @@ -93,9 +155,13 @@ impl ExitConfirmDialog { ) -> ArrayVec<ExitConfirmDialogRenderElement, 2> { let mut rv = ArrayVec::new(); - if !self.is_open { - return rv; - } + let (value, clamped_value) = match &self.state { + State::Hidden => return rv, + State::Showing(anim) | State::Hiding(anim) => (anim.value(), anim.clamped_value()), + State::Visible => (1., 1.), + }; + // Can be out of range when starting from past 0. or 1. from a spring bounce. + let clamped_value = clamped_value.clamp(0., 1.); let scale = output.current_scale().fractional_scale(); let output_size = output_size(output); @@ -117,7 +183,7 @@ impl ExitConfirmDialog { return rv; }; - let location = (output_size.to_f64().to_point() - size.to_point()).downscale(2.); + let location = (output_size.to_point() - size.to_point()).downscale(2.); let mut location = location.to_physical_precise_round(scale).to_logical(scale); location.x = f64::max(0., location.x); location.y = f64::max(0., location.y); @@ -125,14 +191,18 @@ impl ExitConfirmDialog { let elem = TextureRenderElement::from_texture_buffer( buffer, location, - 1., + clamped_value as f32, None, None, Kind::Unspecified, ); - rv.push(ExitConfirmDialogRenderElement::Texture( - PrimaryGpuTextureRenderElement(elem), - )); + let elem = PrimaryGpuTextureRenderElement(elem); + let elem = RescaleRenderElement::from_element( + elem, + (location + size.downscale(2.)).to_physical_precise_round(scale), + value.max(0.) * 0.2 + 0.8, + ); + rv.push(ExitConfirmDialogRenderElement::Texture(elem)); // Backdrop. let data = output.user_data().get_or_insert(|| { @@ -146,7 +216,7 @@ impl ExitConfirmDialog { let elem = SolidColorRenderElement::from_buffer( &data.backdrop, Point::new(0., 0.), - 1., + clamped_value as f32, Kind::Unspecified, ); rv.push(ExitConfirmDialogRenderElement::SolidColor(elem)); |
