core/AsciiFX.Desktop.cs

using System;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Text;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;
 
namespace AsciiFX
{
    // Shows an animation on the desktop: above the wallpaper (static or live), below every normal window,
    // with a see-through background and clicks passing straight through. Windows only.
    public class DesktopPlayer : Form
    {
        [DllImport("user32.dll")] static extern bool SetProcessDPIAware();
        [DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern IntPtr FindWindowEx(IntPtr parent, IntPtr after, string cls, string title);
        [DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern int GetWindowText(IntPtr hwnd, StringBuilder text, int max);
        [DllImport("user32.dll")] static extern bool IsWindowVisible(IntPtr hwnd);
        [DllImport("user32.dll")] static extern IntPtr GetWindow(IntPtr hwnd, uint cmd);
        [DllImport("user32.dll")] static extern bool SetWindowPos(IntPtr hwnd, IntPtr after, int x, int y, int cx, int cy, uint flags);
 
        const string TitlePrefix = "AsciiFX ";
        const int MaxBrushes = 8; // nearby colors share a brush, so each row needs only a few text draws
 
        readonly IAnimation anim;
        readonly int cols, rows;
        readonly char[] chars;
        readonly int[] colors;
        readonly char[] line;
        readonly int[] group; // palette index -> brush
        readonly SolidBrush[] brushes;
        readonly SolidBrush outlineBrush;
        readonly bool outline;
        readonly Font font;
        readonly float cellW, cellH;
        readonly Stopwatch clock = Stopwatch.StartNew();
        int ticks;
 
        // layout "box": a cols x rows window placed by position/x/y.
        // layout "sky": the full screen width and the top heightPercent of the screen.
        public static void Run(IAnimation anim, string name, float fontSize, string layout, int cols, int rows,
                               int heightPercent, bool outline, string position, int x, int y)
        {
            SetProcessDPIAware();
 
            // Size the font for the screen's scaling and measure one monospace cell
            Font font;
            float cellW;
            using (Graphics g = Graphics.FromHwnd(IntPtr.Zero))
            {
                font = new Font("Consolas", fontSize * g.DpiX / 96f, GraphicsUnit.Pixel);
                g.TextRenderingHint = TextRenderingHint.SingleBitPerPixelGridFit;
                cellW = g.MeasureString(new string('M', 20), font, PointF.Empty, StringFormat.GenericTypographic).Width / 20;
            }
            float cellH = (float)(cellW * Cell.Aspect);
 
            if (layout == "sky")
            {
                Rectangle screen = Screen.PrimaryScreen.Bounds;
                cols = (int)((screen.Width - 4) / cellW);
                rows = (int)(screen.Height * heightPercent / 100.0 / cellH);
                position = "top-left"; x = 0; y = 0;
            }
            Application.Run(new DesktopPlayer(anim, name, font, cellW, cellH, cols, rows, outline, position, x, y));
        }
 
        DesktopPlayer(IAnimation anim, string name, Font font, float cellW, float cellH, int cols, int rows,
                      bool outline, string position, int x, int y)
        {
            this.anim = anim; this.font = font; this.cellW = cellW; this.cellH = cellH;
            this.cols = Math.Max(1, cols); this.rows = Math.Max(1, rows); this.outline = outline;
            chars = new char[this.cols * this.rows];
            colors = new int[this.cols * this.rows];
            line = new char[this.cols];
            anim.Resize(this.cols, this.rows);
 
            Text = TitlePrefix + name;
            FormBorderStyle = FormBorderStyle.None;
            ShowInTaskbar = false;
            StartPosition = FormStartPosition.Manual;
            BackColor = Color.FromArgb(1, 0, 1); // keyed out, so only the characters show
            TransparencyKey = BackColor;
            DoubleBuffered = true;
 
            group = GroupColors(anim.Palette, out brushes);
            outlineBrush = new SolidBrush(Color.FromArgb(17, 17, 27));
 
            ClientSize = new Size((int)(this.cols * cellW) + 4, (int)(this.rows * cellH) + 4);
            Location = PlaceOn(Screen.PrimaryScreen.WorkingArea, position, x, y);
 
            Timer timer = new Timer();
            timer.Interval = Math.Max(15, anim.FrameMs);
            timer.Tick += delegate
            {
                if (ticks++ % 15 == 0) KeepOnDesktop();
                Invalidate();
            };
            timer.Start();
        }
 
        // Up to MaxBrushes representative colors from the palette; every palette color uses the closest one
        static int[] GroupColors(int[] palette, out SolidBrush[] brushes)
        {
            int count = Math.Min(MaxBrushes, palette.Length);
            int[] reps = new int[count];
            for (int i = 0; i < count; i++) reps[i] = palette[count == 1 ? 0 : i * (palette.Length - 1) / (count - 1)];
 
            int[] group = new int[palette.Length];
            for (int p = 0; p < palette.Length; p++)
            {
                int best = 0, bestDist = int.MaxValue;
                for (int r = 0; r < count; r++)
                {
                    int dr = ((palette[p] >> 16) & 255) - ((reps[r] >> 16) & 255);
                    int dg = ((palette[p] >> 8) & 255) - ((reps[r] >> 8) & 255);
                    int db = (palette[p] & 255) - (reps[r] & 255);
                    int dist = dr * dr + dg * dg + db * db;
                    if (dist < bestDist) { bestDist = dist; best = r; }
                }
                group[p] = best;
            }
 
            brushes = new SolidBrush[count];
            for (int r = 0; r < count; r++)
                brushes[r] = new SolidBrush(Color.FromArgb((reps[r] >> 16) & 255, (reps[r] >> 8) & 255, reps[r] & 255));
            return group;
        }
 
        // A named spot ("center", "top-left", "bottom", ...), or exact pixels from the top-left when x/y are given
        Point PlaceOn(Rectangle area, string position, int x, int y)
        {
            const int margin = 40;
            string p = (position ?? "center").ToLowerInvariant();
            int left = area.Left + (area.Width - Width) / 2, top = area.Top + (area.Height - Height) / 2;
            if (p.Contains("left")) left = area.Left + margin;
            if (p.Contains("right")) left = area.Right - Width - margin;
            if (p.Contains("top")) top = area.Top + margin;
            if (p.Contains("bottom")) top = area.Bottom - Height - margin;
            if (x >= 0) left = area.Left + x;
            if (y >= 0) top = area.Top + y;
            return new Point(left, top);
        }
 
        // Tool window (no taskbar/Alt+Tab), layered + transparent (clicks pass through), never takes focus
        protected override CreateParams CreateParams
        {
            get
            {
                CreateParams cp = base.CreateParams;
                cp.ExStyle |= 0x80 | 0x20 | 0x80000 | 0x08000000;
                return cp;
            }
        }
 
        protected override bool ShowWithoutActivation { get { return true; } }
 
        protected override void OnPaint(PaintEventArgs e)
        {
            anim.Frame(clock.Elapsed.TotalSeconds, chars, colors);
            Graphics g = e.Graphics;
            g.TextRenderingHint = TextRenderingHint.SingleBitPerPixelGridFit; // no antialiasing, so no colored fringes
 
            for (int y = 0; y < rows; y++)
            {
                float top = 2 + y * cellH;
                int row = y * cols;
                for (int x = 0; x < cols; x++) line[x] = colors[row + x] < 0 ? ' ' : chars[row + x];
 
                // A dark copy offset by a pixel keeps the characters readable on bright wallpapers
                if (outline) g.DrawString(new string(line), font, outlineBrush, 3, top + 1, StringFormat.GenericTypographic);
 
                // Then each run of same-brush characters in one go
                int start = 0;
                while (start < cols)
                {
                    int b = colors[row + start] < 0 ? -1 : group[colors[row + start]], end = start + 1;
                    while (end < cols && (colors[row + end] < 0 ? -1 : group[colors[row + end]]) == b) end++;
                    if (b >= 0)
                        g.DrawString(new string(line, start, end - start), font, brushes[b],
                                     2 + start * cellW, top, StringFormat.GenericTypographic);
                    start = end;
                }
            }
        }
 
        // Stay just above the window that holds the desktop icons and wallpaper, below everything else.
        // Other AsciiFX windows share that layer, so only move when a normal window got in between.
        void KeepOnDesktop()
        {
            IntPtr host = FindWindowEx(IntPtr.Zero, IntPtr.Zero, "Progman", null);
            if (FindWindowEx(host, IntPtr.Zero, "SHELLDLL_DefView", null) == IntPtr.Zero)
            {
                IntPtr worker = IntPtr.Zero;
                while ((worker = FindWindowEx(IntPtr.Zero, worker, "WorkerW", null)) != IntPtr.Zero)
                    if (FindWindowEx(worker, IntPtr.Zero, "SHELLDLL_DefView", null) != IntPtr.Zero) { host = worker; break; }
            }
            StringBuilder text = new StringBuilder(64);
            for (IntPtr w = GetWindow(host, 3); w != IntPtr.Zero; w = GetWindow(w, 3)) // GW_HWNDPREV: walk upward
            {
                if (w == Handle) return;
                text.Length = 0;
                GetWindowText(w, text, 64);
                if (IsWindowVisible(w) && !text.ToString().StartsWith(TitlePrefix)) break;
            }
            IntPtr above = GetWindow(host, 3);
            if (above == IntPtr.Zero) return;
            SetWindowPos(Handle, above, 0, 0, 0, 0, 0x0001 | 0x0002 | 0x0010); // NOSIZE | NOMOVE | NOACTIVATE
        }
    }
}