From faf926288fd5780e77f1496dbf088bdb5ccedebb Mon Sep 17 00:00:00 2001 From: ByteAtATime Date: Sun, 30 Nov 2025 13:41:20 -0800 Subject: [PATCH] feat: implement confetti command --- Cargo.toml | 2 +- src/main.rs | 151 +++++++++++++++++++++++++++++++++++++++++++++++++ src/message.rs | 1 + src/update.rs | 7 +++ 4 files changed, 160 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index f0395b0..4fdaef9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ clap = { version = "4", features = ["derive"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" # pinned to latest commit as of 2025-11-21 -iced = { git = "https://github.com/iced-rs/iced", rev = "8bfd099c5929d927a3fdde666d4c645d0bd83cb7", features = ["advanced", "canvas", "image", "markdown", "svg", "tokio"] } +iced = { git = "https://github.com/iced-rs/iced", rev = "8bfd099c5929d927a3fdde666d4c645d0bd83cb7", features = ["advanced", "canvas", "image", "markdown", "svg", "tokio", "wgpu"] } reqwest = { version = "0.12.8", features = ["blocking", "rustls-tls"] } tokio = { version = "1.43.0", features = ["rt-multi-thread", "macros", "net", "sync", "process"] } phf = { version = "0.13.1", features = ["macros"] } diff --git a/src/main.rs b/src/main.rs index a4140dc..d7f9e18 100644 --- a/src/main.rs +++ b/src/main.rs @@ -61,6 +61,7 @@ enum Command { Toggle, Dev, Settings, + Confetti, } fn boot() -> (State, iced::Task) { @@ -184,6 +185,7 @@ fn main() -> Result<(), Box> { Some(Command::Daemon) => run_daemon(), Some(Command::Dev) => run_dev(), Some(Command::Settings) => settings::run(), + Some(Command::Confetti) => run_confetti(), None => run_application(), } } @@ -298,3 +300,152 @@ fn run_daemon() -> Result<(), Box> { Ok(()) } + +#[cfg(target_os = "linux")] +fn run_confetti() -> Result<(), Box> { + use crate::components::confetti::{Manager, Options}; + use iced::widget::canvas; + use iced::{Color, Length, Point, Rectangle, Size, Task, time, window}; + use iced_layershell::reexport::{Anchor, KeyboardInteractivity, Layer}; + use iced_layershell::settings::{LayerShellSettings, StartMode}; + use iced_layershell::to_layer_message; + + struct ConfettiState { + manager: Manager, + window_id: Option, + fired: bool, + } + + #[to_layer_message] + #[derive(Debug, Clone)] + enum ConfettiMessage { + Tick, + WindowOpened(window::Id), + MonitorSize(Option), + } + + fn boot() -> (ConfettiState, Task) { + ( + ConfettiState { + manager: Manager::new(), + window_id: None, + fired: false, + }, + Task::none(), + ) + } + + fn update(state: &mut ConfettiState, message: ConfettiMessage) -> Task { + match message { + ConfettiMessage::Tick => { + state.manager.update(); + Task::none() + } + ConfettiMessage::WindowOpened(id) => { + state.window_id = Some(id); + window::size(id).map(|arg0: iced::Size| ConfettiMessage::MonitorSize(Some(arg0))) + } + ConfettiMessage::MonitorSize(size) => { + if state.fired { + return Task::none(); + } + state.fired = true; + + // distance = (velocity * cos(angle))/(1 - decay) + // rearranging for velocity, distance(1-decay)/cos(angle) + // (width/2)(1-0.97)/cos(45) = width / 47.1 + let size = size.unwrap_or(Size::new(1920.0, 1080.0)); + let options = Options { + particle_count: 300, + spread: 45.0, + start_velocity: size.width / 47.1, + gravity: size.width / 960.0, + decay: 0.97, + ticks: 160.0, // about 2.5 seconds, seems like a reasonable duration + origin: Point { x: 1.0, y: 1.0 }, + scalar: 1.2, + ..Default::default() + }; + + state.manager.fire_with_bounds( + Options { + angle: 135.0, + origin: Point { x: 1.0, y: 1.0 }, + ..options.clone() + }, + Rectangle::new(Point::ORIGIN, size), + ); + + state.manager.fire_with_bounds( + Options { + angle: 45.0, + origin: Point { x: 0.0, y: 1.0 }, + ..options + }, + Rectangle::new(Point::ORIGIN, size), + ); + + Task::none() + } + _ => Task::none(), + } + } + + fn view(state: &ConfettiState) -> iced::Element<'_, ConfettiMessage> { + iced::widget::container( + canvas(&state.manager) + .width(Length::Fill) + .height(Length::Fill), + ) + .width(Length::Fill) + .height(Length::Fill) + .style(|_| iced::widget::container::Style { + background: Some(Color::TRANSPARENT.into()), + ..Default::default() + }) + .into() + } + + fn subscription(state: &ConfettiState) -> iced::Subscription { + use std::time::Duration; + + let tick = time::every(Duration::from_millis(1000 / 60)).map(|_| ConfettiMessage::Tick); + + let window_events = if state.window_id.is_none() { + iced::event::listen_with(|event, _status, id| { + if let iced::Event::Window(iced::window::Event::Opened { .. }) = event { + return Some(ConfettiMessage::WindowOpened(id)); + } + None + }) + } else { + iced::Subscription::none() + }; + + iced::Subscription::batch([tick, window_events]) + } + + iced_layershell::application(boot, || String::from("flare-confetti"), update, view) + .subscription(subscription) + .style(|_state, theme| iced::theme::Style { + background_color: Color::TRANSPARENT, + text_color: theme.palette().text, + }) + .layer_settings(LayerShellSettings { + start_mode: StartMode::Active, + layer: Layer::Overlay, + anchor: Anchor::Top | Anchor::Bottom | Anchor::Left | Anchor::Right, + keyboard_interactivity: KeyboardInteractivity::None, + exclusive_zone: 0, + events_transparent: true, + ..Default::default() + }) + .run() + .map_err(|e| e.to_string().into()) +} + +#[cfg(not(target_os = "linux"))] +fn run_confetti() -> Result<(), Box> { + eprintln!("Confetti is not supported yet!"); + Ok(()) +} diff --git a/src/message.rs b/src/message.rs index 039f70a..6b9d2e8 100644 --- a/src/message.rs +++ b/src/message.rs @@ -31,6 +31,7 @@ pub enum Message { ExtensionLaunched(Result<(SidecarWriter, Arc>), String>), LaunchApp(AppEntry), ResetFrecency(String), + TriggerConfetti, PopToRoot, WindowOpened(window::Id), diff --git a/src/update.rs b/src/update.rs index 967ae57..a05d4aa 100644 --- a/src/update.rs +++ b/src/update.rs @@ -220,6 +220,13 @@ pub fn update(state: &mut State, message: Message) -> Task { } state.update_selected_actions(); } + Message::TriggerConfetti => { + let current_exe = + std::env::current_exe().unwrap_or_else(|_| std::path::PathBuf::from("flare")); + let _ = std::process::Command::new(current_exe) + .arg("confetti") + .spawn(); + } Message::PopToRoot => { state.writer = None; state.reader = None;