using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Animation;
// ============================================================
// SHARED FADE / SLIDE
//
// One place for the timing and easing every surface animates with, so the main window, the
// dialogs, the overlays and the rail flyouts all appear the same way. An app that hand-rolls a
// DoubleAnimation per call site ends up with four durations and three easings, which reads as
// four different products.
//
// THIS IS THE CANONICAL COPY (consolidated 2026-08-08). Every app carries a byte-identical
// copy of this file - only the namespace line differs - so a diff against this file IS the
// drift check. It replaced five copies that had each grown a different subset: the kit shipped
// no fade-out at all, so KillerNotes invented FadeOutAndClose and MmdPdf invented
// FadeOut(element, done) independently, while KillerScan, KillerShell and Killendar closed
// every dialog with no fade. When something here needs to change, change it HERE first, then
// re-copy into every app.
//
// COPY THIS FILE INTO THE APP and change the namespace - it is a plain static helper with no
// dependencies. Usage:
//
// Loaded += (_, _) => Anim.FadeIn(RootBorder); // a dialog, on its root Border
// Anim.SlideInX(flyout, -12); // a rail flyout, gliding out of the rail
//
// The dialog's root Border must start at Opacity="0" in XAML, or the first frame paints solid
// before the animation takes over and the fade is a flicker rather than a fade.
//
// Two fade-outs, for two shapes of close:
// - FadeOutAndClose(window, ref flag): call from an OnClosing override; it cancels that close,
// fades the whole window, then closes for real. The default for a Window.
// - FadeOut(element, done): fades a named element and runs a callback. For a dialog that must
// hold its DialogResult until after the fade: assigning DialogResult is itself a close
// request, and WPF resets DialogResult to null whenever a close is canceled, so such a
// dialog records the result, fades, and assigns it in the callback (see MmdPdf's
// FileDialog.OnClosing).
// ============================================================
namespace MmdPdf.Controls
{
internal static class Anim
{
/// Standard fade duration in milliseconds, shared by all surfaces.
public const int FadeMs = 150;
/// Fades an element's opacity from 0 to 1 over FadeMs with an ease-out curve.
public static void FadeIn(UIElement element)
{
element.BeginAnimation(UIElement.OpacityProperty,
new DoubleAnimation(0, 1, new Duration(TimeSpan.FromMilliseconds(FadeMs)))
{
EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseOut }
});
}
/// Fades an element out to 0 and calls when it lands.
/// EaseIn mirrors FadeIn's EaseOut, so the surface accelerates away as smoothly as it
/// arrived. Windows use this to fade before actually closing; without it a dialog that
/// fades in vanishes instantly, which reads as a glitch.
public static void FadeOut(UIElement element, Action done)
{
var a = new DoubleAnimation(element.Opacity, 0, new Duration(TimeSpan.FromMilliseconds(FadeMs)))
{
EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseIn }
};
// Completed fires even if the value is already 0, so the callback cannot be stranded.
a.Completed += (_, _) => done?.Invoke();
element.BeginAnimation(UIElement.OpacityProperty, a);
}
///
/// Fades a window out and then closes it for real. Call from an OnClosing override, or
/// wire it to a close button; it returns true if it took over the close, in which case
/// the caller must cancel this one and do nothing else.
///
/// Driven per composition frame rather than with a DoubleAnimation, matching the palette
/// fade in each app's ThemeManager and for the same reason: Timeline-based animation is
/// suppressed outright in some environments (remote sessions, "show animations in
/// Windows" turned off) and fails silently when it is, which reads as a window that
/// vanishes instead of fading. A per-frame opacity write always runs.
///
public static bool FadeOutAndClose(Window window, ref bool alreadyFaded)
{
if (alreadyFaded || !window.IsLoaded || window.Opacity <= 0.01) return false;
alreadyFaded = true;
// Release FadeIn's animation FIRST. It is a DoubleAnimation with the default
// FillBehavior.HoldEnd, so it keeps holding Opacity after it finishes - and a held
// animation outranks a local value, which means every per-frame write below would be
// silently discarded and the window would sit at full opacity until the timer closed it.
window.BeginAnimation(UIElement.OpacityProperty, null);
window.Opacity = 1;
var clock = System.Diagnostics.Stopwatch.StartNew();
double from = window.Opacity;
EventHandler? tick = null;
tick = (_, _) =>
{
double t = clock.Elapsed.TotalMilliseconds / FadeMs;
if (t >= 1)
{
CompositionTarget.Rendering -= tick;
window.Opacity = 0;
// Off the render callback before closing: tearing the window down inside a
// Rendering handler reenters composition.
//
// Hand foreground back to the owner BEFORE the teardown. This close is
// deferred to a dispatcher callback with no input message behind it, and
// Win32 is free to ignore the activation it would otherwise do for us when
// an owned window is destroyed. With an owner chain (main window -> modeless
// pad -> modal dialog) nothing reclaimed foreground on the way back out and
// the MAIN window sank behind other applications. Activating first means the
// window being destroyed is not the foreground one, so there is nothing to
// hand off. For a modal child the owner is Win32-disabled (ShowDialog
// disables the thread's windows without touching WPF's IsEnabled, so it
// cannot be tested for here) and Activate is a harmless no-op; WPF's own
// dialog teardown re-enables and reactivates that case.
window.Dispatcher.BeginInvoke(new Action(() =>
{
Window? owner = window.Owner;
if (owner != null && owner.IsVisible) owner.Activate();
window.Close();
}));
return;
}
window.Opacity = from * (1 - t * t); // quadratic ease-in, mirrors FadeIn
};
CompositionTarget.Rendering += tick;
return true;
}
/// Fade plus a horizontal glide from dx px to rest (negative dx = in from
/// the left). Used by the rail flyouts so they read as sliding out of the rail.
public static void SlideInX(UIElement element, double dx)
{
var tt = new TranslateTransform(dx, 0);
element.RenderTransform = tt;
FadeIn(element);
var a = new DoubleAnimation(dx, 0, new Duration(TimeSpan.FromMilliseconds(FadeMs)))
{
EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseOut }
};
// Clear the transform when it lands: a RenderTransform left in place on a laid-out
// element is a permanent extra composition layer for no benefit.
a.Completed += (_, _) => element.RenderTransform = null;
tt.BeginAnimation(TranslateTransform.XProperty, a);
}
}
}