using System.Runtime.InteropServices; namespace DragonISO.App.Services; /// /// Phase E.4 — Embedded Teams via SetParent. /// /// Reparents Teams' main top-level window into a Dragon-ISO-owned host /// (typically a Border element's HWND). Strips the captured window's /// caption + thick frame so it integrates flush with the host, and /// remembers enough about the original to restore it cleanly later. /// /// The Win32 behavior is well understood for classic Win32 apps, but /// modern Teams runs WebView2 in its main window; WebView2's renderer is /// sensitive to parent changes and may flash white frames during /// reparent, drop input focus, or refuse to redraw until forced. We mark /// the feature experimental and ensure the restore path always runs (the /// caller wraps Embed in a finally block) so operators can fall back to /// auto-hide mode if embedding misbehaves on their specific Teams build. /// /// Lives in its own static class — separated from /// because the embedding lifecycle (reparent → resize → restore) is its /// own thing, and the Win32 surface it requires (SetParent / window-style /// muck / SetWindowPos / MoveWindow) isn't reused by the launch / hide / /// in-call control paths. /// public static class TeamsEmbedHost { [DllImport("user32.dll", SetLastError = true)] private static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent); [DllImport("user32.dll", SetLastError = true)] private static extern int GetWindowLongPtr(IntPtr hWnd, int nIndex); [DllImport("user32.dll", SetLastError = true)] private static extern int SetWindowLongPtr(IntPtr hWnd, int nIndex, int dwNewLong); [DllImport("user32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool MoveWindow(IntPtr hWnd, int X, int Y, int nWidth, int nHeight, [MarshalAs(UnmanagedType.Bool)] bool bRepaint); [DllImport("user32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)] private static extern int GetWindowTextLengthW(IntPtr hWnd); private const int GWL_STYLE = -16; private const long WS_CHILD = 0x40000000; private const long WS_POPUP = unchecked((long)0x80000000); private const long WS_CAPTION = 0x00C00000; private const long WS_THICKFRAME = 0x00040000; private const long WS_BORDER = 0x00800000; private const long WS_DLGFRAME = 0x00400000; private const uint SWP_FRAMECHANGED = 0x0020; private const uint SWP_NOMOVE = 0x0002; private const uint SWP_NOSIZE = 0x0001; private const uint SWP_NOZORDER = 0x0004; private const uint SWP_NOACTIVATE = 0x0010; /// /// Captures the original parent + window style so embedding can be /// reversed cleanly. Tracked per-HWND so multiple consecutive /// embed / unembed cycles don't lose the original chrome. /// private static (IntPtr OriginalParent, int OriginalStyle)? _embedSavedState; private static IntPtr _embeddedHwnd = IntPtr.Zero; /// True when a Teams window is currently parented inside a Dragon-ISO host. public static bool IsEmbedded => _embeddedHwnd != IntPtr.Zero; /// /// Reparents Teams' most-recently-used top-level window into /// . Strips Teams' caption + thick frame /// so it integrates flush with the host. Returns true on success, /// false if no Teams window could be found. /// /// The host HWND is typically obtained via: /// var src = (System.Windows.Interop.HwndSource) /// PresentationSource.FromVisual(MyHostBorder); /// src.Handle // → IntPtr suitable for hostHwnd /// public static bool EmbedTeamsInto(IntPtr hostHwnd, int width, int height) { if (hostHwnd == IntPtr.Zero) return false; var teamsWindows = TeamsLauncher.EnumerateTopLevelTeamsWindows(); if (teamsWindows.Count == 0) return false; // Pick the longest-title window as the "main" one — same // heuristic GetActiveWindowTitle uses; matches the call / // meeting window. IntPtr best = IntPtr.Zero; int bestLen = -1; foreach (var w in teamsWindows) { var len = GetWindowTextLengthW(w); if (len > bestLen) { bestLen = len; best = w; } } if (best == IntPtr.Zero) return false; // Already embedded? Unembed first to clean state. if (_embeddedHwnd != IntPtr.Zero) RestoreEmbed(); // Save original style + parent so we can fully reverse later. var originalStyle = GetWindowLongPtr(best, GWL_STYLE); var originalParent = SetParent(best, hostHwnd); // returns old parent _embedSavedState = (originalParent, originalStyle); _embeddedHwnd = best; // Strip top-level decorations + add WS_CHILD so the OS treats // it as a child window of the host. var newStyle = originalStyle; unchecked { newStyle &= ~(int)WS_CAPTION; newStyle &= ~(int)WS_THICKFRAME; newStyle &= ~(int)WS_BORDER; newStyle &= ~(int)WS_DLGFRAME; newStyle &= ~(int)WS_POPUP; newStyle |= (int)WS_CHILD; } SetWindowLongPtr(best, GWL_STYLE, newStyle); // Force a non-client recalculation so the style change takes // effect. SetWindowPos(best, IntPtr.Zero, 0, 0, 0, 0, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); // Place at top-left of host, full host size. MoveWindow(best, 0, 0, width, height, true); return true; } /// /// Resize the currently-embedded Teams window to /// × . Called when the host element resizes /// (window resize, layout change, etc.). No-op if nothing is embedded. /// public static void ResizeEmbedded(int width, int height) { if (_embeddedHwnd == IntPtr.Zero || width <= 0 || height <= 0) return; MoveWindow(_embeddedHwnd, 0, 0, width, height, true); } /// /// Reverse an active embed: SetParent back to desktop + restore the /// original window style so Teams looks/behaves like a normal /// top-level window again. Safe to call when nothing is embedded — /// no-op. /// public static void RestoreEmbed() { if (_embeddedHwnd == IntPtr.Zero || _embedSavedState is null) return; var (origParent, origStyle) = _embedSavedState.Value; try { // Restore original style FIRST so when we reparent the // window's top-level decorations come back correctly. SetWindowLongPtr(_embeddedHwnd, GWL_STYLE, origStyle); // SetParent(hwnd, Zero) returns to desktop. We could pass // origParent verbatim but for Teams that's always the // desktop anyway, and IntPtr.Zero is documented as // "reparent to desktop". SetParent(_embeddedHwnd, IntPtr.Zero); SetWindowPos(_embeddedHwnd, IntPtr.Zero, 0, 0, 0, 0, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); } catch { /* defensive — restore must never throw */ } finally { _embedSavedState = null; _embeddedHwnd = IntPtr.Zero; } } }