diff --git a/src/overlay.rs b/src/overlay.rs index 3b56c66..03ca3d0 100644 --- a/src/overlay.rs +++ b/src/overlay.rs @@ -306,28 +306,64 @@ pub fn show_shadowplay_toast(title: &str, subtitle: &str, icon: ToastIcon) { } window.style_context().add_provider(&css_provider, gtk::STYLE_PROVIDER_PRIORITY_USER); + let start_time = Instant::now(); + let hbox = Box::new(Orientation::Horizontal, 10); hbox.style_context().add_class("toast-card"); hbox.set_size_request(320, 56); hbox.set_app_paintable(true); + let start_time_draw = start_time; hbox.connect_draw(move |widget, cr| { let w = widget.allocated_width() as f64; let h = widget.allocated_height() as f64; + let elapsed = start_time_draw.elapsed().as_secs_f64(); + let total_dur = 2.80_f64; + + // Dynamic fade transition + let fade_in = (elapsed / 0.22).min(1.0); + let fade_out = ((total_dur - elapsed) / 0.32).clamp(0.0, 1.0); + let alpha = fade_in.min(fade_out); cr.set_operator(gtk::cairo::Operator::Over); - // Sleek translucent neutral dark glass background (matching HUD cards) - cr.set_source_rgba(10.0 / 255.0, 10.0 / 255.0, 10.0 / 255.0, 0.65); + // 1. Entrance accent bloom (first 0.55s) + let bloom = if elapsed < 0.55 { (1.0 - elapsed / 0.55).powi(2) } else { 0.0 }; + if bloom > 0.01 { + cr.set_source_rgba(active_accent.0, active_accent.1, active_accent.2, 0.35 * bloom * alpha); + cr.set_line_width(2.0); + cr.rectangle(1.0, 1.0, w - 2.0, h - 2.0); + let _ = cr.stroke(); + } + + // 2. Sleek translucent neutral dark glass background (matching HUD cards) + cr.set_source_rgba(10.0 / 255.0, 10.0 / 255.0, 10.0 / 255.0, 0.68 * alpha); cr.rectangle(0.0, 0.0, w, h); let _ = cr.fill(); - // Refined subtle accent stroke framing the card - cr.set_source_rgba(active_accent.0, active_accent.1, active_accent.2, 0.55); + // 3. Top specular frost highlight + cr.set_source_rgba(1.0, 1.0, 1.0, 0.15 * alpha); + cr.set_line_width(1.0); + cr.move_to(1.0, 0.5); + cr.line_to(w - 1.0, 0.5); + let _ = cr.stroke(); + + // 4. Refined subtle accent stroke framing the card + cr.set_source_rgba(active_accent.0, active_accent.1, active_accent.2, 0.55 * alpha); cr.set_line_width(1.0); cr.rectangle(0.5, 0.5, w - 1.0, h - 1.0); let _ = cr.stroke(); + // 5. Animated countdown timer line on bottom edge + let progress = 1.0 - (elapsed / total_dur).clamp(0.0, 1.0); + cr.set_source_rgba(1.0, 1.0, 1.0, 0.08 * alpha); + cr.rectangle(0.0, h - 2.0, w, 2.0); + let _ = cr.fill(); + + cr.set_source_rgba(active_accent.0, active_accent.1, active_accent.2, 0.75 * alpha); + cr.rectangle(0.0, h - 2.0, w * progress, 2.0); + let _ = cr.fill(); + gtk::glib::Propagation::Proceed }); @@ -337,18 +373,21 @@ pub fn show_shadowplay_toast(title: &str, subtitle: &str, icon: ToastIcon) { icon_area.set_margin_start(14); icon_area.set_valign(gtk::Align::Center); let icon_type = icon; + let start_time_icon = start_time; icon_area.connect_draw(move |_, cr| { let cx = 16.0; let cy = 16.0; + let elapsed = start_time_icon.elapsed().as_secs_f64(); match icon_type { ToastIcon::Replay => { + let scale = if elapsed < 0.35 { 0.85 + 0.15 * (elapsed / 0.35) } else { 1.0 }; cr.set_source_rgb(accent_rgb.0, accent_rgb.1, accent_rgb.2); cr.set_line_width(1.8); cr.set_line_cap(gtk::cairo::LineCap::Round); cr.set_line_join(gtk::cairo::LineJoin::Round); - let ch_h = 7.0; - let ch_w = 4.4; - let gap = 4.2; + let ch_h = 7.0 * scale; + let ch_w = 4.4 * scale; + let gap = 4.2 * scale; let start_x = cx - (ch_w + gap) * 0.5; for i in 0..2 { let tip_x = start_x + (i as f64) * gap; @@ -360,6 +399,7 @@ pub fn show_shadowplay_toast(title: &str, subtitle: &str, icon: ToastIcon) { } } ToastIcon::Record => { + let pulse = ((elapsed * 5.0).sin() * 0.5 + 0.5) * 0.35 + 0.65; let red = (0.937, 0.267, 0.267); cr.set_source_rgb(red.0, red.1, red.2); cr.set_line_width(1.6); @@ -370,7 +410,7 @@ pub fn show_shadowplay_toast(title: &str, subtitle: &str, icon: ToastIcon) { cr.move_to(cx - half + arm, cy + half); cr.line_to(cx - half, cy + half); cr.line_to(cx - half, cy + half - arm); cr.move_to(cx + half - arm, cy + half); cr.line_to(cx + half, cy + half); cr.line_to(cx + half, cy + half - arm); let _ = cr.stroke(); - cr.arc(cx, cy, 3.5, 0.0, PI * 2.0); + cr.arc(cx, cy, 3.5 * pulse, 0.0, PI * 2.0); let _ = cr.fill(); } ToastIcon::Save => { @@ -451,7 +491,6 @@ pub fn show_shadowplay_toast(title: &str, subtitle: &str, icon: ToastIcon) { let window_clone = window.clone(); let hbox_clone = hbox.clone(); let fixed_clone = fixed.clone(); - let start_time = Instant::now(); gtk::glib::timeout_add_local(Duration::from_millis(16), move || { let elapsed = start_time.elapsed().as_secs_f32(); @@ -460,14 +499,14 @@ pub fn show_shadowplay_toast(title: &str, subtitle: &str, icon: ToastIcon) { return gtk::glib::ControlFlow::Break; } - let slide_x = if elapsed < 0.35 { - let t = (elapsed / 0.35).min(1.0); - let ease = 1.0 - (1.0 - t).powi(3); + let slide_x = if elapsed < 0.38 { + let t = (elapsed / 0.38).min(1.0); + let ease = 1.0 - (1.0 - t).powi(4); (1.0 - ease) * 320.0 - } else if elapsed < 2.40 { + } else if elapsed < 2.45 { 0.0 } else { - let t = ((elapsed - 2.40) / 0.40).min(1.0); + let t = ((elapsed - 2.45) / 0.35).min(1.0); let ease = t.powi(3); ease * 320.0 }; diff --git a/src/overlay_egui.rs b/src/overlay_egui.rs index 8876009..7de9fe7 100644 --- a/src/overlay_egui.rs +++ b/src/overlay_egui.rs @@ -2154,21 +2154,28 @@ impl ScytheOverlayApp { let accent = self.accent_color(); if let Some(notif) = &self.hud_notification { let elapsed = notif.start_time.elapsed().as_secs_f32(); - if elapsed < notif.duration_secs { + let total_dur = notif.duration_secs; + if elapsed < total_dur { ctx.request_repaint(); // 60 FPS animation - let slide_x = if elapsed < 0.35 { - let t = (elapsed / 0.35).min(1.0); - let ease = 1.0 - (1.0 - t).powi(3); - (1.0 - ease) * 360.0 - } else if elapsed < notif.duration_secs - 0.40 { + // Silky smooth quintic decel entrance & cubic exit slide + let slide_x = if elapsed < 0.38 { + let t = (elapsed / 0.38).min(1.0); + let ease = 1.0 - (1.0 - t).powi(4); + (1.0 - ease) * 320.0 + } else if elapsed < total_dur - 0.35 { 0.0 } else { - let t = ((elapsed - (notif.duration_secs - 0.40)) / 0.40).min(1.0); + let t = ((elapsed - (total_dur - 0.35)) / 0.35).min(1.0); let ease = t.powi(3); - ease * 360.0 + ease * 320.0 }; + // Dynamic fade transition + let fade_in = (elapsed / 0.22).clamp(0.0, 1.0); + let fade_out = ((total_dur - elapsed) / 0.32).clamp(0.0, 1.0); + let anim_alpha = fade_in.min(fade_out); + let screen_w = ui.available_width(); let card_w = 320.0; let card_h = 56.0; @@ -2179,34 +2186,70 @@ impl ScytheOverlayApp { let painter = ui.painter(); - // Sleek translucent neutral dark glass toast card (matching HUD cards) + let active_color = if notif.icon == crate::overlay::ToastIcon::Record || notif.icon == crate::overlay::ToastIcon::Error { + Color32::from_rgb(239, 68, 68) + } else { + accent + }; + + // 1. Entrance accent bloom / gleam pulse (first 0.55s) + let bloom = if elapsed < 0.55 { (1.0 - elapsed / 0.55).powi(2) } else { 0.0 }; + if bloom > 0.01 { + painter.rect_stroke( + toast_rect.expand(2.0), + CornerRadius::ZERO, + Stroke::new(1.0_f32, Color32::from_rgba_unmultiplied(active_color.r(), active_color.g(), active_color.b(), (bloom * 90.0 * anim_alpha) as u8)), + egui::StrokeKind::Outside, + ); + } + + // 2. Sleek translucent neutral dark glass toast card fill painter.rect_filled( toast_rect, CornerRadius::ZERO, - Color32::from_rgba_unmultiplied(10, 10, 10, 165), + Color32::from_rgba_unmultiplied(10, 10, 10, (170.0 * anim_alpha) as u8), ); - // Refined subtle accent stroke framing the card - let stroke_color = if notif.icon == crate::overlay::ToastIcon::Record || notif.icon == crate::overlay::ToastIcon::Error { - Color32::from_rgba_unmultiplied(239, 68, 68, 140) - } else { - Color32::from_rgba_unmultiplied(accent.r(), accent.g(), accent.b(), 140) - }; + // 3. Top subtle specular frost highlight + painter.line_segment( + [toast_rect.left_top() + Vec2::new(1.0, 0.5), toast_rect.right_top() + Vec2::new(-1.0, 0.5)], + Stroke::new(1.0_f32, Color32::from_rgba_unmultiplied(255, 255, 255, (38.0 * anim_alpha) as u8)), + ); + + // 4. Refined accent stroke framing the card painter.rect_stroke( toast_rect, CornerRadius::ZERO, - Stroke::new(1.0_f32, stroke_color), + Stroke::new(1.0_f32, Color32::from_rgba_unmultiplied(active_color.r(), active_color.g(), active_color.b(), (140.0 * anim_alpha) as u8)), egui::StrokeKind::Inside, ); + // 5. Animated Countdown Timer Line (bottom edge) + let progress = 1.0 - (elapsed / total_dur).clamp(0.0, 1.0); + let bar_track = egui::Rect::from_min_size(egui::pos2(toast_rect.left(), toast_rect.bottom() - 2.0), egui::vec2(card_w, 2.0)); + painter.rect_filled(bar_track, CornerRadius::ZERO, Color32::from_rgba_unmultiplied(255, 255, 255, (16.0 * anim_alpha) as u8)); + let bar_rect = egui::Rect::from_min_size(egui::pos2(toast_rect.left(), toast_rect.bottom() - 2.0), egui::vec2(card_w * progress, 2.0)); + painter.rect_filled(bar_rect, CornerRadius::ZERO, Color32::from_rgba_unmultiplied(active_color.r(), active_color.g(), active_color.b(), (190.0 * anim_alpha) as u8)); + // Left icon area (centered vertically at x = 24.0) let icon_center = egui::pos2(toast_rect.left() + 24.0, toast_rect.center().y); + if bloom > 0.01 { + painter.circle_filled( + icon_center, + 12.0, + Color32::from_rgba_unmultiplied(active_color.r(), active_color.g(), active_color.b(), (bloom * 40.0 * anim_alpha) as u8), + ); + } + + let icon_tint = Color32::from_rgba_unmultiplied(active_color.r(), active_color.g(), active_color.b(), (255.0 * anim_alpha) as u8); match notif.icon { crate::overlay::ToastIcon::Replay => { - draw_replay_icon(painter, icon_center, 12.0, true, accent); + let scale = if elapsed < 0.35 { 0.85 + 0.15 * (elapsed / 0.35) } else { 1.0 }; + draw_replay_icon(painter, icon_center, 12.0 * scale, true, icon_tint); } crate::overlay::ToastIcon::Record => { - let red_bright = Color32::from_rgb(239, 68, 68); + let pulse = ((elapsed * 5.0).sin() * 0.5 + 0.5) * 0.35 + 0.65; + let red_bright = Color32::from_rgba_unmultiplied(239, 68, 68, (255.0 * anim_alpha) as u8); let half = 8.5; let arm = 3.2; let stroke = Stroke::new(1.6_f32, red_bright); @@ -2218,7 +2261,7 @@ impl ScytheOverlayApp { painter.line_segment([icon_center + Vec2::new(-half, half), icon_center + Vec2::new(-half, half - arm)], stroke); painter.line_segment([icon_center + Vec2::new(half - arm, half), icon_center + Vec2::new(half, half)], stroke); painter.line_segment([icon_center + Vec2::new(half, half), icon_center + Vec2::new(half, half - arm)], stroke); - painter.circle_filled(icon_center, 3.5, red_bright); + painter.circle_filled(icon_center, 3.2 * pulse, red_bright); } crate::overlay::ToastIcon::Save => { painter.add(egui::epaint::PathShape::line( @@ -2227,7 +2270,7 @@ impl ScytheOverlayApp { icon_center + Vec2::new(-2.0, 4.5), icon_center + Vec2::new(6.0, -4.5), ], - Stroke::new(2.0_f32, accent), + Stroke::new(2.0_f32, icon_tint), )); } crate::overlay::ToastIcon::Cursor => { @@ -2242,35 +2285,34 @@ impl ScytheOverlayApp { icon_center + Vec2::new(5.0, 1.0), icon_center + Vec2::new(-5.0, -7.0), ], - Stroke::new(1.8_f32, accent), + Stroke::new(1.8_f32, icon_tint), )); } crate::overlay::ToastIcon::Error => { - let red = Color32::from_rgb(239, 68, 68); - let stroke = Stroke::new(2.0_f32, red); + let stroke = Stroke::new(2.0_f32, icon_tint); painter.line_segment([icon_center + Vec2::new(-5.0, -5.0), icon_center + Vec2::new(5.0, 5.0)], stroke); painter.line_segment([icon_center + Vec2::new(5.0, -5.0), icon_center + Vec2::new(-5.0, 5.0)], stroke); } crate::overlay::ToastIcon::Info => { - painter.circle_filled(icon_center, 4.5, accent); + painter.circle_filled(icon_center, 4.5, icon_tint); } } // Text stack (Title + Subtitle) let text_left = toast_rect.left() + 46.0; painter.text( - egui::pos2(text_left, toast_rect.top() + 19.0), + egui::pos2(text_left, toast_rect.top() + 18.0), egui::Align2::LEFT_CENTER, ¬if.title, FontId::proportional(12.5), - Color32::WHITE, + Color32::from_rgba_unmultiplied(255, 255, 255, (255.0 * anim_alpha) as u8), ); painter.text( - egui::pos2(text_left, toast_rect.top() + 37.0), + egui::pos2(text_left, toast_rect.top() + 36.0), egui::Align2::LEFT_CENTER, ¬if.subtitle, FontId::proportional(11.0), - Color32::from_rgb(175, 180, 190), + Color32::from_rgba_unmultiplied(175, 180, 190, (230.0 * anim_alpha) as u8), ); } else { self.hud_notification = None; @@ -2657,18 +2699,23 @@ impl eframe::App for ShadowPlayToastApp { } ctx.request_repaint_after(Duration::from_millis(16)); - let slide_x = if elapsed < 0.35 { - let t = (elapsed / 0.35).min(1.0); - let ease = 1.0 - (1.0 - t).powi(3); + let slide_x = if elapsed < 0.38 { + let t = (elapsed / 0.38).min(1.0); + let ease = 1.0 - (1.0 - t).powi(4); (1.0 - ease) * 320.0 - } else if elapsed < total_dur - 0.40 { + } else if elapsed < total_dur - 0.35 { 0.0 } else { - let t = ((elapsed - (total_dur - 0.40)) / 0.40).min(1.0); + let t = ((elapsed - (total_dur - 0.35)) / 0.35).min(1.0); let ease = t.powi(3); ease * 320.0 }; + // Dynamic fade transition + let fade_in = (elapsed / 0.22).clamp(0.0, 1.0); + let fade_out = ((total_dur - elapsed) / 0.32).clamp(0.0, 1.0); + let anim_alpha = fade_in.min(fade_out); + let mut visuals = egui::Visuals::dark(); visuals.panel_fill = Color32::TRANSPARENT; visuals.window_fill = Color32::TRANSPARENT; @@ -2683,34 +2730,70 @@ impl eframe::App for ShadowPlayToastApp { ); let painter = ui.painter(); - // Sleek translucent neutral dark glass toast card (matching HUD cards) + let active_color = if self.icon == crate::overlay::ToastIcon::Record || self.icon == crate::overlay::ToastIcon::Error { + Color32::from_rgb(239, 68, 68) + } else { + self.accent + }; + + // 1. Entrance accent bloom / gleam pulse (first 0.55s) + let bloom = if elapsed < 0.55 { (1.0 - elapsed / 0.55).powi(2) } else { 0.0 }; + if bloom > 0.01 { + painter.rect_stroke( + rect.expand(2.0), + CornerRadius::ZERO, + Stroke::new(1.0_f32, Color32::from_rgba_unmultiplied(active_color.r(), active_color.g(), active_color.b(), (bloom * 90.0 * anim_alpha) as u8)), + egui::StrokeKind::Outside, + ); + } + + // 2. Sleek translucent neutral dark glass toast card fill painter.rect_filled( rect, CornerRadius::ZERO, - Color32::from_rgba_unmultiplied(10, 10, 10, 165), + Color32::from_rgba_unmultiplied(10, 10, 10, (170.0 * anim_alpha) as u8), ); - // Refined subtle accent stroke framing the card - let stroke_color = if self.icon == crate::overlay::ToastIcon::Record || self.icon == crate::overlay::ToastIcon::Error { - Color32::from_rgba_unmultiplied(239, 68, 68, 140) - } else { - Color32::from_rgba_unmultiplied(self.accent.r(), self.accent.g(), self.accent.b(), 140) - }; + // 3. Top subtle specular frost highlight + painter.line_segment( + [rect.left_top() + Vec2::new(1.0, 0.5), rect.right_top() + Vec2::new(-1.0, 0.5)], + Stroke::new(1.0_f32, Color32::from_rgba_unmultiplied(255, 255, 255, (38.0 * anim_alpha) as u8)), + ); + + // 4. Refined accent stroke framing the card painter.rect_stroke( rect, CornerRadius::ZERO, - Stroke::new(1.0_f32, stroke_color), + Stroke::new(1.0_f32, Color32::from_rgba_unmultiplied(active_color.r(), active_color.g(), active_color.b(), (140.0 * anim_alpha) as u8)), egui::StrokeKind::Inside, ); + // 5. Animated Countdown Timer Line (bottom edge) + let progress = 1.0 - (elapsed / total_dur).clamp(0.0, 1.0); + let bar_track = egui::Rect::from_min_size(egui::pos2(rect.left(), rect.bottom() - 2.0), egui::vec2(320.0, 2.0)); + painter.rect_filled(bar_track, CornerRadius::ZERO, Color32::from_rgba_unmultiplied(255, 255, 255, (16.0 * anim_alpha) as u8)); + let bar_rect = egui::Rect::from_min_size(egui::pos2(rect.left(), rect.bottom() - 2.0), egui::vec2(320.0 * progress, 2.0)); + painter.rect_filled(bar_rect, CornerRadius::ZERO, Color32::from_rgba_unmultiplied(active_color.r(), active_color.g(), active_color.b(), (190.0 * anim_alpha) as u8)); + // Left icon area (centered vertically at x = 24.0) let icon_center = egui::pos2(rect.left() + 24.0, rect.center().y); + if bloom > 0.01 { + painter.circle_filled( + icon_center, + 12.0, + Color32::from_rgba_unmultiplied(active_color.r(), active_color.g(), active_color.b(), (bloom * 40.0 * anim_alpha) as u8), + ); + } + + let icon_tint = Color32::from_rgba_unmultiplied(active_color.r(), active_color.g(), active_color.b(), (255.0 * anim_alpha) as u8); match self.icon { crate::overlay::ToastIcon::Replay => { - draw_replay_icon(painter, icon_center, 12.0, true, self.accent); + let scale = if elapsed < 0.35 { 0.85 + 0.15 * (elapsed / 0.35) } else { 1.0 }; + draw_replay_icon(painter, icon_center, 12.0 * scale, true, icon_tint); } crate::overlay::ToastIcon::Record => { - let red_bright = Color32::from_rgb(239, 68, 68); + let pulse = ((elapsed * 5.0).sin() * 0.5 + 0.5) * 0.35 + 0.65; + let red_bright = Color32::from_rgba_unmultiplied(239, 68, 68, (255.0 * anim_alpha) as u8); let half = 8.5; let arm = 3.2; let stroke = Stroke::new(1.6_f32, red_bright); @@ -2722,7 +2805,7 @@ impl eframe::App for ShadowPlayToastApp { painter.line_segment([icon_center + Vec2::new(-half, half), icon_center + Vec2::new(-half, half - arm)], stroke); painter.line_segment([icon_center + Vec2::new(half - arm, half), icon_center + Vec2::new(half, half)], stroke); painter.line_segment([icon_center + Vec2::new(half, half), icon_center + Vec2::new(half, half - arm)], stroke); - painter.circle_filled(icon_center, 3.5, red_bright); + painter.circle_filled(icon_center, 3.2 * pulse, red_bright); } crate::overlay::ToastIcon::Save => { painter.add(egui::epaint::PathShape::line( @@ -2731,7 +2814,7 @@ impl eframe::App for ShadowPlayToastApp { icon_center + Vec2::new(-2.0, 4.5), icon_center + Vec2::new(6.0, -4.5), ], - Stroke::new(2.0_f32, self.accent), + Stroke::new(2.0_f32, icon_tint), )); } crate::overlay::ToastIcon::Cursor => { @@ -2746,35 +2829,34 @@ impl eframe::App for ShadowPlayToastApp { icon_center + Vec2::new(5.0, 1.0), icon_center + Vec2::new(-5.0, -7.0), ], - Stroke::new(1.8_f32, self.accent), + Stroke::new(1.8_f32, icon_tint), )); } crate::overlay::ToastIcon::Error => { - let red = Color32::from_rgb(239, 68, 68); - let stroke = Stroke::new(2.0_f32, red); + let stroke = Stroke::new(2.0_f32, icon_tint); painter.line_segment([icon_center + Vec2::new(-5.0, -5.0), icon_center + Vec2::new(5.0, 5.0)], stroke); painter.line_segment([icon_center + Vec2::new(5.0, -5.0), icon_center + Vec2::new(-5.0, 5.0)], stroke); } crate::overlay::ToastIcon::Info => { - painter.circle_filled(icon_center, 4.5, self.accent); + painter.circle_filled(icon_center, 4.5, icon_tint); } } // Text stack (Title + Subtitle) let text_left = rect.left() + 46.0; painter.text( - egui::pos2(text_left, rect.top() + 19.0), + egui::pos2(text_left, rect.top() + 18.0), egui::Align2::LEFT_CENTER, &self.title, FontId::proportional(12.5), - Color32::WHITE, + Color32::from_rgba_unmultiplied(255, 255, 255, (255.0 * anim_alpha) as u8), ); painter.text( - egui::pos2(text_left, rect.top() + 37.0), + egui::pos2(text_left, rect.top() + 36.0), egui::Align2::LEFT_CENTER, &self.subtitle, FontId::proportional(11.0), - Color32::from_rgb(175, 180, 190), + Color32::from_rgba_unmultiplied(175, 180, 190, (230.0 * anim_alpha) as u8), ); }); }