|
2 | 2 |
|
3 | 3 | using System.Diagnostics; |
4 | 4 | using System.IO; |
| 5 | +using System.Runtime.InteropServices; |
5 | 6 | using System.Windows; |
| 7 | +using System.Windows.Interop; |
6 | 8 | using Microsoft.Extensions.Logging; |
7 | 9 | using Microsoft.Extensions.DependencyInjection; |
8 | 10 | using SQLTriage.Data; |
@@ -495,6 +497,18 @@ private void OnBlazorWebViewInitialized(object? sender, Microsoft.AspNetCore.Com |
495 | 497 | _webView2Initialized = true; |
496 | 498 | _coreWebView2 = e.WebView.CoreWebView2; |
497 | 499 |
|
| 500 | + // Wire up parallax background: push monitor+window geometry into |
| 501 | + // the WebView whenever the window moves, resizes, or changes DPI. |
| 502 | + LocationChanged += OnParallaxGeometryChanged; |
| 503 | + SizeChanged += OnParallaxGeometryChanged; |
| 504 | + DpiChanged += OnParallaxGeometryChanged; |
| 505 | + // Push initial state once navigation completes (host page must be |
| 506 | + // loaded before postMessage can reach the JS listener). |
| 507 | + e.WebView.NavigationCompleted += (_, args) => |
| 508 | + { |
| 509 | + if (args.IsSuccess) PushParallaxGeometry(); |
| 510 | + }; |
| 511 | + |
498 | 512 | // Wire up PrintService with the live CoreWebView2 instance |
499 | 513 | var printService = App.Services?.GetService<Data.Services.PrintService>(); |
500 | 514 | printService?.SetWebView(_coreWebView2); |
@@ -527,6 +541,116 @@ private void OnBlazorWebViewInitialized(object? sender, Microsoft.AspNetCore.Com |
527 | 541 | } |
528 | 542 | } |
529 | 543 |
|
| 544 | + // ── Parallax background plumbing ──────────────────────────────────── |
| 545 | + // Anchors the ambient background to the monitor (not the window) by |
| 546 | + // pushing geometry into the WebView. Two layers of throttling: |
| 547 | + // 1) WPF events (LocationChanged etc.) just flip a dirty flag — |
| 548 | + // they fire at mouse-move rate (often >120 Hz on modern mice) |
| 549 | + // and a JSON-serialize + cross-process PostWebMessageAsString |
| 550 | + // per tick was the source of drag jank. |
| 551 | + // 2) A DispatcherTimer at ~60 Hz drains the flag, so we IPC at most |
| 552 | + // once per frame regardless of input rate. |
| 553 | + // The browser side has its own rAF coalescing on top of this. |
| 554 | + private DispatcherTimer? _parallaxTimer; |
| 555 | + private bool _parallaxDirty; |
| 556 | + private int _lastMonitorW, _lastMonitorH, _lastWindowX, _lastWindowY; |
| 557 | + |
| 558 | + private void OnParallaxGeometryChanged(object? sender, EventArgs e) |
| 559 | + { |
| 560 | + _parallaxDirty = true; |
| 561 | + // Lazy-start the timer on first event so we don't pay the tick |
| 562 | + // cost during sessions where the window never moves. |
| 563 | + if (_parallaxTimer == null) |
| 564 | + { |
| 565 | + _parallaxTimer = new DispatcherTimer(DispatcherPriority.Render) |
| 566 | + { |
| 567 | + Interval = TimeSpan.FromMilliseconds(16) |
| 568 | + }; |
| 569 | + _parallaxTimer.Tick += (_, _) => |
| 570 | + { |
| 571 | + if (_parallaxDirty) PushParallaxGeometry(); |
| 572 | + }; |
| 573 | + _parallaxTimer.Start(); |
| 574 | + } |
| 575 | + } |
| 576 | + |
| 577 | + private void PushParallaxGeometry() |
| 578 | + { |
| 579 | + _parallaxDirty = false; |
| 580 | + if (_coreWebView2 == null) return; |
| 581 | + |
| 582 | + try |
| 583 | + { |
| 584 | + var hwnd = new WindowInteropHelper(this).Handle; |
| 585 | + if (hwnd == IntPtr.Zero) return; |
| 586 | + |
| 587 | + var hMonitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); |
| 588 | + var mi = new MONITORINFO { cbSize = Marshal.SizeOf<MONITORINFO>() }; |
| 589 | + if (!GetMonitorInfo(hMonitor, ref mi)) return; |
| 590 | + |
| 591 | + // Convert monitor rect (device pixels) → CSS pixels using this |
| 592 | + // window's current DPI scale. WPF's CompositionTarget gives us |
| 593 | + // a transform that already accounts for per-monitor DPI. |
| 594 | + var src = PresentationSource.FromVisual(this); |
| 595 | + double dpiX = 1.0, dpiY = 1.0; |
| 596 | + if (src?.CompositionTarget != null) |
| 597 | + { |
| 598 | + var m = src.CompositionTarget.TransformToDevice; |
| 599 | + dpiX = m.M11; |
| 600 | + dpiY = m.M22; |
| 601 | + } |
| 602 | + |
| 603 | + int monitorW = (int)((mi.rcMonitor.Right - mi.rcMonitor.Left) / dpiX); |
| 604 | + int monitorH = (int)((mi.rcMonitor.Bottom - mi.rcMonitor.Top) / dpiY); |
| 605 | + // Window.Left/Top are already in CSS-equivalent DIPs at the |
| 606 | + // window's DPI; subtract monitor origin (also DIPs after divide). |
| 607 | + int windowX = (int)(this.Left - (mi.rcMonitor.Left / dpiX)); |
| 608 | + int windowY = (int)(this.Top - (mi.rcMonitor.Top / dpiY)); |
| 609 | + |
| 610 | + // Clamp to non-negative so a window slightly off-screen (e.g. |
| 611 | + // mid-drag across monitors) doesn't push the image outside. |
| 612 | + if (windowX < 0) windowX = 0; |
| 613 | + if (windowY < 0) windowY = 0; |
| 614 | + |
| 615 | + // Skip identical ticks — saves an IPC + JSON parse on the |
| 616 | + // browser side when the OS reports the same coords twice. |
| 617 | + if (monitorW == _lastMonitorW && monitorH == _lastMonitorH && |
| 618 | + windowX == _lastWindowX && windowY == _lastWindowY) return; |
| 619 | + _lastMonitorW = monitorW; _lastMonitorH = monitorH; |
| 620 | + _lastWindowX = windowX; _lastWindowY = windowY; |
| 621 | + |
| 622 | + var json = $"{{\"type\":\"parallax\",\"monitorW\":{monitorW},\"monitorH\":{monitorH},\"windowX\":{windowX},\"windowY\":{windowY}}}"; |
| 623 | + _coreWebView2.PostWebMessageAsString(json); |
| 624 | + } |
| 625 | + catch (Exception ex) |
| 626 | + { |
| 627 | + _logger?.LogDebug(ex, "Parallax geometry push failed"); |
| 628 | + } |
| 629 | + } |
| 630 | + |
| 631 | + // P/Invoke for monitor info — needed because WPF's SystemParameters |
| 632 | + // only describe the *primary* screen, not the monitor the window is |
| 633 | + // currently on. |
| 634 | + private const int MONITOR_DEFAULTTONEAREST = 0x00000002; |
| 635 | + |
| 636 | + [DllImport("user32.dll")] |
| 637 | + private static extern IntPtr MonitorFromWindow(IntPtr hwnd, int dwFlags); |
| 638 | + |
| 639 | + [DllImport("user32.dll", CharSet = CharSet.Auto)] |
| 640 | + private static extern bool GetMonitorInfo(IntPtr hMonitor, ref MONITORINFO lpmi); |
| 641 | + |
| 642 | + [StructLayout(LayoutKind.Sequential)] |
| 643 | + private struct RECT { public int Left, Top, Right, Bottom; } |
| 644 | + |
| 645 | + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] |
| 646 | + private struct MONITORINFO |
| 647 | + { |
| 648 | + public int cbSize; |
| 649 | + public RECT rcMonitor; |
| 650 | + public RECT rcWork; |
| 651 | + public uint dwFlags; |
| 652 | + } |
| 653 | + |
530 | 654 | private void ApplyZoom(int zoomPercent) |
531 | 655 | { |
532 | 656 | if (BlazorWebView == null) return; |
|
0 commit comments