@aryacode/dsh-plugin-arya-computer-use 0.0.0-stage → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1190 @@
1
+ // Original Windows UI for DSH. Uses only framework APIs; no input is synthesized.
2
+ using System;
3
+ using System.Collections.Generic;
4
+ using System.Drawing;
5
+ using System.Drawing.Drawing2D;
6
+ using System.Drawing.Imaging;
7
+ using System.Drawing.Text;
8
+ using System.Runtime.InteropServices;
9
+ using System.Threading;
10
+ #if WCU_CORE
11
+ using System.Text.Json;
12
+ #else
13
+ using System.Web.Script.Serialization;
14
+ #endif
15
+ using System.Windows.Forms;
16
+
17
+ namespace DshTakeover {
18
+ // PowerShell 7 uses modern .NET, where System.Web's serializer is unavailable.
19
+ // Keep the command dictionary's primitive/array shapes identical on both runtimes.
20
+ internal sealed class CommandJson {
21
+ #if WCU_CORE
22
+ public string Serialize(object value) { return JsonSerializer.Serialize(value); }
23
+ public T Deserialize<T>(string text) {
24
+ using (var document = JsonDocument.Parse(text)) return (T)ConvertValue(document.RootElement);
25
+ }
26
+ static object ConvertValue(JsonElement value) {
27
+ switch (value.ValueKind) {
28
+ case JsonValueKind.Object:
29
+ var map = new Dictionary<string,object>();
30
+ foreach (var entry in value.EnumerateObject()) map.Add(entry.Name, ConvertValue(entry.Value));
31
+ return map;
32
+ case JsonValueKind.Array:
33
+ var items = new List<object>();
34
+ foreach (var entry in value.EnumerateArray()) items.Add(ConvertValue(entry));
35
+ return items.ToArray();
36
+ case JsonValueKind.String: return value.GetString();
37
+ case JsonValueKind.Number:
38
+ long integer;
39
+ if (value.TryGetInt64(out integer)) return integer;
40
+ return value.GetDouble();
41
+ case JsonValueKind.True: return true;
42
+ case JsonValueKind.False: return false;
43
+ default: return null;
44
+ }
45
+ }
46
+ #else
47
+ readonly JavaScriptSerializer inner = new JavaScriptSerializer();
48
+ public string Serialize(object value) { return inner.Serialize(value); }
49
+ public T Deserialize<T>(string text) { return inner.Deserialize<T>(text); }
50
+ #endif
51
+ }
52
+ public static class Host {
53
+ public static void Run(string eventName, bool testMode) {
54
+ try { Native.SetProcessDpiAwarenessContext(new IntPtr(-4)); Native.SetThreadDpiAwarenessContext(new IntPtr(-4)); } catch (EntryPointNotFoundException) { }
55
+ Application.SetUnhandledExceptionMode(UnhandledExceptionMode.ThrowException);
56
+ Application.EnableVisualStyles();
57
+ using (var context = new Controller(eventName, testMode)) Application.Run(context);
58
+ }
59
+ }
60
+
61
+ internal static class Native {
62
+ internal delegate IntPtr Hook(int code, IntPtr message, IntPtr data);
63
+ [StructLayout(LayoutKind.Sequential)] internal struct Key { public uint vk, scan, flags, time; public UIntPtr extra; }
64
+ [StructLayout(LayoutKind.Sequential)] internal struct Point { public int x, y; public Point(int a,int b) {x=a;y=b;} }
65
+ [StructLayout(LayoutKind.Sequential)] internal struct Size { public int cx, cy; public Size(int a,int b) {cx=a;cy=b;} }
66
+ [StructLayout(LayoutKind.Sequential)] internal struct Rect { public int left,top,right,bottom; internal Rectangle Bounds { get { return Rectangle.FromLTRB(left,top,right,bottom); } } }
67
+ [StructLayout(LayoutKind.Sequential)] internal struct MonitorInfo { public uint size; public Rect bounds,work; public uint flags; }
68
+ internal delegate bool MonitorCallback(IntPtr monitor,IntPtr dc,IntPtr rectangle,IntPtr data);
69
+ [StructLayout(LayoutKind.Sequential, Pack=1)] internal struct Blend { public byte op, flags, alpha, format; }
70
+ [StructLayout(LayoutKind.Sequential)] internal struct IconInfo { public bool icon; public int hotX, hotY; public IntPtr mask, color; }
71
+ [DllImport("user32.dll", SetLastError=true)] internal static extern bool GetIconInfo(IntPtr icon, out IconInfo info);
72
+ [DllImport("user32.dll")] internal static extern IntPtr LoadCursor(IntPtr instance, IntPtr name);
73
+ [DllImport("user32.dll")] internal static extern uint GetDpiForSystem();
74
+ [DllImport("user32.dll")] internal static extern uint GetDpiForWindow(IntPtr window);
75
+ [DllImport("user32.dll")] internal static extern IntPtr SetThreadDpiAwarenessContext(IntPtr value);
76
+ [DllImport("user32.dll", SetLastError=true)] internal static extern bool SystemParametersInfo(uint action, uint param, out int value, uint ini);
77
+ [DllImport("user32.dll")] internal static extern bool SetProcessDpiAwarenessContext(IntPtr value);
78
+ [DllImport("user32.dll", SetLastError=true)] internal static extern IntPtr SetWindowsHookEx(int id, Hook callback, IntPtr module, uint thread);
79
+ [DllImport("user32.dll")] internal static extern bool UnhookWindowsHookEx(IntPtr hook);
80
+ [DllImport("user32.dll")] internal static extern IntPtr CallNextHookEx(IntPtr hook, int code, IntPtr message, IntPtr data);
81
+ [DllImport("kernel32.dll", CharSet=CharSet.Unicode)] internal static extern IntPtr GetModuleHandle(string name);
82
+ [DllImport("user32.dll")] internal static extern IntPtr GetForegroundWindow();
83
+ [DllImport("user32.dll",SetLastError=true)] internal static extern bool GetCursorPos(out Point point);
84
+ [DllImport("user32.dll")] internal static extern IntPtr MonitorFromPoint(Point point, uint flags);
85
+ [DllImport("user32.dll",SetLastError=true)] internal static extern bool EnumDisplayMonitors(IntPtr dc,IntPtr clip,MonitorCallback callback,IntPtr data);
86
+ [DllImport("user32.dll",CharSet=CharSet.Unicode)] internal static extern bool GetMonitorInfo(IntPtr monitor,ref MonitorInfo info);
87
+ [DllImport("shcore.dll")] internal static extern int GetDpiForMonitor(IntPtr monitor, int type, out uint x, out uint y);
88
+ [DllImport("user32.dll")] internal static extern int GetWindowLong(IntPtr window, int index);
89
+ [DllImport("user32.dll")] internal static extern int SetWindowLong(IntPtr window,int index,int value);
90
+ [DllImport("user32.dll")] internal static extern bool SetWindowDisplayAffinity(IntPtr window, uint affinity);
91
+ [DllImport("user32.dll")] internal static extern bool GetWindowDisplayAffinity(IntPtr window, out uint affinity);
92
+ [DllImport("user32.dll")] internal static extern bool SetWindowPos(IntPtr window,IntPtr after,int x,int y,int width,int height,uint flags);
93
+ [StructLayout(LayoutKind.Sequential)] internal struct ScrollInfo {public uint size,mask;public int minimum,maximum;public uint page;public int position,track;}
94
+ [StructLayout(LayoutKind.Sequential,Pack=4)] internal struct ParagraphFormat {
95
+ public uint size,mask;public short numbering,reserved;public int startIndent,rightIndent,offset;public short alignment,tabCount;
96
+ [MarshalAs(UnmanagedType.ByValArray,SizeConst=32)] public int[] tabs;
97
+ public int spaceBefore,spaceAfter,lineSpacing;public short style;public byte lineRule,outline;
98
+ public short shadingWeight,shadingStyle,numberingStart,numberingStyle,numberingTab,borderSpace,borderWidth,borders;
99
+ }
100
+ [DllImport("user32.dll")] internal static extern bool GetScrollInfo(IntPtr window,int bar,ref ScrollInfo info);
101
+ [DllImport("user32.dll")] internal static extern IntPtr SendMessage(IntPtr window,uint message,IntPtr wParam,IntPtr lParam);
102
+ [DllImport("user32.dll",EntryPoint="SendMessageW")] internal static extern IntPtr SendPointMessage(IntPtr window,uint message,IntPtr wParam,ref Point point);
103
+ [DllImport("user32.dll",EntryPoint="SendMessageW")] internal static extern IntPtr SendParagraphMessage(IntPtr window,uint message,IntPtr wParam,ref ParagraphFormat format);
104
+ [DllImport("user32.dll")] internal static extern bool PrintWindow(IntPtr window,IntPtr dc,uint flags);
105
+ [DllImport("user32.dll")] internal static extern IntPtr GetDC(IntPtr window);
106
+ [DllImport("user32.dll")] internal static extern int ReleaseDC(IntPtr window, IntPtr dc);
107
+ [DllImport("gdi32.dll")] internal static extern IntPtr CreateCompatibleDC(IntPtr dc);
108
+ [DllImport("gdi32.dll")] internal static extern IntPtr SelectObject(IntPtr dc, IntPtr value);
109
+ [DllImport("gdi32.dll")] internal static extern bool DeleteObject(IntPtr value);
110
+ [DllImport("gdi32.dll")] internal static extern bool DeleteDC(IntPtr dc);
111
+ [DllImport("user32.dll", SetLastError=true)] internal static extern bool UpdateLayeredWindow(IntPtr hwnd, IntPtr screenDc, ref Point destination, ref Size size, IntPtr sourceDc, ref Point source, uint key, ref Blend blend, uint flags);
112
+ [StructLayout(LayoutKind.Sequential)] internal struct Msg { public IntPtr window; public uint message; public IntPtr wParam, lParam; public uint time; public Point point; public uint extra; }
113
+ [DllImport("user32.dll")] internal static extern int GetMessage(out Msg message, IntPtr window, uint first, uint last);
114
+ [DllImport("user32.dll")] internal static extern bool PeekMessage(out Msg message, IntPtr window, uint first, uint last, uint remove);
115
+ [DllImport("user32.dll")] internal static extern bool PostThreadMessage(uint thread, uint message, IntPtr wParam, IntPtr lParam);
116
+ [DllImport("kernel32.dll")] internal static extern uint GetCurrentThreadId();
117
+ internal static Rectangle[] ActiveDisplays() {
118
+ var result=new List<Rectangle>();
119
+ if(!EnumDisplayMonitors(IntPtr.Zero,IntPtr.Zero,delegate(IntPtr monitor,IntPtr dc,IntPtr rect,IntPtr data){
120
+ var info=new MonitorInfo {size=(uint)Marshal.SizeOf(typeof(MonitorInfo))};
121
+ if(GetMonitorInfo(monitor,ref info)&&info.bounds.right>info.bounds.left&&info.bounds.bottom>info.bounds.top)result.Add(info.bounds.Bounds);
122
+ return true;
123
+ },IntPtr.Zero))throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error(),"Could not enumerate active displays");
124
+ result.Sort(delegate(Rectangle a,Rectangle b){int x=a.Left.CompareTo(b.Left);return x!=0?x:a.Top.CompareTo(b.Top);});
125
+ return result.ToArray();
126
+ }
127
+ internal static Rectangle DisplayFromPoint(Point point) {
128
+ var info=new MonitorInfo {size=(uint)Marshal.SizeOf(typeof(MonitorInfo))};
129
+ if(!GetMonitorInfo(MonitorFromPoint(point,2),ref info))throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error(),"Could not locate pointer display");
130
+ return info.bounds.Bounds;
131
+ }
132
+ internal static string DisplayIdentity(Rectangle bounds) {
133
+ IntPtr monitor=MonitorFromPoint(new Point(bounds.Left+bounds.Width/2,bounds.Top+bounds.Height/2),2);
134
+ uint x=96,y=96;try{if(GetDpiForMonitor(monitor,0,out x,out y)!=0)x=y=96;}catch(EntryPointNotFoundException){}
135
+ return bounds.ToString()+"/"+monitor.ToInt64()+"/"+x+"/"+y;
136
+ }
137
+ internal static void KeepOnTop(IntPtr window) { SetWindowPos(window,new IntPtr(-1),0,0,0,0,0x213); }
138
+ }
139
+
140
+ // Where the user's own mouse last moved, for the overlay's lock button. A
141
+ // low-level hook on a thread of its own records physical movement only:
142
+ // input injected with SendInput (DSH's clicks among it) carries
143
+ // LLMHF_INJECTED and is skipped, and SetCursorPos never reaches the hook.
144
+ // The hook stores one number, so system input never waits on the overlay.
145
+ internal sealed class PointerWatch : IDisposable {
146
+ readonly Native.Hook callback;
147
+ readonly ManualResetEvent ready = new ManualResetEvent(false);
148
+ IntPtr hook; volatile uint threadId; long packed; volatile bool seen, testFed, disposed;
149
+ internal PointerWatch() {
150
+ callback = OnMouse;
151
+ new Thread(Run) { IsBackground = true, Name = "DSH overlay pointer" }.Start();
152
+ ready.WaitOne(2000);
153
+ }
154
+ internal bool Active { get { return hook != IntPtr.Zero; } }
155
+ void Run() {
156
+ Native.Msg message; Native.PeekMessage(out message, IntPtr.Zero, 0, 0, 0);
157
+ threadId = Native.GetCurrentThreadId();
158
+ if (disposed) { ready.Set(); return; }
159
+ hook = Native.SetWindowsHookEx(14, callback, Native.GetModuleHandle(null), 0);
160
+ ready.Set();
161
+ if (hook == IntPtr.Zero) return;
162
+ try { if (!disposed) while (Native.GetMessage(out message, IntPtr.Zero, 0, 0) > 0) { } }
163
+ finally { Native.UnhookWindowsHookEx(hook); hook = IntPtr.Zero; }
164
+ }
165
+ IntPtr OnMouse(int code, IntPtr message, IntPtr data) {
166
+ if (code >= 0 && !testFed) Record(message.ToInt32(), data);
167
+ return Native.CallNextHookEx(hook, code, message, data);
168
+ }
169
+ // MSLLHOOKSTRUCT: pt.x at 0, pt.y at 4, flags at 12 (1 = injected, 2 = injected from a lower integrity level).
170
+ void Record(int kind, IntPtr data) {
171
+ if (kind != 0x200 || (Marshal.ReadInt32(data, 12) & 0x03) != 0) return;
172
+ Interlocked.Exchange(ref packed, ((long)Marshal.ReadInt32(data, 0) << 32) | (uint)Marshal.ReadInt32(data, 4));
173
+ seen = true;
174
+ }
175
+ // Tests feed the same parser a hook record built in memory, so no real input is needed.
176
+ internal void TestFeed(int x, int y, int flags) {
177
+ testFed = true; IntPtr data = Marshal.AllocHGlobal(32);
178
+ try { for (int offset = 0; offset < 32; offset += 4) Marshal.WriteInt32(data, offset, 0); Marshal.WriteInt32(data, 0, x); Marshal.WriteInt32(data, 4, y); Marshal.WriteInt32(data, 12, flags); Record(0x200, data); }
179
+ finally { Marshal.FreeHGlobal(data); }
180
+ }
181
+ internal bool Latest(out Native.Point point) {
182
+ long value = Interlocked.Read(ref packed); point = new Native.Point((int)(value >> 32), (int)value);
183
+ return seen;
184
+ }
185
+ public void Dispose() { disposed = true; if (threadId != 0) Native.PostThreadMessage(threadId, 0x12, IntPtr.Zero, IntPtr.Zero); }
186
+ }
187
+
188
+ internal sealed class Controller : ApplicationContext {
189
+ readonly Control dispatcher = new Control();
190
+ readonly EventWaitHandle cancel;
191
+ readonly Native.Hook callback;
192
+ readonly IntPtr hook;
193
+ readonly bool testMode;
194
+ readonly CommandJson json = new CommandJson();
195
+ readonly List<Frame> frames = new List<Frame>();
196
+ PointerFrame pointer;
197
+ ProgressFrame progress;
198
+ PointerWatch pointerWatch;
199
+ Native.Point? testOverlayCursor;
200
+ Rectangle? targetWindow;
201
+ Native.Point? ghostPoint;
202
+ Native.Point ghostActual;
203
+ bool ghostPlaced;
204
+ DateTime progressPreviewUntil=DateTime.MinValue;
205
+ bool progressDismissed,progressCloseWhenIdle;
206
+ bool progressSettingEnabled=true;
207
+ int progressKey,progressModifiers;
208
+ Native.Point? testPointerPoint;
209
+ Rectangle[] displays=new Rectangle[0],testDisplays;
210
+ string displaySignature="";
211
+ DateTime nextDisplayPoll=DateTime.MinValue;
212
+ int displayRevision;
213
+ readonly System.Windows.Forms.Timer timer = new System.Windows.Forms.Timer();
214
+ bool armed, paused, shuttingDown;
215
+ readonly HashSet<int> physicalDown = new HashSet<int>(), swallowed = new HashSet<int>();
216
+ string stopShortcut = "";
217
+ int stopKey, stopModifiers;
218
+ DateTime hideAt = DateTime.MaxValue;
219
+ bool indicator = true, cursorIndicator = true, remoteVisible = true;
220
+ int cursorError;
221
+ string text = "Arya is controlling your computer", position = "top";
222
+ string pointerLabel = PointerFrame.Label;
223
+ int idleMs = 800;
224
+
225
+ internal Controller(string eventName, bool tests) {
226
+ testMode = tests;
227
+ cancel = new EventWaitHandle(false, EventResetMode.ManualReset, eventName);
228
+ var handle = dispatcher.Handle;
229
+ callback = OnKey;
230
+ hook = Native.SetWindowsHookEx(13, callback, Native.GetModuleHandle(null), 0);
231
+ if (hook == IntPtr.Zero) throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error(), "Could not install the stop shortcut listener");
232
+ timer.Interval = 35;
233
+ timer.Tick += delegate {
234
+ // A window the user is moving or resizing goes once they are done.
235
+ if(progress!=null&&progressPreviewUntil!=DateTime.MaxValue&&DateTime.UtcNow>progressPreviewUntil&&!progress.Adjusting)CloseProgress();
236
+ if(progressCloseWhenIdle&&progress!=null&&!progress.Adjusting)CloseProgress();
237
+ TrackProgressPointer();
238
+
239
+ double elapsed = hideAt == DateTime.MaxValue ? -1 : (DateTime.UtcNow - hideAt).TotalMilliseconds;
240
+ if (elapsed >= 160) { HideFrames(); HidePointer(); armed = false; hideAt = DateTime.MaxValue; return; }
241
+ byte alpha = elapsed >= 0 ? (byte)Math.Max(0, 255 * (1 - elapsed / 160)) : (byte)255;
242
+ if(armed&&!paused)RefreshDisplays(false);
243
+ UpdatePointer(alpha);
244
+ if (elapsed >= 0) foreach (var frame in frames) frame.Present(alpha);
245
+ };
246
+ timer.Start();
247
+ SystemEventsSubscribe();
248
+ new Thread(ReadCommands) { IsBackground = true, Name = "DSH takeover control" }.Start();
249
+ RefreshDisplays(true);
250
+ Emit(new { kind = "ready", hookActive = true, monitors = displays.Length });
251
+ }
252
+
253
+ void SystemEventsSubscribe() { Microsoft.Win32.SystemEvents.DisplaySettingsChanged += DisplaysChanged; Microsoft.Win32.SystemEvents.SessionSwitch += SessionChanged; Microsoft.Win32.SystemEvents.UserPreferenceChanged += PreferencesChanged; }
254
+ // A pointer size or scheme change arrives as a preference change; measure the arrow again.
255
+ void PreferencesChanged(object sender,Microsoft.Win32.UserPreferenceChangedEventArgs args) {
256
+ if (shuttingDown) return;
257
+ try { dispatcher.BeginInvoke((Action)PointerFrame.ForgetPointerSize); } catch (InvalidOperationException) { }
258
+ }
259
+ void SessionChanged(object sender,Microsoft.Win32.SessionSwitchEventArgs args) { DisplaysChanged(sender,EventArgs.Empty); }
260
+ void DisplaysChanged(object sender, EventArgs args) {
261
+ if (shuttingDown) return;
262
+ try { dispatcher.BeginInvoke((Action)delegate { RefreshDisplays(true); UpdatePointer(255); }); } catch (InvalidOperationException) { }
263
+ }
264
+ bool RefreshDisplays(bool force) {
265
+ if(!force&&DateTime.UtcNow<nextDisplayPoll)return false;
266
+ nextDisplayPoll=DateTime.UtcNow.AddMilliseconds(500);
267
+ Rectangle[] next=testMode&&testDisplays!=null?testDisplays:Native.ActiveDisplays();
268
+ string signature=String.Join(";",Array.ConvertAll(next,delegate(Rectangle r){return Native.DisplayIdentity(r);}));
269
+ if(signature==displaySignature){foreach(var frame in frames)Native.KeepOnTop(frame.Handle);if(pointer!=null)Native.KeepOnTop(pointer.Handle);return false;}
270
+ displays=next;displaySignature=signature;displayRevision++;
271
+ HideFrames();HidePointer();
272
+ if(armed&&!paused&&indicator)ShowFrames();
273
+ return true;
274
+ }
275
+ void ConfigureVisibility(bool visible) {
276
+ remoteVisible=visible;
277
+ foreach(var frame in frames)frame.SetRemoteVisible(visible);
278
+ if(pointer!=null)pointer.SetRemoteVisible(visible);
279
+ }
280
+ void Emit(object value) { lock (json) { Console.Out.WriteLine(json.Serialize(value)); Console.Out.Flush(); } }
281
+ void ReadCommands() {
282
+ try {
283
+ string line;
284
+ while ((line = Console.ReadLine()) != null) {
285
+ var command = new CommandJson().Deserialize<Dictionary<string,object>>(line);
286
+ dispatcher.BeginInvoke((Action)(() => Dispatch(command)));
287
+ }
288
+ } catch (Exception error) { Console.Error.WriteLine(error.Message); }
289
+ finally { try { cancel.Set(); } catch (ObjectDisposedException) { } try { dispatcher.BeginInvoke((Action)(() => ExitThread())); } catch (InvalidOperationException) { } }
290
+ }
291
+ static T Get<T>(Dictionary<string,object> command, string name, T fallback) {
292
+ object value; return command.TryGetValue(name, out value) ? (T)Convert.ChangeType(value, typeof(T)) : fallback;
293
+ }
294
+ static int Int(Dictionary<string,object> map, string name) {
295
+ object value;
296
+ if (map == null || !map.TryGetValue(name, out value) || value == null) return 0;
297
+ return (int)Math.Round(Convert.ToDouble(value, System.Globalization.CultureInfo.InvariantCulture));
298
+ }
299
+ void Dispatch(Dictionary<string,object> command) {
300
+ object id; command.TryGetValue("id", out id);
301
+ try {
302
+ bool consumed = false;
303
+ string action = Get(command, "command", "");
304
+ if (action == "begin" || action == "preview") {
305
+ if (paused) throw new InvalidOperationException("COMPUTER_USE_STOPPED_BY_USER");
306
+ ConfigureShortcut(Get(command, "stopShortcut", stopShortcut));
307
+ bool nextIndicator = Get(command, "indicatorEnabled", true);
308
+ ConfigureVisibility(Get(command,"remoteIndicatorVisible",true));
309
+ cursorIndicator = Get(command, "cursorIndicatorEnabled", true);
310
+ ApplyPointerLabel(Get(command, "pointerLabel", pointerLabel));
311
+ string nextText = Get(command, "indicatorText", "Arya is controlling your computer"), nextPosition = Get(command, "indicatorPosition", "top");
312
+ bool rebuild = nextIndicator != indicator || nextText != text || nextPosition != position || frames.Count == 0;
313
+ indicator = nextIndicator; text = nextText; position = nextPosition;
314
+ idleMs = Math.Max(200, Math.Min(3000, Get(command, "indicatorIdleMs", 800)));
315
+ armed = true; hideAt = action == "preview" ? DateTime.UtcNow.AddMilliseconds(1800) : DateTime.MaxValue;
316
+ bool displaysRebuilt=RefreshDisplays(true);
317
+ if (!indicator) HideFrames();
318
+ else if (rebuild&&!displaysRebuilt) { HideFrames(); ShowFrames(); }
319
+ else foreach (var frame in frames) frame.Present(255);
320
+ UpdatePointer(255);
321
+ } else if (action == "hint") {
322
+ // Where the harness is acting: the target window rect and the ghost
323
+ // pointer position. Both are re-sent as the drive moves.
324
+ object supplied;
325
+ bool changed = false;
326
+ if (command.TryGetValue("target", out supplied)) {
327
+ var map = supplied as Dictionary<string,object>;
328
+ Rectangle? next = map != null && Int(map,"width") > 0 && Int(map,"height") > 0
329
+ ? new Rectangle(Int(map,"left"), Int(map,"top"), Int(map,"width"), Int(map,"height"))
330
+ : (Rectangle?)null;
331
+ if (next != targetWindow) { targetWindow = next; changed = true; }
332
+ }
333
+ if (command.TryGetValue("pointer", out supplied)) {
334
+ var map = supplied as Dictionary<string,object>;
335
+ ghostPoint = map == null ? (Native.Point?)null : new Native.Point(Int(map,"x"), Int(map,"y"));
336
+ }
337
+ if (changed && armed && !paused && indicator) { HideFrames(); ShowFrames(); }
338
+ if (armed && !paused && cursorIndicator) UpdatePointer(255);
339
+ Emit(new { id = id, ok = true, target = changed, ghost = ghostPoint.HasValue });
340
+ } else if (action == "progress") {
341
+ bool enabled=Get(command,"enabled",false),preview=Get(command,"preview",false);
342
+ object settingsObject;var presentedSettings=command.TryGetValue("settings",out settingsObject)?settingsObject as Dictionary<string,object>:null;
343
+ bool settingEnabled=presentedSettings==null||Get(presentedSettings,"progressOverlayEnabled",true);
344
+ if(progressSettingEnabled&&!settingEnabled)progressPreviewUntil=DateTime.MinValue;
345
+ progressSettingEnabled=settingEnabled;
346
+ if(preview){progressDismissed=false;progressPreviewUntil=DateTime.UtcNow.AddSeconds(60);}
347
+ if(!enabled)progressDismissed=false;
348
+ if(progressDismissed&&!preview){Emit(new {id=id,ok=true,progressVisible=false});return;}
349
+ bool leavingPreview=enabled&&!preview&&progressPreviewUntil!=DateTime.MaxValue;
350
+ if(enabled&&!preview)progressPreviewUntil=DateTime.MaxValue;
351
+ if(!enabled&&(progressPreviewUntil==DateTime.MaxValue||DateTime.UtcNow>=progressPreviewUntil)){if(progress!=null&&progress.Adjusting)progressCloseWhenIdle=true;else CloseProgress();}
352
+ else {
353
+ progressCloseWhenIdle=false;
354
+ object supplied;
355
+ var data=command.TryGetValue("progress",out supplied)?supplied as Dictionary<string,object>:null;
356
+ var settings=command.TryGetValue("settings",out supplied)?supplied as Dictionary<string,object>:null;
357
+ bool fresh=progress==null;
358
+ if(fresh)progress=new ProgressFrame(delegate(Dictionary<string,object> patch,Dictionary<string,object> baseline){Emit(new {kind="progress-layout",patch=patch,@base=baseline});},delegate {Emit(new {kind="progress-layout",patch=new Dictionary<string,object>{{"progressOverlayEnabled",false}}});progressPreviewUntil=DateTime.MinValue;progressDismissed=true;CloseProgress();});
359
+ progress.Configure(settings,Get(command,"remoteIndicatorVisible",true));
360
+ int savedKey=stopKey,savedModifiers=stopModifiers;string savedShortcut=stopShortcut;
361
+ ConfigureShortcut(settings!=null?Get(settings,"progressLockShortcut",""):"");progressKey=stopKey;progressModifiers=stopModifiers;
362
+ stopKey=savedKey;stopModifiers=savedModifiers;stopShortcut=savedShortcut;
363
+ if(enabled)progress.UpdateProgress(data);
364
+ if(fresh)progress.Show();
365
+ // The adjustment preview unlocks only while it lasts; the saved lock
366
+ // state is untouched unless the user toggles it themselves.
367
+ if(preview)progress.BeginPreview();else if(leavingPreview)progress.EndPreview();
368
+ }
369
+ } else if(action=="test_progress_close"&&testMode){
370
+ if(progress!=null)progress.RequestClose();
371
+ } else if(action=="test_progress_layout"&&testMode){
372
+ if(progress==null)throw new InvalidOperationException("No progress window");
373
+ progress.Bounds=new Rectangle(Get(command,"x",100),Get(command,"y",100),Get(command,"width",500),Get(command,"height",350));progress.EmitLayout();
374
+ } else if (action == "test_render_progress" && testMode) {
375
+ if(progress==null)throw new InvalidOperationException("No progress window");
376
+ Emit(new {id=id,ok=true,capture=progress.CaptureProgress(Get(command,"path",""),Get(command,"screen",false))});return;
377
+ } else if(action=="test_progress_scroll"&&testMode){
378
+ if(progress!=null){if(Get(command,"wheel",false))progress.ScrollWheel(Get(command,"delta",120));else progress.ScrollLines(Get(command,"lines",0));}
379
+ } else if(action=="test_progress_dpi"&&testMode){
380
+ if(progress!=null)progress.TestDpi((float)Get(command,"scale",1.0));
381
+ } else if(action=="test_progress_scrollbar"&&testMode){
382
+ if(progress!=null)progress.TestScrollbar(Get(command,"x",0),Get(command,"y",0),Get(command,"endY",0));
383
+ } else if(action=="test_progress_hittest"&&testMode){
384
+ if(progress==null)throw new InvalidOperationException("No progress window");
385
+ Emit(new {id=id,ok=true,hit=progress.TestHitAt(Get(command,"x",20),Get(command,"y",20))});return;
386
+ } else if(action=="test_progress_close_click"&&testMode){
387
+ if(progress!=null)progress.TestCloseClick();
388
+ } else if(action=="test_progress_lock_click"&&testMode){
389
+ if(progress!=null)progress.TestLockClick();
390
+ } else if(action=="test_progress_pointer"&&testMode){
391
+ // Where the cursor is (instead of GetCursorPos); a physical move is a
392
+ // hook record fed through the real parser with flags 0, an injected one with flags 1.
393
+ testOverlayCursor=new Native.Point(Get(command,"x",0),Get(command,"y",0));
394
+ int flags=Get(command,"hookFlags",-1);
395
+ if(flags>=0){if(pointerWatch==null)pointerWatch=new PointerWatch();pointerWatch.TestFeed(Get(command,"x",0),Get(command,"y",0),flags);}
396
+ TrackProgressPointer();
397
+ } else if(action=="test_progress_backdrop"&&testMode){
398
+ if(progress!=null)progress.ShowTestBackdrop();
399
+ } else if(action=="test_progress_resize_redraw"&&testMode){
400
+ if(progress!=null)progress.TestResizeRedraw(Get(command,"enabled",true));
401
+ } else if (action == "configure") {
402
+ ConfigureShortcut(Get(command, "stopShortcut", ""));
403
+ ConfigureVisibility(Get(command,"remoteIndicatorVisible",remoteVisible));
404
+ ApplyPointerLabel(Get(command, "pointerLabel", pointerLabel));
405
+ cursorIndicator = Get(command, "cursorIndicatorEnabled", cursorIndicator); UpdatePointer(255);
406
+ } else if (action == "end") {
407
+ hideAt = DateTime.UtcNow.AddMilliseconds(idleMs);
408
+ } else if (action == "stop") {
409
+ Stop("user");
410
+ } else if (action == "resume") {
411
+ paused = false; armed = false; cancel.Reset(); HideFrames(); HidePointer(); hideAt = DateTime.MaxValue;
412
+ } else if (action == "close") {
413
+ cancel.Set(); ExitThread();
414
+ } else if (action == "test_key" && testMode) {
415
+ consumed = HandleKey(Get(command, "vk", 0), Get(command, "injected", false), Get(command, "down", true));
416
+ } else if (action == "test_render" && testMode) {
417
+ using (var image = Frame.Draw(1200, 675, Get(command, "indicatorText", text), Get(command, "indicatorPosition", "top"))) image.Save(Get(command, "path", ""), ImageFormat.Png);
418
+ } else if (action == "test_render_pointer" && testMode) {
419
+ float zoom=(float)Get(command,"scale",2.0);
420
+ using (var image = PointerFrame.Draw(zoom, false, false)) {
421
+ using (var graphics = Graphics.FromImage(image)) Cursors.Arrow.Draw(graphics, new Rectangle((int)(24*zoom),(int)(24*zoom),(int)(16*zoom),(int)(16*zoom)));
422
+ image.Save(Get(command, "path", ""), ImageFormat.Png);
423
+ }
424
+ } else if (action == "test_pointer_source" && testMode) {
425
+ testPointerPoint=new Native.Point(Get(command,"x",200),Get(command,"y",200)); UpdatePointer(255);
426
+ } else if (action == "test_pointer_layout" && testMode) {
427
+ var bounds = PointerFrame.LayoutForPoint(new Native.Point(Get(command,"x",0),Get(command,"y",0)), new Rectangle(Get(command,"left",0),Get(command,"top",0),Get(command,"width",1920),Get(command,"height",1080)), (float)Get(command,"scale",1.0));
428
+ Emit(new { id=id,ok=true,left=bounds.X,top=bounds.Y,width=bounds.Width,height=bounds.Height }); return;
429
+ } else if(action=="test_displays"&&testMode) {
430
+ object supplied;if(!command.TryGetValue("displays",out supplied)||!(supplied is System.Collections.IEnumerable))throw new ArgumentException("Displays array required");
431
+ var next=new List<Rectangle>();
432
+ foreach(var item in (System.Collections.IEnumerable)supplied){var r=item as Dictionary<string,object>;if(r==null)throw new ArgumentException("Display object required");int width=Get(r,"width",0),height=Get(r,"height",0);if(width<64||height<64||width>8192||height>8192)throw new ArgumentException("Invalid test display size");next.Add(new Rectangle(Get(r,"left",0),Get(r,"top",0),width,height));}
433
+ testDisplays=next.ToArray();nextDisplayPoll=DateTime.MinValue;
434
+ } else if(action=="test_restore_displays"&&testMode) {
435
+ testDisplays=null;nextDisplayPoll=DateTime.MinValue;
436
+ } else if (action != "status") throw new InvalidOperationException("Unknown takeover command");
437
+ Native.Point livePoint; bool liveCursorAvailable=Native.GetCursorPos(out livePoint);
438
+ Emit(new { id = id, ok = true, armed = armed, paused = paused, cancelled = cancel.WaitOne(0), consumed = consumed, stopShortcut = stopShortcut, windows = frames.Count, progressVisible=progress!=null, progressClickThrough=progress!=null&&progress.ClickThrough, progressContent=progress!=null?progress.Content:null, progressLocked=progress!=null&&progress.Locked, progressHover=progress!=null?progress.HoverStatus():null, pointerWatchActive=pointerWatch!=null&&pointerWatch.Active, progressReading=progress!=null?progress.ReadingStatus():null, progressOpacity=progress!=null?progress.BackgroundOpacity:0, progressBounds=progress!=null?new {x=progress.Left,y=progress.Top,width=progress.Width,height=progress.Height}:null, remoteIndicatorVisible=remoteVisible, overlayCaptureExcluded=frames.Count>0&&frames.TrueForAll(frame=>frame.CaptureExcluded), overlayClickThrough=frames.Count>0&&frames.TrueForAll(frame=>frame.ClickThrough), displayRevision=displayRevision, displays=Array.ConvertAll(displays,r=>new {left=r.Left,top=r.Top,width=r.Width,height=r.Height}), overlayBounds=frames.ConvertAll(frame=>new {left=frame.DisplayBounds.Left,top=frame.DisplayBounds.Top,width=frame.DisplayBounds.Width,height=frame.DisplayBounds.Height}), cursorError = cursorError, liveCursorAvailable = liveCursorAvailable, pointerSource = testPointerPoint.HasValue ? "test-seam" : "windows", pointerVisible = pointer != null, pointerClickThrough = pointer != null && pointer.ClickThrough, pointerCaptureExcluded = pointer != null && pointer.CaptureExcluded, foreground = Native.GetForegroundWindow().ToInt64() });
439
+ } catch (Exception error) { Emit(new { id = id, ok = false, error = error.Message }); }
440
+ }
441
+ IntPtr OnKey(int code, IntPtr message, IntPtr data) {
442
+ if (code >= 0) {
443
+ int kind = message.ToInt32();
444
+ if (kind == 0x100 || kind == 0x104 || kind == 0x101 || kind == 0x105) {
445
+ var key = (Native.Key)Marshal.PtrToStructure(data, typeof(Native.Key));
446
+ if (HandleKey((int)key.vk, (key.flags & 0x12) != 0, kind == 0x100 || kind == 0x104)) return new IntPtr(1);
447
+ }
448
+ }
449
+ return Native.CallNextHookEx(hook, code, message, data);
450
+ }
451
+ static int Modifier(int vk) {
452
+ if (vk == 0x11 || vk == 0xA2 || vk == 0xA3) return 1;
453
+ if (vk == 0x12 || vk == 0xA4 || vk == 0xA5) return 2;
454
+ if (vk == 0x10 || vk == 0xA0 || vk == 0xA1) return 4;
455
+ if (vk == 0x5B || vk == 0x5C) return 8;
456
+ return 0;
457
+ }
458
+ void ConfigureShortcut(string shortcut) {
459
+ int nextKey = 0, nextModifiers = 0;
460
+ if (shortcut.Length > 0) {
461
+ string[] parts = shortcut.Split('+');
462
+ for (int i = 0; i < parts.Length - 1; i++) {
463
+ int flag = parts[i] == "Ctrl" ? 1 : parts[i] == "Alt" ? 2 : parts[i] == "Shift" ? 4 : parts[i] == "Win" ? 8 : 0;
464
+ if (flag == 0 || (nextModifiers & flag) != 0) throw new ArgumentException("Invalid stop shortcut modifiers");
465
+ nextModifiers |= flag;
466
+ }
467
+ string key = parts[parts.Length - 1]; int number;
468
+ if (key.Length == 1 && ((key[0] >= 'A' && key[0] <= 'Z') || (key[0] >= '0' && key[0] <= '9'))) nextKey = key[0];
469
+ else if (key.StartsWith("F") && int.TryParse(key.Substring(1), out number) && number >= 1 && number <= 24) nextKey = 0x6F + number;
470
+ else {
471
+ var common = new Dictionary<string,int> { {"Space",0x20},{"Enter",0x0D},{"Tab",0x09},{"Backspace",0x08},{"Delete",0x2E},{"Insert",0x2D},{"Home",0x24},{"End",0x23},{"PageUp",0x21},{"PageDown",0x22},{"Up",0x26},{"Down",0x28},{"Left",0x25},{"Right",0x27},{"Pause",0x13},{"CapsLock",0x14},{"NumLock",0x90},{"ScrollLock",0x91} };
472
+ if (!common.TryGetValue(key, out nextKey)) throw new ArgumentException("Invalid stop shortcut key");
473
+ }
474
+ }
475
+ stopShortcut = shortcut; stopKey = nextKey; stopModifiers = nextModifiers;
476
+ }
477
+ bool HandleKey(int vk, bool injected, bool down) {
478
+ if (injected) return false;
479
+ if (!down) { physicalDown.Remove(vk); return swallowed.Remove(vk); }
480
+ physicalDown.Add(vk);
481
+ if (swallowed.Contains(vk)) return true;
482
+ int modifiers = 0; foreach (int key in physicalDown) modifiers |= Modifier(key);
483
+ if(progress!=null&&progressKey!=0&&vk==progressKey&&modifiers==progressModifiers&&!(armed&&!paused&&vk==stopKey&&modifiers==stopModifiers)){
484
+ swallowed.Add(vk);progress.SetLocked(!progress.Locked,true);return true;
485
+ }
486
+ if (!armed || paused) return false;
487
+ if (vk != 0x1B && (stopKey == 0 || vk != stopKey || modifiers != stopModifiers)) return false;
488
+ swallowed.Add(vk);
489
+ // Signal the worker first, before any rendering or pipe activity.
490
+ cancel.Set(); Stop(vk == 0x1B ? "escape" : "shortcut"); return true;
491
+ }
492
+ void Stop(string reason) {
493
+ cancel.Set(); paused = true; armed = false; hideAt = DateTime.MaxValue; HideFrames(); HidePointer();
494
+ Emit(new { kind = "stopped", reason = reason });
495
+ }
496
+ // The highlight marks the application window the harness is driving, so one
497
+ // rim frames the app instead of every monitor's edge. Without a target window
498
+ // (nothing driven yet) the overlay falls back to the display edges.
499
+ void ShowFrames() {
500
+ var target = targetWindow;
501
+ if (target.HasValue && target.Value.Width >= 80 && target.Value.Height >= 80) {
502
+ var rim = Rectangle.Inflate(target.Value, 2, 2);
503
+ var center = new Native.Point(rim.X + rim.Width / 2, rim.Y + rim.Height / 2);
504
+ Rectangle screen = Native.DisplayFromPoint(center);
505
+ int band = Math.Max(44, (int)Math.Round(76 * Math.Max(1f, Math.Min(2f, rim.Width / 1920f))));
506
+ Rectangle surface = rim; string pill = position;
507
+ if (pill != "bottom" && rim.Y - band >= screen.Top) surface = new Rectangle(rim.X, rim.Y - band, rim.Width, rim.Height + band);
508
+ else if (pill == "bottom" || rim.Bottom + band <= screen.Bottom) surface = new Rectangle(rim.X, rim.Y, rim.Width, rim.Height + band);
509
+ var frame = new Frame(surface, rim, text, pill, remoteVisible);
510
+ frames.Add(frame); frame.Show(); frame.Present(255);
511
+ return;
512
+ }
513
+ foreach (var bounds in displays) { var frame = new Frame(bounds, text, position,remoteVisible); frames.Add(frame); frame.Show(); frame.Present(255); }
514
+ }
515
+ void HideFrames() { foreach (var frame in frames) frame.Dispose(); frames.Clear(); }
516
+ // A synthetic cursor: the harness names where it acted and this draws the
517
+ // ghost there. The user's real pointer is never read, moved or highlighted.
518
+ void UpdatePointer(byte alpha) {
519
+ if (!armed || paused || !cursorIndicator) { HidePointer(); return; }
520
+ Native.Point goal;
521
+ if(testMode&&testPointerPoint.HasValue)goal=testPointerPoint.Value;
522
+ else if (ghostPoint.HasValue) goal = ghostPoint.Value;
523
+ else { HidePointer(); return; }
524
+ if (!ghostPlaced) { ghostActual = goal; ghostPlaced = true; }
525
+ else {
526
+ int dx = goal.x - ghostActual.x, dy = goal.y - ghostActual.y;
527
+ if (Math.Abs(dx) <= 2 && Math.Abs(dy) <= 2) ghostActual = goal;
528
+ else ghostActual = new Native.Point(ghostActual.x + (int)Math.Round(dx * 0.4), ghostActual.y + (int)Math.Round(dy * 0.4));
529
+ }
530
+ Native.Point point = ghostActual;
531
+ cursorError=0;
532
+ if (pointer == null) { pointer = new PointerFrame(remoteVisible); pointer.Update(point, alpha); pointer.Show(); }
533
+ else pointer.Update(point, alpha);
534
+ }
535
+ void HidePointer() { if (pointer != null) { pointer.Dispose(); pointer = null; } ghostPlaced = false; }
536
+ // The ghost cursor tag is user-configurable; a change redraws the sprite.
537
+ void ApplyPointerLabel(string next) {
538
+ if (string.IsNullOrWhiteSpace(next)) next = "Arya";
539
+ next = next.Trim();
540
+ if (next.Length > 16) next = next.Substring(0, 16);
541
+ if (next == pointerLabel) return;
542
+ pointerLabel = next; PointerFrame.Label = next; ProgressFrame.Agent = next; HidePointer();
543
+ }
544
+ void CloseProgress() {
545
+ progressCloseWhenIdle=false;
546
+ if(progress!=null){progress.Dispose();progress=null;}
547
+ if(pointerWatch!=null){pointerWatch.Dispose();pointerWatch=null;}
548
+ }
549
+ // The overlay's lock button answers the user's own pointer only. The hook
550
+ // runs only while a locked (or hover-unlocked) overlay is on screen.
551
+ void TrackProgressPointer() {
552
+ if(progress==null||progress.IsDisposed||!progress.IsHandleCreated)return;
553
+ bool watching=progress.WantsPointer;
554
+ if(watching&&pointerWatch==null)pointerWatch=new PointerWatch();
555
+ else if(!watching&&pointerWatch!=null){pointerWatch.Dispose();pointerWatch=null;}
556
+ if(!watching)return;
557
+ Native.Point cursor,moved;
558
+ if(testMode&&testOverlayCursor.HasValue)cursor=testOverlayCursor.Value;
559
+ else if(!Native.GetCursorPos(out cursor))return;
560
+ // The cursor must still be where the user's last physical move left it;
561
+ // DSH moving it elsewhere (SetCursorPos) breaks the match.
562
+ bool physical=pointerWatch.Latest(out moved)&&Math.Abs(moved.x-cursor.x)<=2&&Math.Abs(moved.y-cursor.y)<=2;
563
+ progress.TrackPointer(cursor,physical);
564
+ }
565
+ protected override void Dispose(bool disposing) {
566
+ if (disposing && !shuttingDown) {
567
+ shuttingDown = true; cancel.Set(); timer.Stop(); timer.Dispose(); HideFrames(); HidePointer(); CloseProgress();
568
+ Microsoft.Win32.SystemEvents.DisplaySettingsChanged -= DisplaysChanged;
569
+ Microsoft.Win32.SystemEvents.SessionSwitch -= SessionChanged;
570
+ Microsoft.Win32.SystemEvents.UserPreferenceChanged -= PreferencesChanged;
571
+ if (hook != IntPtr.Zero) Native.UnhookWindowsHookEx(hook);
572
+ dispatcher.Dispose(); cancel.Dispose();
573
+ }
574
+ base.Dispose(disposing);
575
+ }
576
+ }
577
+
578
+ // Independent progress window: never activates, captures input or arms Esc.
579
+ // Per-pixel composition: the background can be translucent without fading text.
580
+ // One physical-resolution surface also avoids opaque system scrollbars and DPI stretching.
581
+ internal sealed class ProgressFrame : Form {
582
+ // Name shown in the panel header; follows the configurable agent name.
583
+ internal static string Agent = "Arya";
584
+ sealed class ReadingLine { internal string text; internal bool heading; internal ReadingLine(string s,bool h){text=s;heading=h;} }
585
+ readonly List<ReadingLine> lines=new List<ReadingLine>();
586
+ internal string Content="";
587
+ internal bool Locked=true;
588
+ internal double BackgroundOpacity=.6;
589
+ Font bodyFont,headingFont,titleFont,footerFont;
590
+ string header="",subheader="",bodyText="";
591
+ string[] headings=new string[0];
592
+ bool followOutput=true,remoteVisible=true,configuredLocked=true,configured,presenting,draggingThumb,revealedResult,previewUnlocked,hoverUnlocked,hoverBlocked=true,sizing;
593
+ // Resting the pointer on the lock button this long unlocks the window; it
594
+ // locks again once the pointer has been away from the window this long.
595
+ internal const int HoverUnlockMs=400,HoverRelockMs=1500;
596
+ DateTime hoverSince=DateTime.MinValue,awaySince=DateTime.MinValue;
597
+ float hoverFraction;
598
+ // Sizes and positions this window reported itself, with when, so that their
599
+ // echo from the settings never moves the window back (see Configure).
600
+ readonly List<KeyValuePair<string,DateTime>> emitted=new List<KeyValuePair<string,DateTime>>();
601
+ string geometry="";
602
+ int paints,scrollY,wheelRemainder,dragOffset;
603
+ float scale=1,testScale;
604
+ Rectangle viewport,track,thumb;
605
+ Form testBackdrop;
606
+ Dictionary<string,object> baseline=new Dictionary<string,object>();
607
+ readonly Action<Dictionary<string,object>,Dictionary<string,object>> changed;
608
+ readonly Action closeRequested;
609
+ int Px(float n){return (int)Math.Round(n*scale);}
610
+ int LineHeight {get{return Math.Max(Px(20),(int)Math.Ceiling(bodyFont.GetHeight())+Px(2));}}
611
+ int TotalHeight {get{return lines.Count*LineHeight;}}
612
+ int MaxScroll {get{return Math.Max(0,TotalHeight-viewport.Height);}}
613
+ internal ProgressFrame(Action<Dictionary<string,object>,Dictionary<string,object>> callback,Action onClose){
614
+ changed=callback;closeRequested=onClose;AutoScaleMode=AutoScaleMode.None;FormBorderStyle=FormBorderStyle.None;
615
+ ShowInTaskbar=false;TopMost=true;StartPosition=FormStartPosition.Manual;
616
+ MinimumSize=new Size(240,160);MaximumSize=new Size(1600,1600);
617
+ SetStyle(ControlStyles.UserPaint|ControlStyles.AllPaintingInWmPaint|ControlStyles.OptimizedDoubleBuffer|ControlStyles.ResizeRedraw,true);
618
+ SetFonts(1);LayoutBody();
619
+ }
620
+ void SetFonts(float next){
621
+ if(bodyFont!=null){bodyFont.Dispose();headingFont.Dispose();titleFont.Dispose();footerFont.Dispose();}
622
+ scale=next;bodyFont=new Font("Microsoft YaHei UI",14.667f*scale,FontStyle.Regular,GraphicsUnit.Pixel);
623
+ headingFont=new Font("Microsoft YaHei UI",14.667f*scale,FontStyle.Bold,GraphicsUnit.Pixel);
624
+ titleFont=new Font("Microsoft YaHei UI",14*scale,FontStyle.Bold,GraphicsUnit.Pixel);
625
+ footerFont=new Font("Microsoft YaHei UI",11.5f*scale,FontStyle.Regular,GraphicsUnit.Pixel);
626
+ }
627
+ void RefreshDpi(){
628
+ float next=1;try{next=Native.GetDpiForWindow(Handle)/96f;}catch(EntryPointNotFoundException){}
629
+ if(testScale>0)next=testScale;next=Math.Max(1,Math.Min(4,next));
630
+ if(Math.Abs(next-scale)>.01f){SetFonts(next);LayoutBody();}
631
+ }
632
+ protected override bool ShowWithoutActivation {get{return true;}}
633
+ protected override CreateParams CreateParams {get{var p=base.CreateParams;p.ExStyle|=0x80000|0x80|0x08000000;if(Locked)p.ExStyle|=0x20;return p;}}
634
+ protected override void OnHandleCreated(EventArgs e){base.OnHandleCreated(e);RefreshDpi();Native.SetWindowDisplayAffinity(Handle,remoteVisible?0u:0x11u);}
635
+ protected override void OnShown(EventArgs e){base.OnShown(e);Present();}
636
+ internal bool ClickThrough {get{return (Native.GetWindowLong(Handle,-20)&0x08000020)==0x08000020;}}
637
+ static string TextValue(Dictionary<string,object> data,string key){object v;if(data==null||!data.TryGetValue(key,out v)||v==null)return "";string s=Convert.ToString(v);return s.Length>1200?s.Substring(s.Length-1200):s;}
638
+ static int NumberValue(Dictionary<string,object> data,string key,int fallback){object v;return data!=null&&data.TryGetValue(key,out v)?Convert.ToInt32(v):fallback;}
639
+ internal void Configure(Dictionary<string,object> settings,bool visible){
640
+ int width=Math.Max(240,Math.Min(1600,NumberValue(settings,"progressOverlayWidth",360))),height=Math.Max(160,Math.Min(1600,NumberValue(settings,"progressOverlayHeight",240)));
641
+ int x=NumberValue(settings,"progressOverlayX",80),y=NumberValue(settings,"progressOverlayY",80),opacity=NumberValue(settings,"progressOverlayOpacity",60);
642
+ string position=TextValue(settings,"progressOverlayPosition");if(position=="")position="top-right";
643
+ object v;bool locked=settings==null||!settings.TryGetValue("progressOverlayLocked",out v)||Convert.ToBoolean(v);
644
+ string signature=width+","+height+","+x+","+y+","+position;
645
+ baseline=new Dictionary<string,object>{{"progressOverlayWidth",width},{"progressOverlayHeight",height},{"progressOverlayX",x},{"progressOverlayY",y},{"progressOverlayPosition",position},{"progressOverlayLocked",locked}};
646
+ if(signature!=geometry){
647
+ // The settings echo each size or position this window reports, about
648
+ // half a second later. An echo of an older report would move the window
649
+ // back if the user has moved or resized it again since (the settings
650
+ // keep the first of two quick reports); keep the window where it is and
651
+ // report its current layout against the settings that just arrived.
652
+ if(position=="custom"&&emitted.Exists(entry=>entry.Key==signature&&(DateTime.UtcNow-entry.Value).TotalSeconds<=10)){
653
+ geometry=signature;if(!sizing&&Bounds!=new Rectangle(x,y,width,height))EmitLayout();
654
+ } else {var area=Screen.PrimaryScreen.WorkingArea;if(position!="custom"){x=position.EndsWith("right")?area.Right-width-20:area.Left+20;y=position.StartsWith("bottom")?area.Bottom-height-20:area.Top+70;}var next=new Rectangle(x,y,width,height);if(Bounds!=next)Bounds=next;geometry=signature;}
655
+ }
656
+ if(!configured||configuredLocked!=locked){SetLocked(locked,false);configuredLocked=locked;}
657
+ configured=true;remoteVisible=visible;BackgroundOpacity=Math.Max(.2,Math.Min(1,opacity/100.0));
658
+ if(IsHandleCreated){Native.SetWindowDisplayAffinity(Handle,visible?0u:0x11u);RefreshDpi();}Present();
659
+ }
660
+ internal void BeginPreview(){previewUnlocked=true;SetLocked(false,false);}
661
+ internal void EndPreview(){if(!previewUnlocked)return;previewUnlocked=false;SetLocked(configuredLocked,false);}
662
+ // After any lock the pointer must leave the button before resting on it
663
+ // unlocks again, so clicking the open padlock (or the shortcut) holds.
664
+ internal void SetLocked(bool locked,bool notify){if(notify)previewUnlocked=false;hoverUnlocked=false;hoverBlocked=locked;hoverSince=awaySince=DateTime.MinValue;hoverFraction=0;ApplyLock(locked);if(notify)changed(new Dictionary<string,object>{{"progressOverlayLocked",locked}},new Dictionary<string,object>{{"progressOverlayLocked",configuredLocked}});}
665
+ void ApplyLock(bool locked){Locked=locked;draggingThumb=false;Capture=false;if(IsHandleCreated){int style=Native.GetWindowLong(Handle,-20);Native.SetWindowLong(Handle,-20,locked?style|0x20:style&~0x20);Native.SetWindowPos(Handle,IntPtr.Zero,0,0,0,0,0x37);}LayoutBody();Present();}
666
+ // Clicking the open padlock locks the window now. A temporary unlock (hover
667
+ // or preview) just ends; a window unlocked in the settings saves the locked
668
+ // state, as the lock shortcut does.
669
+ void LockNow(){bool save=!configuredLocked;previewUnlocked=false;SetLocked(true,save);}
670
+ internal bool WantsPointer {get{return Locked||hoverUnlocked;}}
671
+ // True while the user is adjusting the window, which then stays until they are done.
672
+ internal bool Adjusting {get{return hoverUnlocked||sizing||draggingThumb;}}
673
+ Rectangle LockZone {get{return new Rectangle(Width-Px(32),0,Px(32),Px(34));}}
674
+ Rectangle CloseZone {get{return new Rectangle(Width-Px(60),0,Px(28),Px(34));}}
675
+ // The padlock in the top-right corner is the lock button. While the window
676
+ // passes the mouse through, resting the user's own pointer on it for a
677
+ // moment unlocks the window, so it can be moved and resized; once the
678
+ // pointer has been away for a while it locks again. The saved lock setting
679
+ // is not changed, and DSH's own pointer moves never count (physical=false).
680
+ internal void TrackPointer(Native.Point cursor,bool physical){
681
+ if(IsDisposed)return;
682
+ var now=DateTime.UtcNow;var at=new Point(cursor.x-Left,cursor.y-Top);
683
+ if(Locked){
684
+ bool over=LockZone.Contains(at);
685
+ if(hoverBlocked){if(!over)hoverBlocked=false;return;}
686
+ if(!(physical&&over)){if(hoverSince!=DateTime.MinValue){hoverSince=DateTime.MinValue;hoverFraction=0;Present();}return;}
687
+ if(hoverSince==DateTime.MinValue)hoverSince=now;
688
+ float fraction=(float)Math.Min(1,(now-hoverSince).TotalMilliseconds/HoverUnlockMs);
689
+ if(fraction>=1){hoverSince=awaySince=DateTime.MinValue;hoverFraction=0;hoverUnlocked=true;ApplyLock(false);return;}
690
+ if(fraction-hoverFraction>=.04f||hoverFraction==0){hoverFraction=Math.Max(.01f,fraction);Present();}
691
+ } else if(hoverUnlocked){
692
+ var near=new Rectangle(-Px(8),-Px(8),Width+Px(16),Height+Px(16));
693
+ if(near.Contains(at)||sizing||draggingThumb){awaySince=DateTime.MinValue;return;}
694
+ if(awaySince==DateTime.MinValue)awaySince=now;
695
+ else if((now-awaySince).TotalMilliseconds>=HoverRelockMs)SetLocked(true,false);
696
+ }
697
+ }
698
+ internal object HoverStatus(){var zone=LockZone;return new {unlocked=hoverUnlocked,preview=previewUnlocked,configuredLocked=configuredLocked,blocked=hoverBlocked,dwelling=hoverSince!=DateTime.MinValue,fraction=hoverFraction,away=awaySince!=DateTime.MinValue,sizing=sizing,lockZone=new{x=zone.X,y=zone.Y,width=zone.Width,height=zone.Height}};}
699
+ internal void RequestClose(){closeRequested();}
700
+ internal void EmitLayout(){
701
+ var now=DateTime.UtcNow;string signature=Width+","+Height+","+Left+","+Top+",custom";
702
+ emitted.RemoveAll(entry=>entry.Key==signature||(now-entry.Value).TotalSeconds>10);emitted.Add(new KeyValuePair<string,DateTime>(signature,now));
703
+ changed(new Dictionary<string,object>{{"progressOverlayWidth",Width},{"progressOverlayHeight",Height},{"progressOverlayPosition","custom"},{"progressOverlayX",Left},{"progressOverlayY",Top},{"progressOverlayLocked",previewUnlocked||hoverUnlocked?configuredLocked:Locked}},baseline);
704
+ }
705
+ // The reading area holds whole lines only, so no line is ever cut in half
706
+ // at its top or bottom edge. A locked window has no footer line.
707
+ void LayoutBody(){
708
+ if(bodyFont==null)return;
709
+ int top=Px(subheader.Length==0?39:60),lineHeight=LineHeight;
710
+ int available=Math.Max(lineHeight,ClientSize.Height-top-(Locked?Px(8):Px(26)));
711
+ viewport=new Rectangle(Px(12),top,Math.Max(24,ClientSize.Width-Px(36)),Math.Max(1,available/lineHeight)*lineHeight);
712
+ lines.Clear();
713
+ using(var image=new Bitmap(1,1))using(var g=Graphics.FromImage(image))using(var format=(StringFormat)StringFormat.GenericTypographic.Clone()){
714
+ format.FormatFlags|=StringFormatFlags.MeasureTrailingSpaces;
715
+ int widest=Math.Max(8,(int)(viewport.Width/Math.Max(1f,bodyFont.Size*.2f)));
716
+ foreach(string paragraph in bodyText.Replace("\r","").Split('\n')){
717
+ bool heading=Array.IndexOf(headings,paragraph)>=0;Font font=heading?headingFont:bodyFont;
718
+ if(paragraph.Length==0){lines.Add(new ReadingLine("",false));continue;}
719
+ int at=0;while(at<paragraph.Length){
720
+ int lo=1,hi=Math.Min(paragraph.Length-at,widest),best=1;
721
+ while(lo<=hi){int mid=(lo+hi)/2;float width=g.MeasureString(paragraph.Substring(at,mid),font,Int32.MaxValue,format).Width;if(width<=viewport.Width){best=mid;lo=mid+1;}else hi=mid-1;}
722
+ // Keep surrogate pairs and ordinary Latin words together when wrapping.
723
+ if(at+best<paragraph.Length&&best>1&&Char.IsHighSurrogate(paragraph[at+best-1]))best--;
724
+ if(at+best<paragraph.Length&&best>1&&",。!?;:、)】》…,.!?;:)".IndexOf(paragraph[at+best])>=0)best--;
725
+ if(at+best<paragraph.Length&&Char.IsLetterOrDigit(paragraph[at+best])&&paragraph[at+best]<128){int space=paragraph.LastIndexOf(' ',at+best-1,best);if(space>at+best/2)best=space-at+1;}
726
+ lines.Add(new ReadingLine(paragraph.Substring(at,best),heading));at+=best;
727
+ }
728
+ }
729
+ }
730
+ scrollY=followOutput?MaxScroll:Snap(Math.Min(scrollY,MaxScroll));LayoutScrollbar();
731
+ }
732
+ int Snap(int y){int lineHeight=Math.Max(1,LineHeight);return (int)Math.Round(y/(double)lineHeight)*lineHeight;}
733
+ void LayoutScrollbar(){track=new Rectangle(Width-Px(16),viewport.Y,Px(6),viewport.Height);int height=Math.Min(track.Height,Math.Max(Px(26),(int)((long)track.Height*viewport.Height/Math.Max(1,TotalHeight))));thumb=new Rectangle(track.X,track.Y+(MaxScroll==0?0:(int)((long)(track.Height-height)*scrollY/MaxScroll)),track.Width,height);}
734
+ void ScrollTo(int y){scrollY=Math.Max(0,Math.Min(MaxScroll,Snap(y)));followOutput=scrollY>=MaxScroll-2;LayoutScrollbar();Present();}
735
+ protected override void OnResize(EventArgs e){base.OnResize(e);LayoutBody();Present();}
736
+ protected override void OnLocationChanged(EventArgs e){base.OnLocationChanged(e);if(IsHandleCreated){RefreshDpi();Present();}}
737
+ protected override void OnResizeEnd(EventArgs e){base.OnResizeEnd(e);if(!Locked)EmitLayout();}
738
+ protected override void OnMouseDown(MouseEventArgs e){
739
+ if(!Locked&&e.Button==MouseButtons.Left){if(LockZone.Contains(e.Location)){LockNow();return;}if(CloseZone.Contains(e.Location)){closeRequested();return;}if(e.X>=track.X-Px(6)&&e.Y>=track.Top&&e.Y<track.Bottom&&MaxScroll>0){if(thumb.Contains(e.Location)){draggingThumb=true;dragOffset=e.Y-thumb.Top;Capture=true;}else ScrollTo(scrollY+(e.Y<thumb.Top?-viewport.Height:viewport.Height));return;}}
740
+ base.OnMouseDown(e);
741
+ }
742
+ protected override void OnMouseMove(MouseEventArgs e){if(draggingThumb&&!Locked)ScrollTo((int)((long)(e.Y-dragOffset-track.Top)*MaxScroll/Math.Max(1,track.Height-thumb.Height)));base.OnMouseMove(e);}
743
+ protected override void OnMouseUp(MouseEventArgs e){draggingThumb=false;Capture=false;base.OnMouseUp(e);}
744
+ protected override void OnMouseCaptureChanged(EventArgs e){if(!Capture)draggingThumb=false;base.OnMouseCaptureChanged(e);}
745
+ protected override void WndProc(ref Message m){
746
+ if(m.Msg==0x21){m.Result=new IntPtr(3);return;}
747
+ if(m.Msg==0x020a){if(!Locked)ScrollWheel((short)(m.WParam.ToInt64()>>16));m.Result=IntPtr.Zero;return;}
748
+ if(m.Msg==0x02e0){base.WndProc(ref m);RefreshDpi();Present();return;}
749
+ if(m.Msg==0x0231)sizing=true;else if(m.Msg==0x0232)sizing=false;
750
+ if(m.Msg==0x84){if(Locked){m.Result=new IntPtr(-1);return;}long value=m.LParam.ToInt64();int x=(short)(value&0xffff)-Left,y=(short)((value>>16)&0xffff)-Top;int edge=Px(6);if(x>=Width-Px(60)&&y<Px(34)){m.Result=new IntPtr(1);return;}int hit=y<edge?(x<edge?13:x>=Width-edge?14:12):y>=Height-edge?(x<edge?16:x>=Width-edge?17:15):x<edge?10:x>=Width-edge?11:y<Px(35)?2:1;m.Result=new IntPtr(hit);return;}
751
+ base.WndProc(ref m);
752
+ }
753
+ static List<Dictionary<string,object>> Items(Dictionary<string,object> data,string key){
754
+ var items=new List<Dictionary<string,object>>();object value;
755
+ if(data==null||!data.TryGetValue(key,out value)||value==null||value is string)return items;
756
+ var list=value as System.Collections.IEnumerable;if(list==null)return items;
757
+ foreach(object item in list){var entry=item as Dictionary<string,object>;if(entry!=null)items.Add(entry);}
758
+ return items;
759
+ }
760
+ static bool Flag(Dictionary<string,object> data,string key){object v;if(data==null||!data.TryGetValue(key,out v)||v==null)return false;try{return Convert.ToBoolean(v);}catch(FormatException){return false;}}
761
+ static long Whole(Dictionary<string,object> data,string key){object v;if(data==null||!data.TryGetValue(key,out v)||v==null)return 0;try{return Convert.ToInt64(v);}catch(FormatException){return 0;}catch(InvalidCastException){return 0;}catch(OverflowException){return 0;}}
762
+ static string Shorten(string text,int limit){text=text.Trim();return text.Length>limit?"…"+text.Substring(text.Length-limit+1):text;}
763
+ // DSH renders Markdown, so plain text shows the same words without the
764
+ // marks; paragraphs follow each other without blank lines in a small window.
765
+ static string Readable(string text){
766
+ text=(text??"").Replace("\r","");
767
+ text=System.Text.RegularExpressions.Regex.Replace(text,@"(?m)^[ \t]*```[^\n]*\n?","");
768
+ text=System.Text.RegularExpressions.Regex.Replace(text,@"!\[([^\]\n]*)\]\([^)\n]*\)",match=>match.Groups[1].Value.Trim()==""?"(image)":"(image: "+match.Groups[1].Value.Trim()+")");
769
+ text=System.Text.RegularExpressions.Regex.Replace(text,@"\[([^\]\n]+)\]\([^)\n]*\)","$1");
770
+ text=System.Text.RegularExpressions.Regex.Replace(text,@"(\*\*|__)(?=\S)(.+?)(?<=\S)\1","$2");
771
+ text=System.Text.RegularExpressions.Regex.Replace(text,@"`([^`\n]+)`","$1");
772
+ text=System.Text.RegularExpressions.Regex.Replace(text,@"(?m)^[ \t]{0,3}#{1,6}[ \t]+","");
773
+ text=System.Text.RegularExpressions.Regex.Replace(text,@"(?m)^([ \t]*)[-*+][ \t]+","$1• ");
774
+ text=System.Text.RegularExpressions.Regex.Replace(text,@"\n[ \t]*(?:\n[ \t]*)+","\n");
775
+ return text.Trim();
776
+ }
777
+ // When a task ends with a reply longer than the window, show the reply from
778
+ // its first line (the result) rather than its last; a reader who scrolled
779
+ // away keeps their place.
780
+ void RevealResult(bool finished){
781
+ if(!finished){revealedResult=false;return;}
782
+ if(revealedResult||!followOutput)return;
783
+ int at=-1;for(int i=lines.Count-1;i>=0;i--)if(lines[i].heading&&lines[i].text=="Reply"){at=i;break;}
784
+ if(at<0)return;
785
+ revealedResult=true;
786
+ if(TotalHeight-at*LineHeight>viewport.Height){scrollY=Math.Min(MaxScroll,at*LineHeight);followOutput=false;LayoutScrollbar();}
787
+ }
788
+ // One heading per model step ("Step 3 · scroll"), then the text DSH showed
789
+ // for that step. Older steps keep only their last lines; the current step
790
+ // and the reply are shown in full.
791
+ static string StepHeading(Dictionary<string,object> entry){
792
+ var labels=new List<string>();
793
+ foreach(var action in Items(entry,"actions")){string label=TextValue(action,"label");if(label!="")labels.Add(label+(TextValue(action,"state")=="error"?" (error)":""));}
794
+ return "Step "+TextValue(entry,"step")+(labels.Count>0?" · "+String.Join(", ",labels.ToArray()):"");
795
+ }
796
+ internal void UpdateProgress(Dictionary<string,object> data){
797
+ string status=TextValue(data,"status");
798
+ string step=TextValue(data,"step");if(step==""||step=="0")step="1";
799
+ long started=Whole(data,"startedAt"),ended=Whole(data,"endedAt");
800
+ bool finished=ended>0&&(status=="done"||status=="failed"||status=="stopped");
801
+ string took=started>0&&ended>=started?" · "+Math.Max(1,(long)Math.Round((ended-started)/1000.0))+"s":"";
802
+ if(finished)header=status=="done"?Agent+" · Done · "+step+" steps"+took:status=="failed"?Agent+" · Error · step "+step:Agent+" · Stopped · step "+step;
803
+ else header=Agent+" · Step "+step+" · "+(status=="thinking"?"thinking":status=="acting"?"acting":status=="replying"?"replying":status=="stopped"?"stopped":"working");
804
+ string action=TextValue(data,"action");if(action=="")action=ToolLabel(TextValue(data,"tool"));
805
+ subheader=finished?"":action;
806
+ var steps=Items(data,"steps");var text=new List<string>();var heads=new List<string>();
807
+ Action<string> head=delegate(string value){text.Add(value);heads.Add(value);};
808
+ if(TextValue(data,"displayMode")=="summary"){
809
+ // Summary mode: the AI summaries in order, newest last. Until the first
810
+ // one arrives (or when the model is unavailable) the step names stand in.
811
+ var summaries=Items(data,"summaries");string latest="";
812
+ foreach(var item in summaries){string value=Readable(TextValue(item,"text"));if(value==""||value==latest)continue;head(Flag(item,"final")?"AI summary · result":"AI summary · step "+TextValue(item,"step"));text.Add(value);latest=value;}
813
+ bool finalSummary=summaries.Count>0&&Flag(summaries[summaries.Count-1],"final");
814
+ if(latest==""){
815
+ foreach(var entry in steps)head(StepHeading(entry));
816
+ string summaryStatus=TextValue(data,"summaryStatus");
817
+ text.Add(summaryStatus=="unavailable"?"Summary unavailable — showing the actions instead.":finished?"Summarizing the result…":"Generating a summary…");
818
+ }
819
+ string reply=Readable(steps.Count>0?TextValue(steps[steps.Count-1],"reply"):TextValue(data,"reply"));
820
+ if(finished&&!finalSummary&&reply!=""&&TextValue(data,"summaryStatus")!="working"){head("Reply");text.Add(reply);}
821
+ } else if(steps.Count>0){
822
+ int shown=Math.Max(0,steps.Count-12);
823
+ for(int i=shown;i<steps.Count;i++){
824
+ var entry=steps[i];bool current=i==steps.Count-1;head(StepHeading(entry));
825
+ string reasoning=Readable(TextValue(entry,"reasoning")),reply=Readable(TextValue(entry,"reply"));
826
+ if(reasoning!="")text.Add(current?reasoning:Shorten(reasoning,240));
827
+ if(reply!=""){head("Reply");text.Add(reply);}
828
+ }
829
+ } else {
830
+ string reasoning=Readable(TextValue(data,"reasoning")),reply=Readable(TextValue(data,"reply"));
831
+ if(reasoning!=""){head("Thinking");text.Add(reasoning);}
832
+ if(reply!=""){head("Reply");text.Add(reply);}
833
+ }
834
+ bodyText=text.Count>0?String.Join("\n",text.ToArray()):"Waiting for "+Agent+" output…";headings=heads.ToArray();
835
+ Content=header+"\n"+subheader+"\n\n"+bodyText;LayoutBody();RevealResult(finished);Present();
836
+ }
837
+ static string ToolLabel(string tool){switch(tool){case "snapshot":return "read the screen";case "screenshot":return "screenshot";case "list_windows":return "list windows";case "launch":return "open an app";case "scroll":return "scroll";case "click":return "click";case "set_value":return "set a value";case "type":return "type text";case "press":return "press keys";case "wait":return "wait";case "batch":return "run a batch";case "find":return "find a control";case "get":return "read a control";case "is":return "check a control";case "ocr":return "read screen text";default:return tool;}}
838
+ // A dark halo under light text keeps it readable when a bright page shows
839
+ // through the translucent background.
840
+ static void Shadowed(Graphics g,string text,Font font,Brush brush,Brush shadow,RectangleF where,StringFormat format,float offset){
841
+ g.DrawString(text,font,shadow,new RectangleF(where.X+offset,where.Y+offset,where.Width,where.Height),format);g.DrawString(text,font,brush,where,format);
842
+ }
843
+ // The lock button stays in the top-right corner: a closed padlock while
844
+ // the mouse passes through (a ring fills while the pointer rests on it),
845
+ // an open one (click to lock) with Close beside it while unlocked.
846
+ void DrawButtons(Graphics g,Pen closePen){
847
+ float cx=Width-scale*19,cy=scale*18,r=scale*12;var disc=new RectangleF(cx-r,cy-r,2*r,2*r);
848
+ using(var brush=new SolidBrush(Color.FromArgb(Locked?(hoverFraction>0?76:34):52,220,229,242)))g.FillEllipse(brush,disc);
849
+ if(Locked&&hoverFraction>0)using(var ring=new Pen(Color.FromArgb(245,122,180,255),Math.Max(1.5f,scale*2))){g.DrawArc(ring,disc.X+scale,disc.Y+scale,disc.Width-2*scale,disc.Height-2*scale,-90,360*hoverFraction);}
850
+ Color ink=Locked?Color.FromArgb(220,196,208,224):Color.FromArgb(245,238,242,248);
851
+ float lift=Locked?0:scale*3;
852
+ using(var pen=new Pen(ink,Math.Max(1f,scale*1.3f)))using(var fill=new SolidBrush(ink)){
853
+ g.DrawArc(pen,cx-scale*3.5f,cy-scale*7-lift,scale*7,scale*9,180,180);
854
+ if(lift>0)g.DrawLine(pen,cx-scale*3.5f,cy-scale*2.5f-lift,cx-scale*3.5f,cy-scale*2);
855
+ using(var body=Frame.Rounded(new RectangleF(cx-scale*5.5f,cy-scale*2,scale*11,scale*8),Math.Max(1,scale*1.5f)))g.FillPath(fill,body);
856
+ }
857
+ if(!Locked){float x=Width-scale*46,y=scale*17.5f,d=scale*4.5f;g.DrawLine(closePen,x-d,y-d,x+d,y+d);g.DrawLine(closePen,x-d,y+d,x+d,y-d);}
858
+ }
859
+ Bitmap DrawSurface(){
860
+ var image=new Bitmap(Math.Max(1,Width),Math.Max(1,Height),PixelFormat.Format32bppPArgb);
861
+ using(var g=Graphics.FromImage(image)){
862
+ g.Clear(Color.Transparent);g.SmoothingMode=SmoothingMode.AntiAlias;g.TextRenderingHint=TextRenderingHint.AntiAliasGridFit;
863
+ using(var shape=Frame.Rounded(new RectangleF(.5f,.5f,Width-1,Height-1),Px(8)))using(var bg=new SolidBrush(Color.FromArgb((int)Math.Round(BackgroundOpacity*255),25,29,38)))using(var border=new Pen(Color.FromArgb(140,117,161,218),1)){g.FillPath(bg,shape);g.DrawPath(border,shape);}
864
+ using(var fg=new SolidBrush(Color.FromArgb(255,246,247,250)))using(var muted=new SolidBrush(Color.FromArgb(245,185,198,216)))using(var shadow=new SolidBrush(Color.FromArgb(150,0,0,0)))using(var pen=new Pen(Color.FromArgb(235,203,216,232),Px(1.3f)))using(var format=(StringFormat)StringFormat.GenericTypographic.Clone()){
865
+ format.FormatFlags|=StringFormatFlags.NoWrap|StringFormatFlags.MeasureTrailingSpaces;format.Trimming=StringTrimming.EllipsisCharacter;
866
+ float halo=Math.Max(1f,scale*.75f);
867
+ Shadowed(g,header,titleFont,fg,shadow,new RectangleF(Px(12),Px(10),Math.Max(10,Width-Px(Locked?52:68)),Px(24)),format,halo);
868
+ DrawButtons(g,pen);
869
+ if(subheader!="")Shadowed(g,subheader,footerFont,muted,shadow,new RectangleF(Px(12),Px(36),Math.Max(10,Width-Px(24)),Px(21)),format,halo);
870
+ var clip=g.Save();g.SetClip(viewport);int first=scrollY/LineHeight,last=Math.Min(lines.Count,(scrollY+viewport.Height)/LineHeight+1);
871
+ for(int i=first;i<last;i++){var line=lines[i];Font font=line.heading?headingFont:bodyFont;Brush brush=line.heading?muted:fg;float x=viewport.X,y=viewport.Y+i*LineHeight-scrollY;g.DrawString(line.text,font,shadow,new PointF(x+halo,y+halo),format);g.DrawString(line.text,font,brush,new PointF(x,y),format);}
872
+ g.Restore(clip);
873
+ if(!Locked)Shadowed(g,hoverUnlocked?"drag the title to move · edges to resize · locks when idle":"drag the title to move · edges to resize · scroll to view",footerFont,muted,shadow,new RectangleF(Px(12),Height-Px(22),Math.Max(10,Width-Px(24)),Px(20)),format,halo);
874
+ }
875
+ // The translucent scrollbar appears only when there is more to read.
876
+ if(MaxScroll>0){
877
+ using(var shape=Frame.Rounded(track,Math.Max(1,Px(3))))using(var brush=new SolidBrush(Color.FromArgb(32,220,229,242)))g.FillPath(brush,shape);
878
+ using(var shape=Frame.Rounded(thumb,Math.Max(1,Px(3))))using(var brush=new SolidBrush(Color.FromArgb(112,220,229,242)))g.FillPath(brush,shape);
879
+ }
880
+ }return image;
881
+ }
882
+ void Present(){
883
+ if(presenting||!IsHandleCreated||IsDisposed||Width<1||Height<1)return;presenting=true;
884
+ try{using(var image=DrawSurface()){IntPtr dc=Native.CreateCompatibleDC(IntPtr.Zero),bitmap=image.GetHbitmap(Color.FromArgb(0)),previous=Native.SelectObject(dc,bitmap);try{var dest=new Native.Point(Left,Top);var origin=new Native.Point(0,0);var size=new Native.Size(Width,Height);var blend=new Native.Blend{op=0,flags=0,alpha=255,format=1};if(!Native.UpdateLayeredWindow(Handle,IntPtr.Zero,ref dest,ref size,dc,ref origin,0,ref blend,2))throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error(),"Could not present progress overlay");paints++;}finally{Native.SelectObject(dc,previous);Native.DeleteObject(bitmap);Native.DeleteDC(dc);}}}finally{presenting=false;}
885
+ }
886
+ protected override void OnPaint(PaintEventArgs e){Present();}
887
+ internal object ReadingStatus(){return new {scrollbar=true,position=scrollY,maximum=Math.Max(0,TotalHeight-1),page=viewport.Height,atBottom=scrollY>=MaxScroll-2,following=followOutput,text=bodyText,lineHeight=LineHeight,emPixels=bodyFont.Size,viewport=new{x=viewport.X,y=viewport.Y,width=viewport.Width,height=viewport.Height},resizeRedraw=GetStyle(ControlStyles.ResizeRedraw),paints=paints,dpiScale=scale,textAlpha=255,backgroundAlpha=(int)Math.Round(BackgroundOpacity*255),scrollbarTrackAlpha=MaxScroll>0?32:0,scrollbarThumbAlpha=MaxScroll>0?112:0,thumb=new{x=thumb.X,y=thumb.Y,width=thumb.Width,height=thumb.Height},wrappedLines=lines.Count};}
888
+ internal void ScrollLines(int count){ScrollTo(scrollY+count*LineHeight);}
889
+ internal void ScrollWheel(int delta){wheelRemainder+=delta;int notches=wheelRemainder/120;wheelRemainder%=120;if(notches!=0){int n=SystemInformation.MouseWheelScrollLines;ScrollTo(scrollY-notches*(n<0?viewport.Height:Math.Max(1,n)*LineHeight));}}
890
+ internal int TestHitAt(int x,int y){return Native.SendMessage(Handle,0x0084,IntPtr.Zero,new IntPtr(((Top+y)&0xffff)<<16|((Left+x)&0xffff))).ToInt32();}
891
+ internal void TestCloseClick(){Native.SendMessage(Handle,0x0201,new IntPtr(1),new IntPtr((Px(18)<<16)|(Width-Px(46))));}
892
+ internal void TestLockClick(){Native.SendMessage(Handle,0x0201,new IntPtr(1),new IntPtr((Px(18)<<16)|(Width-Px(18))));}
893
+ internal void TestResizeRedraw(bool enabled){SetStyle(ControlStyles.ResizeRedraw,enabled);}
894
+ internal void TestDpi(float next){testScale=next;RefreshDpi();Present();}
895
+ internal void TestScrollbar(int x,int y,int endY){OnMouseDown(new MouseEventArgs(MouseButtons.Left,1,x,y,0));OnMouseMove(new MouseEventArgs(MouseButtons.Left,0,x,endY,0));OnMouseUp(new MouseEventArgs(MouseButtons.Left,1,x,endY,0));}
896
+ sealed class TestBackdrop : Form {protected override bool ShowWithoutActivation {get{return true;}}}
897
+ internal void ShowTestBackdrop(){if(testBackdrop==null){testBackdrop=new TestBackdrop{FormBorderStyle=FormBorderStyle.None,ShowInTaskbar=false,TopMost=true,BackColor=Color.FromArgb(40,40,40),StartPosition=FormStartPosition.Manual,Bounds=new Rectangle(Left-4,Top-4,1000,900)};testBackdrop.Show();Native.KeepOnTop(Handle);}}
898
+ internal object CaptureProgress(string path,bool screen){
899
+ if(screen&&testBackdrop==null)throw new InvalidOperationException("Screen capture requires a synthetic backdrop");
900
+ using(var bitmap=DrawSurface()){if(screen)using(var g=Graphics.FromImage(bitmap)){g.CompositingMode=CompositingMode.SourceCopy;g.CopyFromScreen(Left,Top,0,0,new Size(Width,Height));}int stray=0;for(int y=Px(8);y<Math.Min(Px(34),Height);y++)for(int x=Px(12);x<Width-Px(55);x++){Color c=bitmap.GetPixel(x,y);if(c.B-c.R>65&&c.B-c.G>20)stray++;}bitmap.Save(path,ImageFormat.Png);return new {strayHeaderPixels=stray,width=Width,height=Height};}
901
+ }
902
+ protected override void Dispose(bool disposing){if(disposing){if(testBackdrop!=null)testBackdrop.Dispose();if(bodyFont!=null){bodyFont.Dispose();headingFont.Dispose();titleFont.Dispose();footerFont.Dispose();}}base.Dispose(disposing);}
903
+ }
904
+
905
+
906
+ // A small cursor-following layer. The system cursor, theme and input state
907
+ // remain owned by Windows; closing/crashing this process leaves no cursor changes.
908
+ internal sealed class PointerFrame : Form {
909
+ IntPtr dc, bitmap, previous;
910
+ float scale;
911
+ bool flipX, flipY;
912
+ Rectangle lastBounds;
913
+ byte lastAlpha;
914
+ SizeF lastExtent;
915
+ bool remoteVisible;
916
+ internal PointerFrame(bool visible) {
917
+ remoteVisible=visible;
918
+ AutoScaleMode = AutoScaleMode.None; FormBorderStyle = FormBorderStyle.None;
919
+ ShowInTaskbar = false; TopMost = true; StartPosition = FormStartPosition.Manual;
920
+ Bounds = new Rectangle(0,0,1,1);
921
+ dc = Native.CreateCompatibleDC(IntPtr.Zero);
922
+ }
923
+ protected override bool ShowWithoutActivation { get { return true; } }
924
+ protected override CreateParams CreateParams {
925
+ get { var value=base.CreateParams; value.ExStyle|=0x80000|0x20|0x80|0x08000000; return value; }
926
+ }
927
+ protected override void OnHandleCreated(EventArgs e) { base.OnHandleCreated(e); SetRemoteVisible(remoteVisible); }
928
+ internal void SetRemoteVisible(bool visible) {remoteVisible=visible;if(IsHandleCreated)Native.SetWindowDisplayAffinity(Handle,visible?0u:0x11u);}
929
+ internal bool ClickThrough { get { return (Native.GetWindowLong(Handle,-20)&0x08080020)==0x08080020; } }
930
+ internal bool CaptureExcluded { get { uint affinity; return Native.GetWindowDisplayAffinity(Handle,out affinity)&&affinity==0x11; } }
931
+ // Geometry in 96-DPI units. The tag sits just past the pointer's visible
932
+ // extent; the canvas grows with it, and the default arrow keeps 120x84.
933
+ static float TagX(SizeF extent) { return Math.Max(13f,extent.Width+2); }
934
+ static float TagY(SizeF extent) { return Math.Max(17f,extent.Height-1); }
935
+ static float CanvasWidth(SizeF extent) { return Math.Max(120f,24+TagX(extent)+48); }
936
+ static float CanvasHeight(SizeF extent) { return Math.Max(84f,24+TagY(extent)+27); }
937
+ static bool FlipX(Native.Point point, Rectangle screen, float zoom, SizeF extent) { return screen.Right-point.x < (CanvasWidth(extent)-24)*zoom; }
938
+ static bool FlipY(Native.Point point, Rectangle screen, float zoom, SizeF extent) { return screen.Bottom-point.y < (CanvasHeight(extent)-18)*zoom; }
939
+ internal static Rectangle LayoutForPoint(Native.Point point, Rectangle screen, float zoom) { return LayoutForPoint(point,screen,zoom,SizeF.Empty); }
940
+ internal static Rectangle LayoutForPoint(Native.Point point, Rectangle screen, float zoom, SizeF extent) {
941
+ float canvasWidth=CanvasWidth(extent),canvasHeight=CanvasHeight(extent);
942
+ int width=(int)Math.Ceiling(canvasWidth*zoom),height=(int)Math.Ceiling(canvasHeight*zoom);
943
+ int centerX=(int)Math.Round((FlipX(point,screen,zoom,extent)?canvasWidth-24:24)*zoom),centerY=(int)Math.Round((FlipY(point,screen,zoom,extent)?canvasHeight-24:24)*zoom);
944
+ return new Rectangle(point.x-centerX,point.y-centerY,width,height);
945
+ }
946
+ // The user's pointer size: the arrow's visible extent past its hotspot, in
947
+ // 96-DPI units. Measured once, and again only after Windows reports a
948
+ // preference change (pointer size or cursor scheme).
949
+ // The ghost cursor tag text; set from the plugin settings (default "Arya").
950
+ internal static string Label = "Arya";
951
+ static SizeF? pointerExtent;
952
+ internal static void ForgetPointerSize() { pointerExtent=null; }
953
+ internal static SizeF PointerExtent() {
954
+ if(!pointerExtent.HasValue) {
955
+ var measured=MeasureCursor(Native.LoadCursor(IntPtr.Zero,new IntPtr(32512)));
956
+ float dpi=96; try { dpi=Native.GetDpiForSystem(); } catch(EntryPointNotFoundException) { }
957
+ // Windows enlarges the accessibility pointer when drawing it, not in the
958
+ // arrow image; SPI_GETCURSORBASESIZE reports that size (32 by default).
959
+ int baseSize; if(!Native.SystemParametersInfo(0x2028,0,out baseSize,0)||baseSize<32)baseSize=32;
960
+ float factor=Math.Min(8f,baseSize/32f)/Math.Max(1f,dpi/96f);
961
+ pointerExtent=new SizeF(measured.Width*factor,measured.Height*factor);
962
+ }
963
+ return pointerExtent.Value;
964
+ }
965
+ // Visible pixels past the hotspot, in physical pixels.
966
+ internal static SizeF MeasureCursor(IntPtr cursor) {
967
+ Native.IconInfo info;
968
+ if(cursor==IntPtr.Zero||!Native.GetIconInfo(cursor,out info))return SizeF.Empty;
969
+ try {
970
+ using(var icon=Icon.FromHandle(cursor))
971
+ using(var image=icon.ToBitmap()) {
972
+ var data=image.LockBits(new Rectangle(0,0,image.Width,image.Height),ImageLockMode.ReadOnly,PixelFormat.Format32bppArgb);
973
+ int right=-1,bottom=-1;
974
+ try {
975
+ int row=Math.Abs(data.Stride)/4;var pixels=new int[row*image.Height];
976
+ Marshal.Copy(data.Scan0,pixels,0,pixels.Length);
977
+ for(int y=0;y<image.Height;y++)for(int x=0;x<image.Width;x++)if(((pixels[y*row+x]>>24)&0xff)>40){if(x>right)right=x;if(y>bottom)bottom=y;}
978
+ } finally { image.UnlockBits(data); }
979
+ // A monochrome pointer draws without alpha; size it from its cell like the arrow.
980
+ if(right<0)return new SizeF(Math.Max(0,image.Width*.375f-info.hotX),Math.Max(0,image.Height*.6f-info.hotY));
981
+ return new SizeF(Math.Max(0,right-info.hotX+1),Math.Max(0,bottom-info.hotY+1));
982
+ }
983
+ } catch(ArgumentException) { return SizeF.Empty; }
984
+ catch(ExternalException) { return SizeF.Empty; }
985
+ finally {
986
+ if(info.mask!=IntPtr.Zero)Native.DeleteObject(info.mask);
987
+ if(info.color!=IntPtr.Zero)Native.DeleteObject(info.color);
988
+ }
989
+ }
990
+ internal void Update(Native.Point point, byte alpha) {
991
+ var screen=Native.DisplayFromPoint(point);
992
+ uint dpiX=96,dpiY=96; try { if(Native.GetDpiForMonitor(Native.MonitorFromPoint(point,2),0,out dpiX,out dpiY)!=0)dpiX=96; } catch(EntryPointNotFoundException) { }
993
+ float zoom=Math.Max(1f,Math.Min(3f,dpiX/96f));
994
+ var extent=PointerExtent();
995
+ bool nextFlipX=FlipX(point,screen,zoom,extent),nextFlipY=FlipY(point,screen,zoom,extent);
996
+ bool redraw=bitmap==IntPtr.Zero||Math.Abs(scale-zoom)>.01f||flipX!=nextFlipX||flipY!=nextFlipY||extent!=lastExtent;
997
+ if(redraw) {
998
+ using(var image=Draw(zoom,nextFlipX,nextFlipY,extent)) {
999
+ IntPtr next=image.GetHbitmap(Color.FromArgb(0));
1000
+ IntPtr selected=Native.SelectObject(dc,next);
1001
+ if(bitmap==IntPtr.Zero)previous=selected; else Native.DeleteObject(bitmap);
1002
+ bitmap=next;
1003
+ }
1004
+ scale=zoom;flipX=nextFlipX;flipY=nextFlipY;lastExtent=extent;
1005
+ }
1006
+ var bounds=LayoutForPoint(point,screen,zoom,extent);
1007
+ if(!redraw&&bounds==lastBounds&&alpha==lastAlpha)return;
1008
+ var destination=new Native.Point(bounds.X,bounds.Y);var origin=new Native.Point(0,0);var size=new Native.Size(bounds.Width,bounds.Height);
1009
+ var blend=new Native.Blend { op=0,flags=0,alpha=alpha,format=1 };
1010
+ if(!Native.UpdateLayeredWindow(Handle,IntPtr.Zero,ref destination,ref size,dc,ref origin,0,ref blend,2))throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error(),"Could not present pointer indicator");
1011
+ lastBounds=bounds;lastAlpha=alpha;
1012
+ }
1013
+ internal static Bitmap Draw(float zoom,bool left,bool up) { return Draw(zoom,left,up,SizeF.Empty); }
1014
+ internal static Bitmap Draw(float zoom,bool left,bool up,SizeF extent) {
1015
+ float canvasWidth=CanvasWidth(extent),canvasHeight=CanvasHeight(extent);
1016
+ var image=new Bitmap((int)Math.Ceiling(canvasWidth*zoom),(int)Math.Ceiling(canvasHeight*zoom),PixelFormat.Format32bppPArgb);
1017
+ using(var g=Graphics.FromImage(image)) {
1018
+ g.Clear(Color.Transparent);g.SmoothingMode=SmoothingMode.AntiAlias;g.TextRenderingHint=TextRenderingHint.AntiAliasGridFit;g.ScaleTransform(zoom,zoom);
1019
+ float x=left?canvasWidth-24:24,y=up?canvasHeight-24:24;
1020
+ // A soft spotlight under the tip instead of a hard ring.
1021
+ using(var spot=new GraphicsPath()) {
1022
+ spot.AddEllipse(x-16,y-16,32,32);
1023
+ using(var glow=new PathGradientBrush(spot) { CenterColor=Color.FromArgb(150,86,134,254), SurroundColors=new[] { Color.FromArgb(0,86,134,254) } }) g.FillPath(glow,spot);
1024
+ }
1025
+ // The synthetic cursor itself: a collaborative-style arrow whose tip sits
1026
+ // on the acted control. The user's own pointer is never drawn or followed.
1027
+ using(var arrow=new GraphicsPath()) {
1028
+ const float k=1.7f;
1029
+ arrow.AddPolygon(new[] {
1030
+ new PointF(x,y), new PointF(x,y+15.5f*k), new PointF(x+4.2f*k,y+11.6f*k),
1031
+ new PointF(x+7.1f*k,y+18.2f*k), new PointF(x+10.2f*k,y+16.8f*k), new PointF(x+7.3f*k,y+10.3f*k),
1032
+ new PointF(x+12.6f*k,y+10.1f*k)
1033
+ });
1034
+ using(var shadow=new SolidBrush(Color.FromArgb(70,0,0,0))) {
1035
+ g.TranslateTransform(1.6f,1.8f);g.FillPath(shadow,arrow);g.TranslateTransform(-1.6f,-1.8f);
1036
+ }
1037
+ using(var fill=new SolidBrush(Color.FromArgb(255,255,255,255))) g.FillPath(fill,arrow);
1038
+ using(var edge=new Pen(Color.FromArgb(255,57,100,254),1.8f)) g.DrawPath(edge,arrow);
1039
+ }
1040
+ // A solid name tag just past the arrow, like collaborative cursors.
1041
+ using(var font=new Font("Segoe UI",10,FontStyle.Bold,GraphicsUnit.Pixel))
1042
+ using(var format=new StringFormat(StringFormat.GenericTypographic) { Alignment=StringAlignment.Center, LineAlignment=StringAlignment.Center }) {
1043
+ float w=(float)Math.Ceiling(g.MeasureString(Label,font,PointF.Empty,format).Width)+14,h=19;
1044
+ // Below-right clears the pointer's whole extent; the other corners only need to clear the tip.
1045
+ float bx=left?x-8-w:up?x+13:x+TagX(extent),by=up?y-10-h:left?y+17:y+TagY(extent);
1046
+ using(var shadow=Frame.Rounded(new RectangleF(bx,by+1.5f,w,h),6)) using(var brush=new SolidBrush(Color.FromArgb(60,0,0,0))) g.FillPath(brush,shadow);
1047
+ using(var shape=TagShape(new RectangleF(bx,by,w,h),left,up)) {
1048
+ using(var fill=new SolidBrush(Color.FromArgb(255,57,100,254)))g.FillPath(fill,shape);
1049
+ using(var outline=new Pen(Color.FromArgb(240,255,255,255),1.25f))g.DrawPath(outline,shape);
1050
+ }
1051
+ using(var brush=new SolidBrush(Color.White))g.DrawString(Label,font,brush,new RectangleF(bx,by+.5f,w,h),format);
1052
+ }
1053
+ }
1054
+ return image;
1055
+ }
1056
+ // The corner nearest the pointer stays tight, so the tag reads as attached.
1057
+ static GraphicsPath TagShape(RectangleF r,bool left,bool up) {
1058
+ float topLeft=!left&&!up?2:6,topRight=left&&!up?2:6,bottomRight=left&&up?2:6,bottomLeft=!left&&up?2:6;
1059
+ var p=new GraphicsPath();
1060
+ p.AddArc(r.X,r.Y,topLeft*2,topLeft*2,180,90);p.AddArc(r.Right-topRight*2,r.Y,topRight*2,topRight*2,270,90);
1061
+ p.AddArc(r.Right-bottomRight*2,r.Bottom-bottomRight*2,bottomRight*2,bottomRight*2,0,90);p.AddArc(r.X,r.Bottom-bottomLeft*2,bottomLeft*2,bottomLeft*2,90,90);
1062
+ p.CloseFigure();return p;
1063
+ }
1064
+ protected override void Dispose(bool disposing) {
1065
+ if(dc!=IntPtr.Zero) { if(bitmap!=IntPtr.Zero){Native.SelectObject(dc,previous);Native.DeleteObject(bitmap);}Native.DeleteDC(dc);dc=IntPtr.Zero; }
1066
+ base.Dispose(disposing);
1067
+ }
1068
+ }
1069
+
1070
+ internal sealed class Frame : Form {
1071
+ readonly Rectangle rectangle;
1072
+ readonly Rectangle rim;
1073
+ bool remoteVisible;
1074
+ IntPtr dc, bitmap, previous;
1075
+ internal Frame(Rectangle bounds, string text, string position,bool visible)
1076
+ : this(bounds, new Rectangle(0,0,bounds.Width,bounds.Height), text, position, visible) { }
1077
+ // surface is the window that spans the app plus the pill band; rim is the
1078
+ // app window itself, expressed in absolute screen coordinates.
1079
+ internal Frame(Rectangle bounds, Rectangle rimBounds, string text, string position, bool visible) {
1080
+ remoteVisible=visible;rectangle = bounds;
1081
+ rim = new Rectangle(rimBounds.X - bounds.X, rimBounds.Y - bounds.Y, rimBounds.Width, rimBounds.Height);
1082
+ AutoScaleMode=AutoScaleMode.None;FormBorderStyle = FormBorderStyle.None;
1083
+ ShowInTaskbar = false; TopMost = true; StartPosition = FormStartPosition.Manual;
1084
+ Bounds=bounds;
1085
+ using (var image = Draw(bounds.Width, bounds.Height, rim, text, position)) bitmap = image.GetHbitmap(Color.FromArgb(0));
1086
+ dc = Native.CreateCompatibleDC(IntPtr.Zero); previous = Native.SelectObject(dc, bitmap);
1087
+ }
1088
+ protected override bool ShowWithoutActivation { get { return true; } }
1089
+ protected override CreateParams CreateParams {
1090
+ get { var value = base.CreateParams; value.ExStyle |= 0x80000 | 0x20 | 0x80 | 0x08000000; return value; }
1091
+ }
1092
+ protected override void OnHandleCreated(EventArgs e) { base.OnHandleCreated(e); SetRemoteVisible(remoteVisible); }
1093
+ internal void SetRemoteVisible(bool visible) {remoteVisible=visible;if(IsHandleCreated)Native.SetWindowDisplayAffinity(Handle,visible?0u:0x11u);}
1094
+ internal Rectangle DisplayBounds {get{return rectangle;}}
1095
+ internal bool CaptureExcluded { get { uint affinity;return Native.GetWindowDisplayAffinity(Handle,out affinity)&&affinity==0x11; } }
1096
+ internal bool ClickThrough {get{return (Native.GetWindowLong(Handle,-20)&0x08080020)==0x08080020;}}
1097
+ internal void Present(byte alpha) {
1098
+ if (IsDisposed) return;
1099
+ var destination = new Native.Point(rectangle.X, rectangle.Y); var origin = new Native.Point(0,0);
1100
+ var size = new Native.Size(rectangle.Width,rectangle.Height);
1101
+ var blend = new Native.Blend { op=0, flags=0, alpha=alpha, format=1 };
1102
+ if (!Native.UpdateLayeredWindow(Handle, IntPtr.Zero, ref destination, ref size, dc, ref origin, 0, ref blend, 2)) throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error(), "Could not present takeover overlay");
1103
+ }
1104
+ internal static Bitmap Draw(int width, int height, string text, string position) {
1105
+ return Draw(width, height, new Rectangle(0,0,width,height), text, position);
1106
+ }
1107
+ // rim is the rectangle the glow outlines, in surface coordinates. The pill
1108
+ // prefers the free band above the rim, then below, and only then goes inside.
1109
+ internal static Bitmap Draw(int width, int height, Rectangle rim, string text, string position) {
1110
+ var image = new Bitmap(width,height,PixelFormat.Format32bppPArgb);
1111
+ using (var g = Graphics.FromImage(image)) {
1112
+ g.SmoothingMode = SmoothingMode.AntiAlias;
1113
+ // ClearType needs an opaque surface; layered windows need grayscale text.
1114
+ g.TextRenderingHint = TextRenderingHint.AntiAliasGridFit;
1115
+ g.Clear(Color.Transparent);
1116
+ float scale = Math.Max(1f, Math.Min(2f, width/1920f));
1117
+ // Edge glow: a bright core line that reads on dark wallpapers, then a
1118
+ // DeepSeek-blue falloff that reads on light ones.
1119
+ g.CompositingMode = CompositingMode.SourceCopy;
1120
+ int core = (int)Math.Round(2*scale), glow = core+(int)Math.Round(18*scale);
1121
+ glow = Math.Max(core + 1, Math.Min(glow, Math.Max(4, Math.Min(rim.Width, rim.Height) / 2 - 1)));
1122
+ for (int i=0;i<glow;i++) {
1123
+ var color = i<core ? Color.FromArgb(235,150,184,255) : Color.FromArgb((int)(175*Math.Pow(1-(i-core)/(double)(glow-core),2.2)),77,124,254);
1124
+ using (var pen = new Pen(color,1)) g.DrawRectangle(pen, rim.X+i, rim.Y+i, rim.Width-2*i-1, rim.Height-2*i-1);
1125
+ }
1126
+ g.CompositingMode = CompositingMode.SourceOver;
1127
+ // Status pill: live dot, the user's text, then an Esc keycap.
1128
+ using (var font = new Font("Microsoft YaHei UI",10.5f*scale,FontStyle.Regular,GraphicsUnit.Point))
1129
+ using (var keyFont = new Font("Segoe UI",8.5f*scale,FontStyle.Bold,GraphicsUnit.Point))
1130
+ using (var hintFont = new Font("Microsoft YaHei UI",9.5f*scale,FontStyle.Regular,GraphicsUnit.Point))
1131
+ using (var measure = new StringFormat(StringFormat.GenericTypographic) { FormatFlags = StringFormatFlags.NoWrap | StringFormatFlags.MeasureTrailingSpaces }) {
1132
+ const string hint = "Stop";
1133
+ float pillHeight=40*scale, padding=16*scale, dot=8*scale, gap=10*scale, keyHeight=20*scale;
1134
+ float keyWidth=g.MeasureString("Esc",keyFont,PointF.Empty,measure).Width+12*scale;
1135
+ float hintWidth=g.MeasureString(hint,hintFont,PointF.Empty,measure).Width;
1136
+ float fixedWidth=padding*2+dot+gap*3+1+keyWidth+6*scale+hintWidth;
1137
+ float textWidth=Math.Min(g.MeasureString(text,font,PointF.Empty,measure).Width,Math.Max(48*scale,width-48*scale-fixedWidth));
1138
+ float pillWidth=fixedWidth+textWidth;
1139
+ float above=rim.Y-pillHeight-10*scale, below=rim.Bottom+10*scale;
1140
+ bool fitsAbove=above>=4*scale, fitsBelow=below+pillHeight<=height-4*scale;
1141
+ float y = position=="bottom"
1142
+ ? (fitsBelow?below:(fitsAbove?above:rim.Y+24*scale))
1143
+ : (fitsAbove?above:(fitsBelow?below:rim.Y+24*scale));
1144
+ float x=(float)Math.Round(rim.X+(rim.Width-pillWidth)/2);
1145
+ if (x < 4*scale) x = 4*scale;
1146
+ if (x + pillWidth > width - 4*scale) x = Math.Max(4*scale, width - 4*scale - pillWidth);
1147
+ float middle=y+pillHeight/2;
1148
+ for (int i=1;i<=6;i++)
1149
+ using (var shadow=Rounded(new RectangleF(x-i,y-i+2*scale,pillWidth+2*i,pillHeight+2*i),pillHeight/2+i))
1150
+ using (var brush=new SolidBrush(Color.FromArgb(9,0,0,0))) g.FillPath(brush,shadow);
1151
+ using (var shape=Rounded(new RectangleF(x,y,pillWidth,pillHeight),pillHeight/2)) {
1152
+ using (var fill=new SolidBrush(Color.FromArgb(238,20,22,28))) g.FillPath(fill,shape);
1153
+ using (var outline=new Pen(Color.FromArgb(40,255,255,255),1)) g.DrawPath(outline,shape);
1154
+ }
1155
+ float cursor=x+padding;
1156
+ using (var halo=new SolidBrush(Color.FromArgb(64,86,134,254))) g.FillEllipse(halo,cursor-3*scale,middle-dot/2-3*scale,dot+6*scale,dot+6*scale);
1157
+ using (var fill=new SolidBrush(Color.FromArgb(255,86,134,254))) g.FillEllipse(fill,cursor,middle-dot/2,dot,dot);
1158
+ cursor+=dot+gap;
1159
+ using (var brush=new SolidBrush(Color.FromArgb(255,245,247,250)))
1160
+ using (var format=new StringFormat(StringFormat.GenericTypographic) { LineAlignment=StringAlignment.Center, Trimming=StringTrimming.EllipsisCharacter, FormatFlags=StringFormatFlags.NoWrap })
1161
+ g.DrawString(text,font,brush,new RectangleF(cursor,y,textWidth+1,pillHeight),format);
1162
+ cursor+=textWidth+gap;
1163
+ using (var line=new Pen(Color.FromArgb(56,255,255,255),1)) g.DrawLine(line,cursor,middle-8*scale,cursor,middle+8*scale);
1164
+ cursor+=1+gap;
1165
+ var key=new RectangleF(cursor,middle-keyHeight/2,keyWidth,keyHeight);
1166
+ using (var shape=Rounded(key,5*scale)) {
1167
+ using (var fill=new SolidBrush(Color.FromArgb(34,255,255,255))) g.FillPath(fill,shape);
1168
+ using (var outline=new Pen(Color.FromArgb(64,255,255,255),1)) g.DrawPath(outline,shape);
1169
+ }
1170
+ using (var brush=new SolidBrush(Color.FromArgb(255,245,247,250)))
1171
+ using (var format=new StringFormat(StringFormat.GenericTypographic) { Alignment=StringAlignment.Center, LineAlignment=StringAlignment.Center })
1172
+ g.DrawString("Esc",keyFont,brush,key,format);
1173
+ cursor+=keyWidth+6*scale;
1174
+ using (var brush=new SolidBrush(Color.FromArgb(200,245,247,250)))
1175
+ using (var format=new StringFormat(StringFormat.GenericTypographic) { LineAlignment=StringAlignment.Center, FormatFlags=StringFormatFlags.NoWrap })
1176
+ g.DrawString(hint,hintFont,brush,new RectangleF(cursor,y,hintWidth+2,pillHeight),format);
1177
+ }
1178
+ }
1179
+ return image;
1180
+ }
1181
+ internal static GraphicsPath Rounded(RectangleF r,float radius) {
1182
+ var p=new GraphicsPath(); float d=radius*2;
1183
+ p.AddArc(r.X,r.Y,d,d,180,90);p.AddArc(r.Right-d,r.Y,d,d,270,90);p.AddArc(r.Right-d,r.Bottom-d,d,d,0,90);p.AddArc(r.X,r.Bottom-d,d,d,90,90);p.CloseFigure();return p;
1184
+ }
1185
+ protected override void Dispose(bool disposing) {
1186
+ if (dc!=IntPtr.Zero) { Native.SelectObject(dc,previous); Native.DeleteObject(bitmap); Native.DeleteDC(dc); dc=IntPtr.Zero; }
1187
+ base.Dispose(disposing);
1188
+ }
1189
+ }
1190
+ }