chore(bridge): 删除桥接包的提交记录和Git属性配置 删除了 .codely-bridge-commit 文件中存储的提交哈希值 移除了 .gitattributes 文件中的行结束符配置规则 ```
582 lines
25 KiB
C#
582 lines
25 KiB
C#
#if UNITY_EDITOR_WIN
|
||
using System;
|
||
using System.Reflection;
|
||
using System.Runtime.InteropServices;
|
||
using System.Text;
|
||
using UnityEditor;
|
||
using UnityEngine;
|
||
|
||
using UnityTcp.Editor.Helpers;
|
||
namespace Cn.Tuanjie.Codely.Editor
|
||
{
|
||
/// <summary>
|
||
/// Locate the Win32 HWND that backs a Unity EditorWindow.
|
||
///
|
||
/// EditorWindow.position is in DPI-scaled "points" while Win32 reports
|
||
/// physical pixels, and Unity's point↔pixel mapping for monitors other
|
||
/// than the primary is not a single linear factor. A purely absolute
|
||
/// rect comparison therefore only works reliably on the primary monitor —
|
||
/// on secondary monitors it used to never match and the embedded panel
|
||
/// hung on "Loading" forever. The lookup below layers several strategies,
|
||
/// most reliable first:
|
||
///
|
||
/// 1. Reflection: the host GUIView's internal nativeHandle. If it is our
|
||
/// process's UnityGUIViewWndClass window, done — coordinate-free.
|
||
/// 2. If reflection yields a valid window of ours with a different class
|
||
/// (e.g. the ContainerWindow), restrict the search to that top-level
|
||
/// window; a sole GUIView child there is accepted outright (typical
|
||
/// for floating panels).
|
||
/// 3. Rect search over UnityGUIViewWndClass children, testing containment
|
||
/// under several coordinate hypotheses per candidate:
|
||
/// (a) legacy: window rect × EditorGUIUtility.pixelsPerPoint versus
|
||
/// physical pixels — exact on the primary monitor.
|
||
/// (b) pure scale: candidate physical rect ÷ its own GetDpiForWindow
|
||
/// factor versus the window rect in points.
|
||
/// (d) monitor-anchored: like (b) but preserving the monitor's
|
||
/// physical origin (points = monOrigin + (phys − monOrigin)/scale).
|
||
/// (c) container-relative: compare the candidate's offset within its
|
||
/// top-level window against the view's offset within its
|
||
/// ContainerWindow (both in points). Relative offsets never cross
|
||
/// monitors, so this holds under ANY per-monitor mapping.
|
||
/// Smallest matching candidate wins (the dock host's GUIView fully
|
||
/// contains its hosted EditorWindow's content rect; slack absorbs the
|
||
/// tab strip / scrollbar offset).
|
||
/// 4. When everything fails, a throttled diagnostic dump of every
|
||
/// candidate is logged so field reports pin down the actual mapping.
|
||
///
|
||
/// IMPORTANT: the EnumWindowsProc delegates are stored in static fields so
|
||
/// they're allocated once. Repeatedly creating fresh delegate instances on
|
||
/// every editor tick crashes Mono's delegate_hash_table when the marshal
|
||
/// cache fills up (mono-2.0-bdwgc:marshal.c delegate_hash_table_add assert).
|
||
/// </summary>
|
||
public static class NativeWindowHelper
|
||
{
|
||
// ── Win32 ──────────────────────────────────────────────────────────
|
||
|
||
[DllImport("user32.dll")]
|
||
static extern bool EnumWindows(EnumWindowsProc cb, IntPtr param);
|
||
|
||
[DllImport("user32.dll")]
|
||
static extern bool EnumChildWindows(IntPtr parent, EnumWindowsProc cb, IntPtr param);
|
||
|
||
[DllImport("user32.dll", CharSet = CharSet.Unicode, EntryPoint = "GetClassNameW")]
|
||
static extern int GetClassNameW(IntPtr hwnd, [Out] char[] buf, int max);
|
||
|
||
[DllImport("user32.dll")]
|
||
static extern uint GetWindowThreadProcessId(IntPtr hwnd, out uint processId);
|
||
|
||
[DllImport("user32.dll")]
|
||
static extern bool GetWindowRect(IntPtr hwnd, out RECT rect);
|
||
|
||
[DllImport("user32.dll")]
|
||
static extern bool GetClientRect(IntPtr hwnd, out RECT rect);
|
||
|
||
[DllImport("user32.dll")]
|
||
static extern bool ClientToScreen(IntPtr hwnd, ref POINT pt);
|
||
|
||
[DllImport("user32.dll")]
|
||
static extern bool IsWindow(IntPtr hwnd);
|
||
|
||
[DllImport("user32.dll")]
|
||
static extern IntPtr GetAncestor(IntPtr hwnd, uint flags);
|
||
|
||
[DllImport("user32.dll")]
|
||
static extern IntPtr MonitorFromWindow(IntPtr hwnd, uint flags);
|
||
|
||
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
||
static extern bool GetMonitorInfoW(IntPtr hMonitor, ref MONITORINFO mi);
|
||
|
||
// Windows 10 1607+; call sites guard with s_dpiApiAvailable.
|
||
[DllImport("user32.dll")]
|
||
static extern uint GetDpiForWindow(IntPtr hwnd);
|
||
|
||
const uint GA_ROOT = 2;
|
||
const uint MONITOR_DEFAULTTONEAREST = 2;
|
||
|
||
[StructLayout(LayoutKind.Sequential)]
|
||
struct RECT
|
||
{
|
||
public int Left, Top, Right, Bottom;
|
||
public int Width => Right - Left;
|
||
public int Height => Bottom - Top;
|
||
}
|
||
|
||
[StructLayout(LayoutKind.Sequential)]
|
||
struct POINT
|
||
{
|
||
public int X, Y;
|
||
}
|
||
|
||
[StructLayout(LayoutKind.Sequential)]
|
||
struct MONITORINFO
|
||
{
|
||
public int cbSize;
|
||
public RECT rcMonitor;
|
||
public RECT rcWork;
|
||
public uint dwFlags;
|
||
}
|
||
|
||
delegate bool EnumWindowsProc(IntPtr hwnd, IntPtr param);
|
||
|
||
const string kGUIViewClass = "UnityGUIViewWndClass";
|
||
|
||
// tab strip / scrollbar / window chrome can make the host GUIView several
|
||
// dozen pixels larger than the EditorWindow's inner content rect — give
|
||
// enough slack on each side to absorb that. Used as physical pixels in
|
||
// hypothesis (a) and as points elsewhere; both stay well below the size
|
||
// of a real dock area.
|
||
const int Slack = 64;
|
||
|
||
static readonly int s_currentPid = System.Diagnostics.Process.GetCurrentProcess().Id;
|
||
|
||
// Static delegate instances. Created once at type init, kept alive forever,
|
||
// so EnumWindows / EnumChildWindows always see the same managed callback.
|
||
static readonly EnumWindowsProc s_outerProc = OuterEnumProc;
|
||
static readonly EnumWindowsProc s_innerProc = InnerEnumProc;
|
||
|
||
// Per-call state. Safe to use static state because GetHWND is only invoked
|
||
// from the editor main thread and EnumWindows is synchronous.
|
||
static readonly char[] s_classBuf = new char[256];
|
||
static int s_targetLeft, s_targetTop, s_targetRight, s_targetBottom; // physical px, hypothesis (a)
|
||
static float s_targetPtLeft, s_targetPtTop, s_targetPtRight, s_targetPtBottom; // points, hypotheses (b)/(d)
|
||
static bool s_haveContainer; // hypothesis (c) inputs resolved
|
||
static bool s_relIsExact; // rel rect comes from the GUIView itself → near-equality match
|
||
static float s_containerW, s_containerH; // container size, points
|
||
static float s_relLeft, s_relTop, s_relRight, s_relBottom; // rect relative to container, points
|
||
static IntPtr s_best;
|
||
static long s_bestArea;
|
||
static int s_candidateCount;
|
||
static IntPtr s_firstCandidate;
|
||
|
||
// Flipped to false on pre-1607 Windows where GetDpiForWindow is missing;
|
||
// per-monitor hypotheses are then skipped and behavior matches the legacy search.
|
||
static bool s_dpiApiAvailable = true;
|
||
|
||
// Diagnostics: dump all candidates on the first failure and every 32nd
|
||
// one after that, so a stuck editor produces a conclusive log without
|
||
// flooding the console (retries run about twice per second).
|
||
static int s_failCount;
|
||
static StringBuilder s_diag;
|
||
static IntPtr s_diagReflected;
|
||
static string s_diagReflectedClass = "";
|
||
|
||
// Cached reflection lookups for hypothesis (c). Resolved once; any miss
|
||
// simply disables that hypothesis.
|
||
static bool s_reflectionResolved;
|
||
static FieldInfo s_fiParent; // EditorWindow.m_Parent
|
||
static PropertyInfo s_piWindow; // View.window → ContainerWindow
|
||
static PropertyInfo s_piScreenPosition; // View.screenPosition (points)
|
||
static PropertyInfo s_piContainerPosition; // ContainerWindow.position (points)
|
||
|
||
// ── Public API ─────────────────────────────────────────────────────
|
||
|
||
public static IntPtr GetHWND(EditorWindow win)
|
||
{
|
||
if (win == null) return IntPtr.Zero;
|
||
|
||
try
|
||
{
|
||
// Strategy 1/2: reflected native handle, exact or as search scope.
|
||
IntPtr searchRoot;
|
||
IntPtr reflected = TryReflectedHandle(win, out searchRoot);
|
||
if (reflected != IntPtr.Zero)
|
||
{
|
||
s_failCount = 0;
|
||
return reflected;
|
||
}
|
||
|
||
Rect pos = win.position;
|
||
if (pos.width < 1f || pos.height < 1f) return IntPtr.Zero;
|
||
|
||
PrepareTargets(win, pos);
|
||
|
||
bool collectDiag = s_failCount == 0 || (s_failCount % 32) == 0;
|
||
s_diag = collectDiag ? new StringBuilder() : null;
|
||
|
||
IntPtr found = IntPtr.Zero;
|
||
if (searchRoot != IntPtr.Zero)
|
||
{
|
||
if (s_diag != null) s_diag.AppendLine($"-- scoped pass under root=0x{searchRoot.ToInt64():X} --");
|
||
found = SearchScoped(searchRoot);
|
||
}
|
||
if (found == IntPtr.Zero)
|
||
{
|
||
if (s_diag != null) s_diag.AppendLine("-- global pass --");
|
||
found = SearchGlobal();
|
||
}
|
||
|
||
if (found != IntPtr.Zero)
|
||
{
|
||
s_failCount = 0;
|
||
s_diag = null;
|
||
return found;
|
||
}
|
||
|
||
s_failCount++;
|
||
if (s_diag != null)
|
||
{
|
||
LogDiagnostics(win, pos, searchRoot);
|
||
s_diag = null;
|
||
}
|
||
return IntPtr.Zero;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
CodelyLogger.LogWarning($"[NativeWindowHelper] GetHWND exception: {ex.Message}");
|
||
return IntPtr.Zero;
|
||
}
|
||
}
|
||
|
||
// ── Strategy 1/2: reflection ───────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// Read GUIView.nativeHandle through the shared reflection helper. If it
|
||
/// is our process's UnityGUIViewWndClass window, return it. If it is a
|
||
/// valid window of ours with a different class (some editor builds hand
|
||
/// out the container window instead), expose its top-level ancestor via
|
||
/// <paramref name="searchRoot"/> so the rect search can be scoped to the
|
||
/// right container. Anything else (zero, foreign, not a window at all —
|
||
/// e.g. a native object pointer) is ignored.
|
||
/// </summary>
|
||
static IntPtr TryReflectedHandle(EditorWindow win, out IntPtr searchRoot)
|
||
{
|
||
searchRoot = IntPtr.Zero;
|
||
s_diagReflected = IntPtr.Zero;
|
||
s_diagReflectedClass = "";
|
||
try
|
||
{
|
||
IntPtr handle = EditorWindowNativeHandleHelper.GetGUIViewHandle(win);
|
||
s_diagReflected = handle;
|
||
if (handle == IntPtr.Zero || !IsWindow(handle)) return IntPtr.Zero;
|
||
|
||
GetWindowThreadProcessId(handle, out uint pid);
|
||
if ((int)pid != s_currentPid) return IntPtr.Zero;
|
||
|
||
string cls = ClassNameOf(handle);
|
||
s_diagReflectedClass = cls;
|
||
if (cls == kGUIViewClass) return handle;
|
||
|
||
searchRoot = GetAncestor(handle, GA_ROOT);
|
||
return IntPtr.Zero;
|
||
}
|
||
catch
|
||
{
|
||
// Reflection target changed shape or the native view is mid-teardown.
|
||
return IntPtr.Zero;
|
||
}
|
||
}
|
||
|
||
// ── Strategy 3: rect search ────────────────────────────────────────
|
||
|
||
static IntPtr SearchScoped(IntPtr root)
|
||
{
|
||
ResetSearchState();
|
||
EnumChildWindows(root, s_innerProc, IntPtr.Zero);
|
||
if (s_best != IntPtr.Zero) return s_best;
|
||
|
||
// The scope IS the window's own container; if it hosts exactly one
|
||
// GUIView there is nothing to disambiguate — take it even when no
|
||
// coordinate hypothesis matched (typical floating panel).
|
||
if (s_candidateCount == 1) return s_firstCandidate;
|
||
return IntPtr.Zero;
|
||
}
|
||
|
||
static IntPtr SearchGlobal()
|
||
{
|
||
ResetSearchState();
|
||
EnumWindows(s_outerProc, IntPtr.Zero);
|
||
return s_best;
|
||
}
|
||
|
||
static void ResetSearchState()
|
||
{
|
||
s_best = IntPtr.Zero;
|
||
s_bestArea = long.MaxValue;
|
||
s_candidateCount = 0;
|
||
s_firstCandidate = IntPtr.Zero;
|
||
}
|
||
|
||
/// <summary>Fill the per-call matching targets for all hypotheses.</summary>
|
||
static void PrepareTargets(EditorWindow win, Rect pos)
|
||
{
|
||
Rect target = ScaleToPixels(pos);
|
||
s_targetLeft = (int)target.x;
|
||
s_targetTop = (int)target.y;
|
||
s_targetRight = (int)(target.x + target.width);
|
||
s_targetBottom = (int)(target.y + target.height);
|
||
|
||
s_targetPtLeft = pos.x;
|
||
s_targetPtTop = pos.y;
|
||
s_targetPtRight = pos.x + pos.width;
|
||
s_targetPtBottom = pos.y + pos.height;
|
||
|
||
// Hypothesis (c): the window's rect relative to its ContainerWindow,
|
||
// in points. Prefer the host GUIView's own screenPosition — it should
|
||
// equal the candidate HWND's rect, allowing a near-equality match.
|
||
s_haveContainer = false;
|
||
s_relIsExact = false;
|
||
try
|
||
{
|
||
ResolveReflection();
|
||
object parent = s_fiParent?.GetValue(win);
|
||
if (parent == null) return;
|
||
|
||
object container = s_piWindow?.GetValue(parent, null);
|
||
if (container == null || s_piContainerPosition == null) return;
|
||
|
||
Rect containerPos = (Rect)s_piContainerPosition.GetValue(container, null);
|
||
if (containerPos.width < 1f || containerPos.height < 1f) return;
|
||
|
||
Rect basis = pos;
|
||
if (s_piScreenPosition != null)
|
||
{
|
||
Rect sp = (Rect)s_piScreenPosition.GetValue(parent, null);
|
||
if (sp.width >= 1f && sp.height >= 1f)
|
||
{
|
||
basis = sp;
|
||
s_relIsExact = true;
|
||
}
|
||
}
|
||
|
||
s_containerW = containerPos.width;
|
||
s_containerH = containerPos.height;
|
||
s_relLeft = basis.x - containerPos.x;
|
||
s_relTop = basis.y - containerPos.y;
|
||
s_relRight = s_relLeft + basis.width;
|
||
s_relBottom = s_relTop + basis.height;
|
||
s_haveContainer = true;
|
||
}
|
||
catch
|
||
{
|
||
s_haveContainer = false;
|
||
}
|
||
}
|
||
|
||
static void ResolveReflection()
|
||
{
|
||
if (s_reflectionResolved) return;
|
||
s_reflectionResolved = true;
|
||
try
|
||
{
|
||
const BindingFlags flags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public;
|
||
s_fiParent = typeof(EditorWindow).GetField("m_Parent", flags);
|
||
|
||
Type viewType = s_fiParent?.FieldType; // HostView (derives from GUIView/View)
|
||
s_piWindow = viewType?.GetProperty("window", flags);
|
||
s_piScreenPosition = viewType?.GetProperty("screenPosition", flags);
|
||
|
||
Type containerType = s_piWindow?.PropertyType; // ContainerWindow
|
||
s_piContainerPosition = containerType?.GetProperty("position", flags);
|
||
}
|
||
catch
|
||
{
|
||
// Editor internals changed shape; hypothesis (c) stays disabled.
|
||
}
|
||
}
|
||
|
||
// ── Internals ──────────────────────────────────────────────────────
|
||
|
||
// EditorWindow.position is in DPI-scaled "points"; Win32 GetWindowRect
|
||
// returns physical pixels. Convert before comparing. Note this uses the
|
||
// CALLER's pixelsPerPoint, which only matches the window's monitor when
|
||
// both share a DPI scale — the other hypotheses cover the rest.
|
||
static Rect ScaleToPixels(Rect r)
|
||
{
|
||
float dpi;
|
||
try { dpi = EditorGUIUtility.pixelsPerPoint; }
|
||
catch { dpi = 1f; }
|
||
if (dpi <= 0f) dpi = 1f;
|
||
return new Rect(r.x * dpi, r.y * dpi, r.width * dpi, r.height * dpi);
|
||
}
|
||
|
||
static string ClassNameOf(IntPtr hwnd)
|
||
{
|
||
int clen = GetClassNameW(hwnd, s_classBuf, s_classBuf.Length);
|
||
return clen > 0 ? new string(s_classBuf, 0, clen) : "";
|
||
}
|
||
|
||
[AOT.MonoPInvokeCallback(typeof(EnumWindowsProc))]
|
||
static bool OuterEnumProc(IntPtr topHwnd, IntPtr param)
|
||
{
|
||
try
|
||
{
|
||
GetWindowThreadProcessId(topHwnd, out uint pid);
|
||
if ((int)pid != s_currentPid) return true;
|
||
EnumChildWindows(topHwnd, s_innerProc, IntPtr.Zero);
|
||
}
|
||
catch { /* ignore */ }
|
||
return true;
|
||
}
|
||
|
||
[AOT.MonoPInvokeCallback(typeof(EnumWindowsProc))]
|
||
static bool InnerEnumProc(IntPtr childHwnd, IntPtr param)
|
||
{
|
||
try
|
||
{
|
||
if (ClassNameOf(childHwnd) != kGUIViewClass) return true;
|
||
|
||
if (!GetWindowRect(childHwnd, out RECT wr)) return true;
|
||
|
||
s_candidateCount++;
|
||
if (s_candidateCount == 1) s_firstCandidate = childHwnd;
|
||
|
||
float scale = s_dpiApiAvailable ? GetWindowScale(childHwnd) : 0f;
|
||
|
||
// (a) physical-pixel comparison with the caller-context scale
|
||
bool a =
|
||
wr.Left - Slack <= s_targetLeft &&
|
||
wr.Top - Slack <= s_targetTop &&
|
||
wr.Right + Slack >= s_targetRight &&
|
||
wr.Bottom + Slack >= s_targetBottom;
|
||
|
||
// (b) point-space comparison with the candidate's own monitor scale
|
||
bool b = false;
|
||
if (!a && scale > 0f)
|
||
{
|
||
b =
|
||
wr.Left / scale - Slack <= s_targetPtLeft &&
|
||
wr.Top / scale - Slack <= s_targetPtTop &&
|
||
wr.Right / scale + Slack >= s_targetPtRight &&
|
||
wr.Bottom / scale + Slack >= s_targetPtBottom;
|
||
}
|
||
|
||
// (d) like (b) but anchored at the candidate's monitor origin
|
||
bool d = false;
|
||
if (!a && !b && scale > 0f)
|
||
{
|
||
var mi = new MONITORINFO { cbSize = Marshal.SizeOf(typeof(MONITORINFO)) };
|
||
IntPtr hmon = MonitorFromWindow(childHwnd, MONITOR_DEFAULTTONEAREST);
|
||
if (hmon != IntPtr.Zero && GetMonitorInfoW(hmon, ref mi))
|
||
{
|
||
float mx = mi.rcMonitor.Left, my = mi.rcMonitor.Top;
|
||
d =
|
||
mx + (wr.Left - mx) / scale - Slack <= s_targetPtLeft &&
|
||
my + (wr.Top - my) / scale - Slack <= s_targetPtTop &&
|
||
mx + (wr.Right - mx) / scale + Slack >= s_targetPtRight &&
|
||
my + (wr.Bottom - my) / scale + Slack >= s_targetPtBottom;
|
||
}
|
||
}
|
||
|
||
// (c) container-relative comparison — mapping-independent
|
||
bool c = false;
|
||
if (!a && !b && !d && scale > 0f && s_haveContainer)
|
||
{
|
||
c = MatchesContainerRelative(childHwnd, wr, scale);
|
||
}
|
||
|
||
if (s_diag != null)
|
||
{
|
||
// Extra geometry so a single failed dump reconstructs the mapping.
|
||
string monStr = "?", rootStr = "?";
|
||
var dmi = new MONITORINFO { cbSize = Marshal.SizeOf(typeof(MONITORINFO)) };
|
||
IntPtr dmon = MonitorFromWindow(childHwnd, MONITOR_DEFAULTTONEAREST);
|
||
if (dmon != IntPtr.Zero && GetMonitorInfoW(dmon, ref dmi))
|
||
monStr = $"({dmi.rcMonitor.Left},{dmi.rcMonitor.Top})";
|
||
IntPtr droot = GetAncestor(childHwnd, GA_ROOT);
|
||
if (droot != IntPtr.Zero && GetClientRect(droot, out RECT drc))
|
||
{
|
||
var dorigin = new POINT();
|
||
if (ClientToScreen(droot, ref dorigin))
|
||
rootStr = $"0x{droot.ToInt64():X}@({dorigin.X},{dorigin.Y}) client={drc.Right}x{drc.Bottom}";
|
||
}
|
||
s_diag.AppendLine(
|
||
$" cand=0x{childHwnd.ToInt64():X} phys=({wr.Left},{wr.Top})-({wr.Right},{wr.Bottom}) " +
|
||
$"scale={scale:F2} mon={monStr} root={rootStr} a={a} b={b} d={d} c={c}");
|
||
}
|
||
|
||
if (!(a || b || d || c)) return true;
|
||
|
||
long area = (long)wr.Width * wr.Height;
|
||
if (area < s_bestArea)
|
||
{
|
||
s_bestArea = area;
|
||
s_best = childHwnd;
|
||
}
|
||
}
|
||
catch { /* window may have closed mid-enumeration */ }
|
||
return true;
|
||
}
|
||
|
||
static bool MatchesContainerRelative(IntPtr childHwnd, RECT wr, float scale)
|
||
{
|
||
IntPtr root = GetAncestor(childHwnd, GA_ROOT);
|
||
if (root == IntPtr.Zero) return false;
|
||
if (!GetClientRect(root, out RECT rc)) return false;
|
||
var origin = new POINT();
|
||
if (!ClientToScreen(root, ref origin)) return false;
|
||
|
||
// The right top-level window has (about) the ContainerWindow's size.
|
||
// Unity's ContainerWindow.position may describe the client area or the
|
||
// full frame depending on chrome — accept either within slack.
|
||
float clientW = rc.Right / scale, clientH = rc.Bottom / scale;
|
||
bool sizeOk =
|
||
Math.Abs(clientW - s_containerW) <= Slack &&
|
||
Math.Abs(clientH - s_containerH) <= Slack;
|
||
if (!sizeOk && GetWindowRect(root, out RECT rwr))
|
||
{
|
||
sizeOk =
|
||
Math.Abs(rwr.Width / scale - s_containerW) <= Slack &&
|
||
Math.Abs(rwr.Height / scale - s_containerH) <= Slack;
|
||
}
|
||
if (!sizeOk) return false;
|
||
|
||
float relLeft = (wr.Left - origin.X) / scale;
|
||
float relTop = (wr.Top - origin.Y) / scale;
|
||
float relRight = (wr.Right - origin.X) / scale;
|
||
float relBottom = (wr.Bottom - origin.Y) / scale;
|
||
|
||
if (s_relIsExact)
|
||
{
|
||
// Comparing the GUIView's own rect — edges should line up.
|
||
return
|
||
Math.Abs(relLeft - s_relLeft) <= Slack &&
|
||
Math.Abs(relTop - s_relTop) <= Slack &&
|
||
Math.Abs(relRight - s_relRight) <= Slack &&
|
||
Math.Abs(relBottom - s_relBottom) <= Slack;
|
||
}
|
||
|
||
// Comparing the hosted EditorWindow content rect — the GUIView contains it.
|
||
return
|
||
relLeft - Slack <= s_relLeft &&
|
||
relTop - Slack <= s_relTop &&
|
||
relRight + Slack >= s_relRight &&
|
||
relBottom + Slack >= s_relBottom;
|
||
}
|
||
|
||
static float GetWindowScale(IntPtr hwnd)
|
||
{
|
||
try
|
||
{
|
||
uint dpi = GetDpiForWindow(hwnd);
|
||
return dpi > 0 ? dpi / 96f : 0f;
|
||
}
|
||
catch (EntryPointNotFoundException)
|
||
{
|
||
s_dpiApiAvailable = false;
|
||
return 0f;
|
||
}
|
||
}
|
||
|
||
static void LogDiagnostics(EditorWindow win, Rect pos, IntPtr searchRoot)
|
||
{
|
||
try
|
||
{
|
||
float ppp;
|
||
try { ppp = EditorGUIUtility.pixelsPerPoint; } catch { ppp = -1f; }
|
||
|
||
var header = new StringBuilder();
|
||
header.AppendLine($"[NativeWindowHelper] diagnose (fail #{s_failCount}): no GUIView matched {win.GetType().Name}");
|
||
header.AppendLine($" win.position=({pos.x:F1},{pos.y:F1},{pos.width:F1},{pos.height:F1}) pixelsPerPoint={ppp:F2} dpiApi={s_dpiApiAvailable}");
|
||
header.AppendLine($" reflected=0x{s_diagReflected.ToInt64():X} class='{s_diagReflectedClass}' searchRoot=0x{searchRoot.ToInt64():X}");
|
||
header.AppendLine(s_haveContainer
|
||
? $" container={s_containerW:F1}x{s_containerH:F1} rel=({s_relLeft:F1},{s_relTop:F1})-({s_relRight:F1},{s_relBottom:F1}) exact={s_relIsExact}"
|
||
: " container=<unavailable>");
|
||
header.Append(s_diag);
|
||
CodelyLogger.LogWarning(header.ToString());
|
||
}
|
||
catch { /* diagnostics must never break the lookup */ }
|
||
}
|
||
}
|
||
}
|
||
#endif
|