using System; using System.IO; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Interop; using System.Windows.Media; using WF = System.Windows.Forms; namespace QuickClaude { public class MainWindow : Window { readonly Settings settings; readonly ClaudeRunner runner = new ClaudeRunner(); readonly TextBox transcript = new TextBox(); readonly TextBox input = new TextBox(); readonly TextBlock status = new TextBlock(); readonly TextBlock grip = new TextBlock(); // Holds the transcript. The transcript inside is always sized to a whole number of lines // and sits at the bottom, so no line is ever shown cut in half (see FitTranscript). readonly Border transcriptHost = new Border { ClipToBounds = true }; double lineHeight = 13; WF.NotifyIcon tray; WF.ToolStripMenuItem fontMenu, sizeMenu, snapItem, topItem, blackItem, whiteItem, lookItem, editItem, autoItem, folderItem; bool lastWasTool; bool quitting; // Nearly invisible fill so the window still catches the mouse (fully clear pixels click through) static readonly Brush HitFill = new SolidColorBrush(Color.FromArgb(1, 0, 0, 0)); // The current font (picked from the tray's Font menu). Still called Mono since // ConfirmWindow and DarkMenu read it when they open. internal static FontFamily Mono = new FontFamily("Consolas, Courier New"); // Picked from the tray's Font size menu, 10 to 18 (same units as wIRC's font size // slider: 10sp = 10 WPF units). ConfirmWindow and DarkMenu read it when they open. internal const double MinTextSize = 10, MaxTextSize = 18; internal static double TextSize = 10; const double ResizeEdge = 6; public MainWindow() { settings = Settings.Load(); Mono = Fonts.Find(settings.Font).Family; TextSize = Math.Clamp(Math.Round(settings.FontSize), MinTextSize, MaxTextSize); Title = "QuickClaude"; WindowStyle = WindowStyle.None; AllowsTransparency = true; Background = HitFill; ShowInTaskbar = false; ResizeMode = ResizeMode.CanResize; MinWidth = 240; MinHeight = 160; FontFamily = Mono; FontSize = TextSize; Width = Math.Max(240, settings.Width); Height = Math.Max(160, settings.Height); Topmost = settings.Topmost; if (double.IsNaN(settings.Left) || double.IsNaN(settings.Top) || !OnScreen(settings.Left, settings.Top)) WindowStartupLocation = WindowStartupLocation.CenterScreen; else { Left = settings.Left; Top = settings.Top; } BuildLayout(); ApplyColor(); BuildTray(); runner.TextDelta += t => Dispatcher.InvokeAsync(() => OnText(t)); runner.ToolUsed += t => Dispatcher.InvokeAsync(() => OnTool(t)); runner.SessionStarted += id => Dispatcher.InvokeAsync(() => { settings.SessionId = id; settings.Save(); }); runner.Finished += err => Dispatcher.InvokeAsync(() => OnFinished(err)); try { if (File.Exists(Settings.TranscriptPath)) transcript.Text = File.ReadAllText(Settings.TranscriptPath); } catch { } if (transcript.Text.Length == 0) transcript.Text = "QuickClaude. Type below and press Enter. Shift+Enter for a new line, Esc stops Claude, Ctrl+Alt+C hides or shows this note.\n"; Loaded += (s, e) => { MeasureLineHeight(); transcript.ScrollToEnd(); input.Focus(); }; PreviewMouseLeftButtonUp += ClickToInput; SourceInitialized += (s, e) => RegisterHotkey(); LocationChanged += (s, e) => SavePlace(); Closing += (s, e) => { if (!quitting) { e.Cancel = true; Hide(); return; } runner.Stop(); SavePlace(); SaveTranscript(); tray.Visible = false; tray.Dispose(); }; } void BuildLayout() { var rootGrid = new Grid { Background = HitFill, Margin = new Thickness(4) }; rootGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); rootGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }); rootGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); // Top row: a solid "clod" label that moves the window, plus a quiet status line grip.Text = "clod"; grip.FontSize = TextSize; grip.FontFamily = Mono; grip.Background = HitFill; grip.Cursor = Cursors.SizeAll; grip.ToolTip = "Drag to move"; grip.MouseLeftButtonDown += (s, e) => { try { DragMove(); } catch { } SavePlace(); }; status.FontSize = TextSize; status.FontFamily = Mono; status.Margin = new Thickness(8, 0, 0, 0); var top = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 0, 0, 4) }; top.Children.Add(grip); top.Children.Add(status); Grid.SetRow(top, 0); rootGrid.Children.Add(top); // Conversation: read-only, selectable text with no border or fill StyleBox(transcript); transcript.IsReadOnly = true; transcript.TextWrapping = TextWrapping.Wrap; transcript.VerticalScrollBarVisibility = ScrollBarVisibility.Hidden; transcript.IsUndoEnabled = false; transcript.VerticalAlignment = VerticalAlignment.Bottom; transcriptHost.Background = HitFill; transcriptHost.Child = transcript; transcriptHost.SizeChanged += (s, e) => FitTranscript(); Grid.SetRow(transcriptHost, 1); rootGrid.Children.Add(transcriptHost); // Input line var bottom = new Grid { Margin = new Thickness(0, 6, 0, 0) }; StyleBox(input); input.AcceptsReturn = true; input.TextWrapping = TextWrapping.Wrap; input.MaxHeight = 120; input.VerticalScrollBarVisibility = ScrollBarVisibility.Hidden; input.Padding = new Thickness(0, 4, 0, 0); input.PreviewKeyDown += InputKeyDown; bottom.Children.Add(input); Grid.SetRow(bottom, 2); rootGrid.Children.Add(bottom); Content = rootGrid; BuildContextMenus(); } // Right-click menus: Copy (and Cut/Paste in the input) plus New conversation; blank areas get just the latter. // Rebuilt when the font changes so they use it too. void BuildContextMenus() { transcript.ContextMenu = DarkMenu.Make(transcript, NewConversation); input.ContextMenu = DarkMenu.Make(input, NewConversation); ((Grid)Content).ContextMenu = DarkMenu.Make(null, NewConversation); } // Height of one line of transcript text at the current font and size. Measured as the // difference between two lines and one, so it's exactly what the text box steps by. void MeasureLineHeight() { var probe = new TextBlock { FontFamily = Mono, FontSize = TextSize, Text = "Mg" }; probe.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); double one = probe.DesiredSize.Height; probe.Text = "Mg\nMg"; probe.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); double two = probe.DesiredSize.Height; lineHeight = two - one > 1 ? two - one : Math.Max(1, one); FitTranscript(); } // Make the transcript exactly as tall as the whole lines that fit, bottom-aligned, and keep // it scrolled to the end so the top line is whole too. Runs on every size change (window // resize, the input box growing, font changes). void FitTranscript() { double avail = transcriptHost.ActualHeight; if (avail <= 0 || lineHeight <= 0) return; int lines = Math.Max(1, (int)Math.Floor((avail + 0.01) / lineHeight)); transcript.Height = lines * lineHeight; transcript.ScrollToEnd(); } // Leftover space under the last whole line, in WPF units double TranscriptGap() => Math.Max(0, transcriptHost.ActualHeight - (double.IsNaN(transcript.Height) ? 0 : transcript.Height)); void SetFontSize(double size) { size = Math.Clamp(size, MinTextSize, MaxTextSize); settings.FontSize = size; settings.Save(); TextSize = size; FontSize = size; transcript.FontSize = size; input.FontSize = size; status.FontSize = size; grip.FontSize = size; MeasureLineHeight(); BuildContextMenus(); transcript.ScrollToEnd(); if (sizeMenu != null) foreach (WF.ToolStripMenuItem item in sizeMenu.DropDownItems) item.Checked = item.Text == size.ToString(); } void SetFont(string name) { var choice = Fonts.Find(name); settings.Font = choice.Name; settings.Save(); Mono = choice.Family; FontFamily = Mono; transcript.FontFamily = Mono; input.FontFamily = Mono; status.FontFamily = Mono; grip.FontFamily = Mono; MeasureLineHeight(); BuildContextMenus(); if (fontMenu != null) foreach (WF.ToolStripMenuItem item in fontMenu.DropDownItems) item.Checked = item.Text == choice.Name; } static void StyleBox(TextBox tb) { tb.Background = Brushes.Transparent; tb.BorderThickness = new Thickness(0); tb.FontFamily = Mono; tb.FontSize = TextSize; tb.Padding = new Thickness(0); tb.FocusVisualStyle = null; tb.Template = BareTextBoxTemplate; } // Just the text host: no border, so the theme's blue focus/hover border never shows internal static readonly ControlTemplate BareTextBoxTemplate = MakeBareTextBoxTemplate(); static ControlTemplate MakeBareTextBoxTemplate() { var host = new FrameworkElementFactory(typeof(ScrollViewer), "PART_ContentHost"); host.SetValue(ScrollViewer.FocusableProperty, false); host.SetValue(ScrollViewer.HorizontalScrollBarVisibilityProperty, ScrollBarVisibility.Hidden); host.SetValue(ScrollViewer.VerticalScrollBarVisibilityProperty, ScrollBarVisibility.Hidden); var root = new FrameworkElementFactory(typeof(Border)); root.SetValue(Border.BackgroundProperty, new TemplateBindingExtension(Control.BackgroundProperty)); root.SetValue(Border.PaddingProperty, new TemplateBindingExtension(Control.PaddingProperty)); root.AppendChild(host); var t = new ControlTemplate(typeof(TextBox)) { VisualTree = root }; t.Seal(); return t; } void ApplyColor() { var c = settings.TextColor == "White" ? Colors.White : Colors.Black; var solid = new SolidColorBrush(c); var faint = new SolidColorBrush(Color.FromArgb(110, c.R, c.G, c.B)); transcript.Foreground = solid; input.Foreground = solid; input.CaretBrush = solid; transcript.CaretBrush = solid; status.Foreground = faint; grip.Foreground = solid; if (blackItem != null) { blackItem.Checked = c == Colors.Black; whiteItem.Checked = c == Colors.White; } } // A click anywhere puts the cursor in the input. Clicks on the input itself and on "clod" // keep their own behavior, and a drag that selected conversation text keeps the selection // (and focus) so it can be copied. Edge resizing never gets here: it's non-client. void ClickToInput(object sender, MouseButtonEventArgs e) { var src = e.OriginalSource as DependencyObject; if (IsWithin(src, input) || IsWithin(src, grip)) return; bool inTranscript = IsWithin(src, transcript); Dispatcher.BeginInvoke(new Action(() => { if (inTranscript && transcript.SelectionLength > 0) return; input.Focus(); Keyboard.Focus(input); }), System.Windows.Threading.DispatcherPriority.Input); } static bool IsWithin(DependencyObject node, DependencyObject target) { while (node != null) { if (node == target) return true; node = node is Visual || node is System.Windows.Media.Media3D.Visual3D ? VisualTreeHelper.GetParent(node) : LogicalTreeHelper.GetParent(node); } return false; } void InputKeyDown(object sender, KeyEventArgs e) { if (e.Key == Key.Escape) { if (runner.Running) runner.Stop(); else Hide(); e.Handled = true; } else if (e.Key == Key.Enter && (Keyboard.Modifiers & ModifierKeys.Shift) == 0) { e.Handled = true; Send(); } } async void Send() { var prompt = input.Text.Trim(); if (prompt.Length == 0 || runner.Running) return; var claude = ClaudeRunner.FindClaude(settings.ClaudePath); if (claude == null) { Append((transcript.Text.Length > 0 ? "\n" : "") + "Can't find Claude Code on this computer. Install it, or use the tray menu's \"Find claude.exe...\" to point at it.\n"); return; } input.Clear(); var lead = transcript.Text.Length == 0 ? "" : (transcript.Text.EndsWith("\n") ? "\n" : "\n\n"); Append(lead + "> " + prompt.Replace("\n", "\n ") + "\n\n"); lastWasTool = false; status.Text = "working... (Esc to stop)"; await runner.RunAsync(claude, prompt, settings.WorkDir, settings.SessionId, settings.Mode); } void OnText(string t) { if (t == "\u0001") { if (lastWasTool && !transcript.Text.EndsWith("\n")) Append("\n"); lastWasTool = false; return; } Append(t); } void OnTool(string line) { if (transcript.Text.Length > 0 && !transcript.Text.EndsWith("\n")) Append("\n"); Append(line + "\n"); lastWasTool = true; } void OnFinished(string err) { if (err != null) { if (!transcript.Text.EndsWith("\n")) Append("\n"); Append("(" + err.Trim() + ")\n"); } else if (!transcript.Text.EndsWith("\n")) Append("\n"); status.Text = ""; SaveTranscript(); } void Append(string text) { transcript.AppendText(text); transcript.ScrollToEnd(); } void SaveTranscript() { try { Directory.CreateDirectory(Settings.Folder); var t = transcript.Text; if (t.Length > 200000) t = t.Substring(t.Length - 200000); File.WriteAllText(Settings.TranscriptPath, t, new UTF8Encoding(false)); } catch { } } void SavePlace() { if (WindowState != WindowState.Normal) return; settings.Left = Left; settings.Top = Top; settings.Width = Width; settings.Height = Height; settings.Save(); } static bool OnScreen(double left, double top) { foreach (var s in WF.Screen.AllScreens) if (s.WorkingArea.Contains((int)left + 20, (int)top + 20)) return true; return false; } public void Toggle() { if (IsVisible) Hide(); else { Show(); Activate(); input.Focus(); Keyboard.Focus(input); } } void NewConversation() { if (runner.Running) runner.Stop(); settings.SessionId = ""; settings.Save(); transcript.Clear(); Append("New conversation.\n"); SaveTranscript(); if (!IsVisible) Toggle(); } void SetMode(string mode) { settings.Mode = mode; settings.Save(); lookItem.Checked = mode == "default"; editItem.Checked = mode == "acceptEdits"; autoItem.Checked = mode == "auto"; } void BuildTray() { var menu = new WF.ContextMenuStrip(); menu.Items.Add("Show / hide (Ctrl+Alt+C)", null, (s, e) => Toggle()); menu.Items.Add("New conversation", null, (s, e) => NewConversation()); menu.Items.Add(new WF.ToolStripSeparator()); var color = new WF.ToolStripMenuItem("Text color"); blackItem = new WF.ToolStripMenuItem("Black", null, (s, e) => { settings.TextColor = "Black"; settings.Save(); ApplyColor(); }); whiteItem = new WF.ToolStripMenuItem("White", null, (s, e) => { settings.TextColor = "White"; settings.Save(); ApplyColor(); }); color.DropDownItems.Add(blackItem); color.DropDownItems.Add(whiteItem); menu.Items.Add(color); fontMenu = new WF.ToolStripMenuItem("Font"); var current = Fonts.Find(settings.Font).Name; foreach (var choice in Fonts.All) { var name = choice.Name; fontMenu.DropDownItems.Add(new WF.ToolStripMenuItem(name, null, (s, e) => SetFont(name)) { Checked = name == current }); } menu.Items.Add(fontMenu); sizeMenu = new WF.ToolStripMenuItem("Font size"); for (int px = (int)MinTextSize; px <= (int)MaxTextSize; px++) { double sz = px; sizeMenu.DropDownItems.Add(new WF.ToolStripMenuItem(px.ToString(), null, (s, e) => SetFontSize(sz)) { Checked = sz == TextSize }); } menu.Items.Add(sizeMenu); snapItem = new WF.ToolStripMenuItem("Snap to desktop grid", null, (s, e) => { settings.SnapToGrid = !settings.SnapToGrid; snapItem.Checked = settings.SnapToGrid; settings.Save(); }) { Checked = settings.SnapToGrid }; menu.Items.Add(snapItem); topItem = new WF.ToolStripMenuItem("Always on top", null, (s, e) => { settings.Topmost = !settings.Topmost; Topmost = settings.Topmost; topItem.Checked = settings.Topmost; settings.Save(); }) { Checked = settings.Topmost }; menu.Items.Add(topItem); menu.Items.Add(new WF.ToolStripSeparator()); var can = new WF.ToolStripMenuItem("Claude can"); lookItem = new WF.ToolStripMenuItem("Only look (read files, no changes)", null, (s, e) => SetMode("default")); editItem = new WF.ToolStripMenuItem("Edit files in the work folder", null, (s, e) => SetMode("acceptEdits")); autoItem = new WF.ToolStripMenuItem("Auto (Claude Code's auto mode)", null, (s, e) => SetMode("auto")); can.DropDownItems.Add(lookItem); can.DropDownItems.Add(editItem); can.DropDownItems.Add(autoItem); menu.Items.Add(can); folderItem = new WF.ToolStripMenuItem("Work folder: " + settings.WorkDir, null, (s, e) => { using var dlg = new WF.FolderBrowserDialog { SelectedPath = settings.WorkDir, Description = "Folder Claude works in" }; if (dlg.ShowDialog() == WF.DialogResult.OK) { settings.WorkDir = dlg.SelectedPath; settings.Save(); folderItem.Text = "Work folder: " + settings.WorkDir; } }); menu.Items.Add(folderItem); menu.Items.Add("Find claude.exe...", null, (s, e) => { using var dlg = new WF.OpenFileDialog { Filter = "Claude Code|claude.exe;claude.cmd|All files|*.*", Title = "Find Claude Code" }; if (dlg.ShowDialog() == WF.DialogResult.OK) { settings.ClaudePath = dlg.FileName; settings.Save(); } }); menu.Items.Add(new WF.ToolStripSeparator()); menu.Items.Add("Exit", null, (s, e) => { quitting = true; Close(); System.Windows.Application.Current.Shutdown(); }); tray = new WF.NotifyIcon { Icon = TrayIcon.Make(), Text = "QuickClaude", ContextMenuStrip = menu, Visible = true }; tray.MouseClick += (s, e) => { if (e.Button == WF.MouseButtons.Left) Toggle(); }; SetMode(settings.Mode); ApplyColor(); } // Global hotkey: Ctrl+Alt+C [DllImport("user32.dll")] static extern bool RegisterHotKey(IntPtr hWnd, int id, uint mods, uint vk); const int HotkeyId = 0x5143; void RegisterHotkey() { var helper = new WindowInteropHelper(this); RegisterHotKey(helper.Handle, HotkeyId, 0x0001 | 0x0002 | 0x4000, 0x43); HwndSource.FromHwnd(helper.Handle)?.AddHook((IntPtr hwnd, int msg, IntPtr w, IntPtr l, ref bool handled) => { if (msg == 0x0312 && w.ToInt32() == HotkeyId) { Toggle(); handled = true; } else if (msg == WM_NCHITTEST) { var hit = EdgeHit(hwnd, l); if (hit != 0) { handled = true; return new IntPtr(hit); } } else if (msg == WM_ENTERSIZEMOVE) GetWindowRect(hwnd, out moveStart); else if (msg == WM_EXITSIZEMOVE) { AfterMoveOrResize(hwnd); SavePlace(); } return IntPtr.Zero; }); } // Resize from any edge or corner: report a ~6px border strip as the matching hit code [DllImport("user32.dll")] static extern bool GetWindowRect(IntPtr hWnd, out RECT rect); [StructLayout(LayoutKind.Sequential)] struct RECT { public int Left, Top, Right, Bottom; } const int WM_NCHITTEST = 0x0084, WM_ENTERSIZEMOVE = 0x0231, WM_EXITSIZEMOVE = 0x0232; RECT moveStart; [DllImport("user32.dll")] static extern bool SetWindowPos(IntPtr hWnd, IntPtr after, int x, int y, int cx, int cy, uint flags); [DllImport("user32.dll")] static extern IntPtr MonitorFromWindow(IntPtr hwnd, uint flags); [DllImport("user32.dll", CharSet = CharSet.Auto)] static extern bool GetMonitorInfo(IntPtr mon, ref MONITORINFO info); [DllImport("user32.dll")] static extern bool SystemParametersInfo(uint action, uint param, out int value, uint ini); [StructLayout(LayoutKind.Sequential)] struct MONITORINFO { public int cbSize; public RECT rcMonitor, rcWork; public uint dwFlags; } const uint SWP_NOSIZE = 0x1, SWP_NOMOVE = 0x2, SWP_NOZORDER = 0x4, SWP_NOACTIVATE = 0x10; const uint SPI_ICONHORIZONTALSPACING = 0x000D, SPI_ICONVERTICALSPACING = 0x0018; [DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern IntPtr FindWindow(string cls, string name); [DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern IntPtr FindWindowEx(IntPtr parent, IntPtr after, string cls, string name); [DllImport("user32.dll")] static extern IntPtr SendMessage(IntPtr h, int msg, IntPtr w, IntPtr l); const int LVM_GETITEMSPACING = 0x1033; // The desktop icon grid, asked of the desktop's own icon view so it follows the icon size // and spacing Windows is really using (wIRC asks the same way, so both line up), falling // back to the system icon spacing. In physical pixels. static bool DesktopGridSpacing(out int sx, out int sy) { sx = sy = 0; try { var view = FindWindowEx(FindWindow("Progman", null), IntPtr.Zero, "SHELLDLL_DefView", null); var worker = IntPtr.Zero; while (view == IntPtr.Zero) { worker = FindWindowEx(IntPtr.Zero, worker, "WorkerW", null); if (worker == IntPtr.Zero) break; view = FindWindowEx(worker, IntPtr.Zero, "SHELLDLL_DefView", null); } var list = view == IntPtr.Zero ? IntPtr.Zero : FindWindowEx(view, IntPtr.Zero, "SysListView32", null); if (list != IntPtr.Zero) { long r = SendMessage(list, LVM_GETITEMSPACING, IntPtr.Zero, IntPtr.Zero).ToInt64(); sx = (int)(r & 0xFFFF); sy = (int)((r >> 16) & 0xFFFF); if (sx > 8 && sy > 8) return true; } } catch { } return SystemParametersInfo(SPI_ICONHORIZONTALSPACING, 0, out sx, 0) && sx > 8 && SystemParametersInfo(SPI_ICONVERTICALSPACING, 0, out sy, 0) && sy > 8; } // After a drag or resize ends: a resize trims the window to whole transcript lines, and a // plain move snaps the top-left corner to the desktop icon grid (same spacing Windows uses // for desktop icons, counted from the top-left of the screen's work area). void AfterMoveOrResize(IntPtr hwnd) { if (!GetWindowRect(hwnd, out var r)) return; bool resized = (r.Right - r.Left) != (moveStart.Right - moveStart.Left) || (r.Bottom - r.Top) != (moveStart.Bottom - moveStart.Top); double scale = PresentationSource.FromVisual(this)?.CompositionTarget?.TransformToDevice.M22 ?? 1; if (resized) { UpdateLayout(); double gap = TranscriptGap(); int gapPx = (int)Math.Floor(gap * scale); int h = r.Bottom - r.Top; int minPx = (int)Math.Ceiling(MinHeight * scale); if (gapPx > 0 && h - gapPx >= minPx) { // Keep whichever edge didn't move: resizing from the top keeps the bottom fixed bool fromTop = r.Top != moveStart.Top && r.Bottom == moveStart.Bottom; int top = fromTop ? r.Top + gapPx : r.Top; SetWindowPos(hwnd, IntPtr.Zero, r.Left, top, r.Right - r.Left, h - gapPx, SWP_NOZORDER | SWP_NOACTIVATE); } return; } if (!settings.SnapToGrid) return; if (r.Left == moveStart.Left && r.Top == moveStart.Top) return; if (!DesktopGridSpacing(out int sx, out int sy)) return; var info = new MONITORINFO { cbSize = Marshal.SizeOf() }; if (!GetMonitorInfo(MonitorFromWindow(hwnd, 2 /* nearest */), ref info)) return; var w = info.rcWork; int x = w.Left + (int)Math.Round((r.Left - w.Left) / (double)sx) * sx; int y = w.Top + (int)Math.Round((r.Top - w.Top) / (double)sy) * sy; if (x != r.Left || y != r.Top) SetWindowPos(hwnd, IntPtr.Zero, x, y, 0, 0, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); } const int HTLEFT = 10, HTRIGHT = 11, HTTOP = 12, HTTOPLEFT = 13, HTTOPRIGHT = 14, HTBOTTOM = 15, HTBOTTOMLEFT = 16, HTBOTTOMRIGHT = 17; int EdgeHit(IntPtr hwnd, IntPtr l) { if (!GetWindowRect(hwnd, out var r)) return 0; long lp = l.ToInt64(); int x = (short)(lp & 0xFFFF), y = (short)((lp >> 16) & 0xFFFF); double scale = PresentationSource.FromVisual(this)?.CompositionTarget?.TransformToDevice.M11 ?? 1; int edge = (int)Math.Ceiling(ResizeEdge * scale); bool left = x < r.Left + edge, right = x >= r.Right - edge; bool top = y < r.Top + edge, bottom = y >= r.Bottom - edge; if (top && left) return HTTOPLEFT; if (top && right) return HTTOPRIGHT; if (bottom && left) return HTBOTTOMLEFT; if (bottom && right) return HTBOTTOMRIGHT; if (left) return HTLEFT; if (right) return HTRIGHT; if (top) return HTTOP; if (bottom) return HTBOTTOM; return 0; } } }