osd: overshoot the volume/brightness fill before settling
pbar.set_fraction() jumped straight to the target with no motion. anim::spring_to clamps every frame to [min(from,to), max(from,to)] (a safety clamp for its other callers, e.g. never letting a collapsing drawer's height go negative), which also means a single spring_to call can never visibly overshoot its target. animate_osd_fill chains two legs instead — one toward a point a few percent past the real value, then one back onto it — to get the same overshoot-and-settle read the rest of the theme's motion has. A generation token guards leg 2 against firing after a newer OSD event has already superseded it.
This commit is contained in:
parent
4cdd6f0213
commit
ba4e3480d4
1 changed files with 88 additions and 2 deletions
90
src/osd.rs
90
src/osd.rs
|
|
@ -1,4 +1,8 @@
|
||||||
use std::{cell::Cell, rc::Rc, time::Duration};
|
use std::{
|
||||||
|
cell::{Cell, RefCell},
|
||||||
|
rc::Rc,
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
use gtk4::prelude::*;
|
use gtk4::prelude::*;
|
||||||
use gtk4_layer_shell::LayerShell;
|
use gtk4_layer_shell::LayerShell;
|
||||||
|
|
@ -174,6 +178,15 @@ async fn run_osd(window: gtk4::Window, mut rx: mpsc::Receiver<OsdEvent>) {
|
||||||
container.append(&pbar);
|
container.append(&pbar);
|
||||||
|
|
||||||
let dismiss_token = Rc::new(Cell::new(0u32));
|
let dismiss_token = Rc::new(Cell::new(0u32));
|
||||||
|
// OSD fill overshoot (ANIMATION WORK #5): current fraction (as a whole
|
||||||
|
// percent, matching `anim::spring_to`'s `i32` interpolation), the
|
||||||
|
// in-flight tick callback (so a fast double-tap of volume-up cancels the
|
||||||
|
// previous run instead of fighting it), and a generation token so a
|
||||||
|
// superseded run's queued second leg (see `animate_osd_fill` below)
|
||||||
|
// never applies after a newer event has already taken over.
|
||||||
|
let fill_pct = Rc::new(Cell::new(0i32));
|
||||||
|
let fill_anim: Rc<RefCell<Option<gtk4::TickCallbackId>>> = Rc::new(RefCell::new(None));
|
||||||
|
let fill_token = Rc::new(Cell::new(0u32));
|
||||||
|
|
||||||
while let Some(event) = rx.recv().await {
|
while let Some(event) = rx.recv().await {
|
||||||
let (icon_svg, pct, muted) = match event {
|
let (icon_svg, pct, muted) = match event {
|
||||||
|
|
@ -188,7 +201,7 @@ async fn run_osd(window: gtk4::Window, mut rx: mpsc::Receiver<OsdEvent>) {
|
||||||
} else {
|
} else {
|
||||||
icon.remove_css_class("osd-icon-muted");
|
icon.remove_css_class("osd-icon-muted");
|
||||||
}
|
}
|
||||||
pbar.set_fraction(pct as f64 / 100.0);
|
animate_osd_fill(&pbar, &fill_pct, &fill_anim, &fill_token, pct);
|
||||||
window.set_visible(true);
|
window.set_visible(true);
|
||||||
|
|
||||||
let token = dismiss_token.get().wrapping_add(1);
|
let token = dismiss_token.get().wrapping_add(1);
|
||||||
|
|
@ -204,6 +217,79 @@ async fn run_osd(window: gtk4::Window, mut rx: mpsc::Receiver<OsdEvent>) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// How many percentage points the fill runs past its real target before
|
||||||
|
/// easing back — subtle, matching the rest of the theme's "spring" motion
|
||||||
|
/// rather than a dramatic bounce.
|
||||||
|
const OSD_OVERSHOOT_PCT: i32 = 4;
|
||||||
|
/// Leg 1 (toward the overshoot point) / leg 2 (settling back onto the real
|
||||||
|
/// target) durations. Two legs, not one: `anim::spring_to` clamps every
|
||||||
|
/// frame to `[min(from, to), max(from, to)]` (see its own doc comment and
|
||||||
|
/// tests) specifically so a caller like the capsule drawer can never end up
|
||||||
|
/// with a negative size request — which also means a single `spring_to`
|
||||||
|
/// call can *never* visibly overshoot `to`, no matter how much the
|
||||||
|
/// underlying curve wants to. Chaining two calls — first to a point past
|
||||||
|
/// the target, then back onto it — is what actually produces the overshoot.
|
||||||
|
const OSD_LEG1_MS: f64 = 160.0;
|
||||||
|
const OSD_LEG2_MS: f64 = 200.0;
|
||||||
|
|
||||||
|
/// Animates `pbar`'s fraction from wherever `current` says it currently is
|
||||||
|
/// to `target`, overshooting slightly past it and settling back — see the
|
||||||
|
/// constants above for why this takes two `spring_to` legs instead of one.
|
||||||
|
/// Cancels any run already in flight (a fast double volume-step must
|
||||||
|
/// continue from the current visual position, not fight or restart it) and
|
||||||
|
/// stamps a fresh generation token so a superseded run's leg 2, queued via
|
||||||
|
/// `timeout_add_local_once` below, is a no-op if it fires after a newer
|
||||||
|
/// call has already taken over.
|
||||||
|
fn animate_osd_fill(
|
||||||
|
pbar: >k4::ProgressBar,
|
||||||
|
current: &Rc<Cell<i32>>,
|
||||||
|
anim: &Rc<RefCell<Option<gtk4::TickCallbackId>>>,
|
||||||
|
token: &Rc<Cell<u32>>,
|
||||||
|
target: u8,
|
||||||
|
) {
|
||||||
|
if let Some(id) = anim.borrow_mut().take() {
|
||||||
|
id.remove();
|
||||||
|
}
|
||||||
|
let my_token = token.get().wrapping_add(1);
|
||||||
|
token.set(my_token);
|
||||||
|
|
||||||
|
let from = current.get();
|
||||||
|
let to = i32::from(target);
|
||||||
|
if from == to {
|
||||||
|
pbar.set_fraction(f64::from(to) / 100.0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let overshoot = if to > from {
|
||||||
|
(to + OSD_OVERSHOOT_PCT).min(100)
|
||||||
|
} else {
|
||||||
|
(to - OSD_OVERSHOOT_PCT).max(0)
|
||||||
|
};
|
||||||
|
|
||||||
|
let leg1_bar = pbar.clone();
|
||||||
|
let leg1_current = current.clone();
|
||||||
|
let id = bread_theme::anim::spring_to(pbar, from, overshoot, OSD_LEG1_MS, move |v| {
|
||||||
|
leg1_current.set(v);
|
||||||
|
leg1_bar.set_fraction(f64::from(v) / 100.0);
|
||||||
|
});
|
||||||
|
*anim.borrow_mut() = Some(id);
|
||||||
|
|
||||||
|
let leg2_bar = pbar.clone();
|
||||||
|
let leg2_current = current.clone();
|
||||||
|
let leg2_anim = anim.clone();
|
||||||
|
let leg2_token = token.clone();
|
||||||
|
gtk4::glib::timeout_add_local_once(Duration::from_millis(OSD_LEG1_MS as u64), move || {
|
||||||
|
if leg2_token.get() != my_token {
|
||||||
|
return; // superseded by a newer OSD event before leg 1 finished
|
||||||
|
}
|
||||||
|
let inner_bar = leg2_bar.clone();
|
||||||
|
let id = bread_theme::anim::spring_to(&leg2_bar, overshoot, to, OSD_LEG2_MS, move |v| {
|
||||||
|
leg2_current.set(v);
|
||||||
|
inner_bar.set_fraction(f64::from(v) / 100.0);
|
||||||
|
});
|
||||||
|
*leg2_anim.borrow_mut() = Some(id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn create_window() -> gtk4::Window {
|
fn create_window() -> gtk4::Window {
|
||||||
let window = gtk4::Window::new();
|
let window = gtk4::Window::new();
|
||||||
window.add_css_class("breadbar-osd");
|
window.add_css_class("breadbar-osd");
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue