Web source: crop capture to actual content bounds (anchor 0,0); slider click-to-position; web URI X clears; tab order

This commit is contained in:
2026-08-28 11:10:49 -07:00
parent 23874c2dd6
commit f39ebd96ce
6 changed files with 98 additions and 30 deletions
+58 -3
View File
@@ -1,6 +1,7 @@
using System;
using System.Drawing;
using System.IO;
using System.Text.Json;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
@@ -26,6 +27,10 @@ public sealed class WebView2Manager : IDisposable
public required WebView2 Control { get; init; }
public required DispatcherTimer CaptureTimer { get; init; }
public VideoFrame? LatestFrame;
public double ViewportCssW;
public double ViewportCssH;
public double ContentCssW;
public double ContentCssH;
public bool Disposed;
public bool Initialized;
}
@@ -127,6 +132,7 @@ public sealed class WebView2Manager : IDisposable
"document.documentElement.style.overflow='hidden';" +
"document.body.style.background='transparent';" +
"document.body.style.overflow='hidden';");
_ = QueryContentBoundsAsync(source.Id);
_ = CaptureFrame(source.Id);
};
@@ -153,6 +159,35 @@ public sealed class WebView2Manager : IDisposable
catch (Exception ex) { AppLog.Write($"WebView2Manager: navigate failed: {ex.Message}"); }
}
// The page's real content rect (scrollWidth/Height) can be smaller than the
// viewport for widgets that don't fill it — the leftover area is dead space
// that must not stretch across the selection box. Crop the capture to the
// content bounds (anchored at 0,0) so the box shows exactly the widget.
private async Task QueryContentBoundsAsync(string sourceId)
{
if (!_sessions.TryGetValue(sourceId, out var session) || session.Disposed) return;
try
{
if (session.Control.CoreWebView2 is not { } cws) return;
var json = await cws.ExecuteScriptAsync(
"JSON.stringify({w:document.documentElement.scrollWidth||0," +
"h:document.documentElement.scrollHeight||0," +
"vw:window.innerWidth||0,vh:window.innerHeight||0})");
if (string.IsNullOrEmpty(json) || json == "null") return;
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
session.ViewportCssW = root.GetProperty("vw").GetDouble();
session.ViewportCssH = root.GetProperty("vh").GetDouble();
session.ContentCssW = root.GetProperty("w").GetDouble();
session.ContentCssH = root.GetProperty("h").GetDouble();
_ = CaptureFrame(sourceId);
}
catch (Exception ex)
{
AppLog.Write($"WebView2Manager: content bounds query failed: {ex.Message}");
}
}
private async Task CaptureFrame(string sourceId)
{
if (!_sessions.TryGetValue(sourceId, out var session) || session.Disposed) return;
@@ -187,11 +222,31 @@ public sealed class WebView2Manager : IDisposable
formatted.CopyPixels(pixels, pixW * 4, 0);
var stride = pixW * 4;
var wb = new WriteableBitmap(pixW, pixH, 96, 96, PixelFormats.Bgra32, null);
wb.WritePixels(new Int32Rect(0, 0, pixW, pixH), pixels, stride, 0);
int cropW = pixW, cropH = pixH;
if (session.ViewportCssW >= 1 && session.ViewportCssH >= 1 &&
session.ContentCssW >= 1 && session.ContentCssH >= 1)
{
var sx = pixW / session.ViewportCssW;
var sy = pixH / session.ViewportCssH;
cropW = (int)Math.Clamp(Math.Round(session.ContentCssW * sx), 1, pixW);
cropH = (int)Math.Clamp(Math.Round(session.ContentCssH * sy), 1, pixH);
}
var outPixels = pixels;
if (cropW != pixW || cropH != pixH)
{
var dstStride = cropW * 4;
outPixels = new byte[dstStride * cropH];
for (var r = 0; r < cropH; r++)
Buffer.BlockCopy(pixels, r * stride, outPixels, r * dstStride, dstStride);
}
var wb = new WriteableBitmap(cropW, cropH, 96, 96, PixelFormats.Bgra32, null);
wb.WritePixels(new Int32Rect(0, 0, cropW, cropH), outPixels, cropW * 4, 0);
wb.Freeze();
session.LatestFrame = new VideoFrame(pixW, pixH, pixels);
session.LatestFrame = new VideoFrame(cropW, cropH, outPixels);
_ = _dispatcher.BeginInvoke(() =>
{