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:
+20
-14
@@ -6,26 +6,32 @@
|
||||
working). **NOT pushed — user rule: never push without explicit instruction (2026-08-24).** No feature
|
||||
branches pre-1.0: all work lands on `main` per work unit.
|
||||
|
||||
## The web-source rendering work (2026-08-28) — CURRENT MODEL: stable viewport + Fill
|
||||
## The web-source rendering work (2026-08-28) — CURRENT MODEL: stable viewport + content-bounds crop
|
||||
|
||||
**Background:** viewport==box made dragging the box reflow the page (truncation, scrollbars,
|
||||
unrecoverable image, "janky at stressful geometries"). Fixed-viewport + `Uniform` left letterbox
|
||||
dead space. CSS reset enlarged the gap. Scrollbars were called "super fail".
|
||||
dead space. CSS reset enlarged the gap. Scrollbars were called "super fail". The right/bottom
|
||||
dead-space + "not anchored at (0,0)" report persisted across every model — dead space was
|
||||
page-internal (widget content smaller than the viewport), which is now cropped out.
|
||||
|
||||
**The model that satisfies invariants (implemented, needs visual verification):**
|
||||
- `Services/WebView2Manager.cs` — the page renders ONCE at the viewport size the element was
|
||||
created at (snapshot in `Register`, never tracked → no reflow, no truncation during resize).
|
||||
`overflow:hidden` (html+body) + transparent-bg injection on every nav — nothing else, no
|
||||
layout/CSS meddling. Immediate capture on `NavigationCompleted`. `FramePump` unchanged.
|
||||
- `MainWindow.xaml` — web `<Image>` uses `Stretch="Fill"`: any box the user draws is covered
|
||||
edge-to-edge, zero dead space, zero crop (OBS free-transform: fixed browser resolution,
|
||||
scene-item scales the texture). Aspect distortion only when the box aspect ≠ viewport aspect.
|
||||
- Invariants: **page viewport is stable; display Fill-scales texture to the box.**
|
||||
**The model that satisfies invariants (implemented, visual verification pending):**
|
||||
- `Services/WebView2Manager.cs` — stable viewport = the element's size at `Register` (snapshot,
|
||||
never tracked → no reflow/truncation during resize). `overflow:hidden` (html+body) +
|
||||
transparent-bg injection on every nav (nothing else). After each navigation,
|
||||
`QueryContentBoundsAsync` reads the page's real content rect
|
||||
(`scrollWidth/scrollHeight` vs `innerWidth/innerHeight` via `ExecuteScriptAsync` + JSON) and
|
||||
`CaptureFrame` **crops the capture to the actual content bounds, anchored at (0,0)** — dead
|
||||
space inside the viewport no longer reaches the box. Immediate capture on `NavigationCompleted`.
|
||||
- `MainWindow.xaml` — web `<Image>` uses `Stretch="Fill"`: the cropped frame maps edge-to-edge,
|
||||
so the selection box always bounds the real widget. `FramePump` unchanged.
|
||||
- Invariants: **page viewport stable; display Fill-crops to content and fills the box.**
|
||||
|
||||
Build: 0 warnings; tests 4/4 (snapshot at register, no tracking after, min 1, transparent bg).
|
||||
**Property panel / sliders (2026-08-28):** web URI ✕ clears the textbox; web ✓/✕ icon buttons
|
||||
`IsTabStop="False"` so Tab walks X→Y→W→H→URI; Slider style `IsMoveToPointEnabled="True"`
|
||||
restores click-anywhere-on-bar.
|
||||
|
||||
**Rejected for the record:** fixed 1920×1080+`Uniform` (letterbox), CSS margin/100% reset
|
||||
(enlarged gap), viewport==box (reflow/truncation/scrollbars).
|
||||
Build: 0 warnings; tests 4/4. Rejected for the record: fixed 1920×1080+`Uniform` (letterbox),
|
||||
CSS margin/100% reset (enlarged gap), viewport==box (reflow/truncation/scrollbars).
|
||||
|
||||
## Key facts / landmines
|
||||
|
||||
|
||||
+2
-2
@@ -546,13 +546,13 @@
|
||||
<TextBlock Text="URI" Foreground="#a0a0b0" FontSize="11" Margin="0,0,0,2"/>
|
||||
<DockPanel>
|
||||
<Button DockPanel.Dock="Right" Style="{StaticResource IconButton}"
|
||||
Width="22" Height="22" Margin="4,0,0,0"
|
||||
Width="22" Height="22" Margin="4,0,0,0" IsTabStop="False"
|
||||
Click="WebAccept_Click">
|
||||
<Path Data="M4,9 L8,14 L16,4" Stroke="#22c55e" StrokeThickness="2"
|
||||
Width="14" Height="14" Stretch="Uniform"/>
|
||||
</Button>
|
||||
<Button DockPanel.Dock="Right" Style="{StaticResource IconButton}"
|
||||
Width="22" Height="22" ToolTip="Revert"
|
||||
Width="22" Height="22" ToolTip="Revert" IsTabStop="False"
|
||||
Click="WebRevert_Click" Margin="4,0,0,0">
|
||||
<Path Data="M5,5 L15,15 M15,5 L5,15" Stroke="#e94560" StrokeThickness="2"
|
||||
Width="14" Height="14" Stretch="Uniform"/>
|
||||
|
||||
+2
-1
@@ -859,7 +859,8 @@ public partial class MainWindow : Window
|
||||
private void WebRevert_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_snapshotElement is Models.Source src)
|
||||
src.WebUri = _snapshotWebUri;
|
||||
src.WebUri = string.Empty;
|
||||
_snapshotWebUri = string.Empty;
|
||||
}
|
||||
|
||||
private void WebAccept_Click(object sender, RoutedEventArgs e)
|
||||
|
||||
@@ -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(() =>
|
||||
{
|
||||
|
||||
@@ -917,17 +917,22 @@ capturing the primary display (which causes the infinity mirror effect).
|
||||
8. ✅ Preview shows live web content (not just a blank rectangle)
|
||||
9. ☐ Handle navigation errors, invalid URIs, timeout gracefully
|
||||
|
||||
**Rendering model (2026-08-28, stable-viewport + fill — the working invariant):**
|
||||
**Rendering model (2026-08-28, stable viewport + content-bounds crop):**
|
||||
the page renders ONCE at the size the element was created at (snapshot in `Register`, never
|
||||
tracked) — so dragging the box never reflows the page, never truncates it, and scrollbars are
|
||||
suppressed (`overflow:hidden` on html/body injection; background-transparent only otherwise,
|
||||
no layout meddling). The captured texture is displayed with `Stretch="Fill"`, so any box the
|
||||
user draws is covered edge-to-edge with zero dead space (OBS free-transform behavior:
|
||||
browser renders at a fixed resolution, the scene-item scales the texture). The invariants:
|
||||
the page viewport is stable; the display Fill-scales it to the box. Graceful-error handling
|
||||
(item 9) is a follow-up. Attempts rejected for the record: fixed 1920×1080 + `Uniform`
|
||||
(letterbox dead space), CSS margin/100% reset (enlarged the gap), viewport==box (page reflow
|
||||
→ truncation/scrollbars/jank).
|
||||
tracked) — dragging the box never reflows the page; scrollbars suppressed via
|
||||
`overflow:hidden` html/body injection (background-transparent only otherwise). After each
|
||||
navigation the manager queries the page's real content rect (`scrollWidth/scrollHeight` vs
|
||||
`innerWidth/innerHeight`) and **crops the capture to the actual content bounds, anchored at
|
||||
(0,0)** — widgets that don't fill their viewport no longer stretch dead space across the box.
|
||||
`Stretch="Fill"` maps the cropped frame edge-to-edge (box is covered; the fill/adornment rect
|
||||
snaps to it). Graceful-error handling (item 9) is a follow-up. Rejected for the record:
|
||||
fixed-viewport 1920×1080 + `Uniform` (letterbox), CSS margin/100% reset (enlarged gaps),
|
||||
viewport==box (page reflow → truncation/scrollbars).
|
||||
|
||||
**Properties panel (2026-08-28):** web URI ✓/✕ icon buttons are `IsTabStop="False"` so Tab flows
|
||||
X→Y→W→H→URI; the ✕ button now clears the URI textbox (was reverting to the pre-accept snapshot).
|
||||
Slider style gained `IsMoveToPointEnabled="True"` — click-anywhere-on-bar jumps the thumb to the
|
||||
click (volume sliders keep their manual `SetSliderValueFromClick`, harmless duplication).
|
||||
|
||||
### Design decisions
|
||||
|
||||
|
||||
@@ -352,6 +352,7 @@
|
||||
<Style TargetType="Slider">
|
||||
<Setter Property="Height" Value="20"/>
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
<Setter Property="IsMoveToPointEnabled" Value="True"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Slider">
|
||||
|
||||
Reference in New Issue
Block a user