use std::thread;
use std::time::{Duration, Instant};
use rand::{rng, RngExt};
use windows::Win32::Foundation::{HWND, RECT};
use windows::Win32::UI::WindowsAndMessaging::{
GetForegroundWindow, GetWindowRect, SetWindowPos, SET_WINDOW_POS_FLAGS, SWP_NOACTIVATE,
SWP_NOSIZE, SWP_NOZORDER,
};
use crate::desktop::{list_windows, WindowInfo};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WobbleDirection {
Horizontal,
Vertical,
Both,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WobbleSettings {
pub duration: Duration,
pub direction: WobbleDirection,
pub min_amplitude: i32,
pub max_amplitude: i32,
}
impl Default for WobbleSettings {
fn default() -> Self {
Self {
duration: Duration::from_secs(2),
direction: WobbleDirection::Horizontal,
min_amplitude: 4,
max_amplitude: 8,
}
}
}
const WOBBLE_STEP: Duration = Duration::from_millis(10);
const WOBBLE_FREQ_HZ: f64 = 8.0;
const AMPLITUDE_FREQ_HZ: f64 = 0.5;
fn wobble_flags() -> SET_WINDOW_POS_FLAGS {
SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE
}
pub fn wobble_window(window: HWND, settings: WobbleSettings) -> bool {
let mut rect = RECT::default();
if !unsafe { GetWindowRect(window, &mut rect).is_ok() } {
return false;
}
let (base_x, base_y) = (rect.left, rect.top);
let total = settings.duration.as_secs_f64();
let min_amp = settings.min_amplitude.max(0) as f64;
let max_amp = settings
.max_amplitude
.max(settings.min_amplitude.max(0)) as f64;
if total <= 0.0 || max_amp == 0.0 {
return true;
}
let started = Instant::now();
loop {
let elapsed = started.elapsed().as_secs_f64();
if elapsed >= total {
break;
}
let envelope = (std::f64::consts::PI * elapsed / total).sin();
let amp_mod = 0.5 + 0.5 * (2.0 * std::f64::consts::PI * AMPLITUDE_FREQ_HZ * elapsed).sin();
let amp = (min_amp + (max_amp - min_amp) * amp_mod) * envelope;
let wobble = (2.0 * std::f64::consts::PI * WOBBLE_FREQ_HZ * elapsed).sin();
let (dx, dy) = match settings.direction {
WobbleDirection::Horizontal => (amp * wobble, 0.0),
WobbleDirection::Vertical => (0.0, amp * wobble),
WobbleDirection::Both => (
amp * wobble,
amp * (wobble + std::f64::consts::FRAC_PI_2).sin(),
),
};
unsafe {
let _ = SetWindowPos(
window,
None,
base_x + dx.round() as i32,
base_y + dy.round() as i32,
0,
0,
wobble_flags(),
);
}
thread::sleep(WOBBLE_STEP);
}
unsafe {
let _ = SetWindowPos(window, None, base_x, base_y, 0, 0, wobble_flags());
}
true
}
pub fn wobble_random_window(settings: WobbleSettings) -> Option<WindowInfo> {
let windows = list_windows();
if windows.is_empty() {
return None;
}
let mut rng = rng();
let index = rng.random_range(0..windows.len());
let target = &windows[index];
wobble_window(target.handle(), settings);
Some(target.clone())
}
pub fn wobble_by_title(title: &str, settings: WobbleSettings) -> Option<WindowInfo> {
list_windows()
.into_iter()
.find(|w| w.title() == title)
.inspect(|w| {
wobble_window(w.handle(), settings);
})
}
pub fn wobble_foreground_window(settings: WobbleSettings) -> bool {
let window = unsafe { GetForegroundWindow() };
if window.0.is_null() {
return false;
}
wobble_window(window, settings)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn zero_duration_is_a_noop() {
let window = unsafe { GetForegroundWindow() };
if window.0.is_null() {
return;
}
let settings = WobbleSettings {
duration: Duration::ZERO,
..WobbleSettings::default()
};
assert!(wobble_window(window, settings));
}
}