Files
LlamaCasty/Services/Win32WindowEnumerator.cs
gramps 4e0915a36e feat(backdrop): Capture Window… in-app window pin (TASK 38, Good Dog ONE test)
Click-to-pin an open window as the full-bleed Live backdrop, via the existing
window:<hwnd> capture path (same render layer as game/desktop). Session-scoped:
window:/picker: keys heal to auto on reload (HWND-recycle hazard); pin wins
while the window enumerates, then auto-fallbacks (game → desktop → static); a
dead window's capture session heals to auto, no dead ends.

Fix: ScreenCaptureSourceFactory.Resolve hex-parsed "window:0x<Hwnd>" WITHOUT
stripping the 0x prefix — NumberStyles.HexNumber rejects it, so every pin
resolved null ("No capture target for this key") and the heal silently rolled
back. Same flaw in IsAliveWindowPin (pin-wins guard was dead). Both fixed.

Commit also carries an out-of-scope prerequisite: PillRadioTests.cs:105 had a
committed stray token ("...PrimaryStartButtonLabel soil;", CS1003) blocking the
whole test-project compile — token removed.

Plus the previous session's uncommitted pricing docs (one-time $29/$49) that
share TASKS.md/ai.md/HANDOFF.md.
2026-09-22 07:00:47 -07:00

122 lines
4.1 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using ytLive.Models;
namespace ytLive.Services;
/// <summary>
/// Win32 implementation of <see cref="IWindowEnumerator"/>. Enumerates visible
/// top-level windows for the "Capture Window…" submenu, mirroring the style of
/// <see cref="Win32FullScreenDetector"/>: visible, non-cloaked, zero-rect
/// excluded, and windows belonging to this process excluded (the app must never
/// offer itself for pinning). Ordered by process name, then title.
/// </summary>
public sealed class Win32WindowEnumerator : IWindowEnumerator
{
private static readonly IntPtr OwnProcessId = new(Environment.ProcessId);
public IReadOnlyList<WindowInfo> Enumerate()
{
var results = new List<WindowInfo>();
EnumWindows((hwnd, _) =>
{
if (TryDescribe(hwnd, out var info))
results.Add(info);
return true; // keep enumerating
}, IntPtr.Zero);
results.Sort(static (a, b) =>
{
var byProcess = string.Compare(a.ProcessName, b.ProcessName, StringComparison.OrdinalIgnoreCase);
return byProcess != 0 ? byProcess
: string.Compare(a.Title, b.Title, StringComparison.OrdinalIgnoreCase);
});
return results;
}
private static bool TryDescribe(IntPtr hwnd, out WindowInfo info)
{
info = null!;
if (GetWindowThreadProcessId(hwnd, out var pid) == 0) return false;
if (pid == 0 || new IntPtr((long)pid) == OwnProcessId) return false;
if (!IsWindowVisible(hwnd)) return false;
if (IsCloaked(hwnd)) return false;
if (!TryGetRect(hwnd, out var rect) || rect.Right <= rect.Left || rect.Bottom <= rect.Top) return false;
var title = GetTitle(hwnd);
if (string.IsNullOrWhiteSpace(title)) return false;
var processName = ProcessName((int)pid);
if (string.IsNullOrWhiteSpace(processName)) return false;
info = new WindowInfo(title, processName, hwnd.ToInt64(), (int)pid);
return true;
}
private static string ProcessName(int pid)
{
try
{
using var process = Process.GetProcessById(pid);
return process.ProcessName;
}
catch { return string.Empty; }
}
private static string GetTitle(IntPtr hwnd)
{
var length = GetWindowTextLength(hwnd);
if (length <= 0) return string.Empty;
var sb = new StringBuilder(length + 1);
GetWindowText(hwnd, sb, sb.Capacity);
return sb.ToString();
}
private static bool IsCloaked(IntPtr hwnd)
{
const int DWMWA_CLOAKED = 14;
var cloaked = 0;
return DwmGetWindowAttribute(hwnd, DWMWA_CLOAKED, out cloaked, sizeof(int)) == 0 && cloaked != 0;
}
private static bool TryGetRect(IntPtr hwnd, out Rect rect)
{
rect = default;
return GetWindowRect(hwnd, out rect);
}
[StructLayout(LayoutKind.Sequential)]
private struct Rect
{
public int Left;
public int Top;
public int Right;
public int Bottom;
}
private delegate bool EnumWindowsProc(IntPtr hwnd, IntPtr lParam);
[DllImport("user32.dll")]
private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
[DllImport("user32.dll")]
private static extern bool IsWindowVisible(IntPtr hWnd);
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern int GetWindowTextLength(IntPtr hWnd);
[DllImport("user32.dll")]
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
[DllImport("user32.dll")]
private static extern bool GetWindowRect(IntPtr hWnd, out Rect lpRect);
[DllImport("dwmapi.dll")]
private static extern int DwmGetWindowAttribute(IntPtr hwnd, int dwAttribute, out int pvAttribute, int cbAttribute);
}