Web source: stable viewport (snapshot at register) + Stretch=Fill + overflow:hidden — no reflow, no scrollbars, no dead space
This commit is contained in:
+16
-24
@@ -6,34 +6,26 @@
|
|||||||
working). **NOT pushed — user rule: never push without explicit instruction (2026-08-24).** No feature
|
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.
|
branches pre-1.0: all work lands on `main` per work unit.
|
||||||
|
|
||||||
## The web-source rendering work (2026-08-28) — CURRENT MODEL: viewport == element box
|
## The web-source rendering work (2026-08-28) — CURRENT MODEL: stable viewport + Fill
|
||||||
|
|
||||||
**Background:** the fixed-viewport fix (1920×1080 control + `Stretch="Uniform"`) made the page
|
**Background:** viewport==box made dragging the box reflow the page (truncation, scrollbars,
|
||||||
render without truncation but left dead space between the selection box and the content (box
|
unrecoverable image, "janky at stressful geometries"). Fixed-viewport + `Uniform` left letterbox
|
||||||
aspect ≠ capture aspect → letterbox; box is sized by the parent layout, capture is a separate
|
dead space. CSS reset enlarged the gap. Scrollbars were called "super fail".
|
||||||
hardcoded viewport — they bind to different rects). A CSS-reset experiment made the gap worse
|
|
||||||
and was shelved by the creator. He stressed the systems must be GEOMETRY-AGNOSTIC: no 16:9,
|
|
||||||
no fixed render size, no per-widget assumptions.
|
|
||||||
|
|
||||||
**The model that delivers it (implemented, needs visual verification):**
|
**The model that satisfies invariants (implemented, needs visual verification):**
|
||||||
- `Services/WebView2Manager.cs` — the WebView2 viewport IS the element box
|
- `Services/WebView2Manager.cs` — the page renders ONCE at the viewport size the element was
|
||||||
(`Source.Width × Source.Height`). The page renders at exactly the box size; a `PropertyChanged`
|
created at (snapshot in `Register`, never tracked → no reflow, no truncation during resize).
|
||||||
hook keeps the viewport locked to the box (re-triggers capture on resize, sub-0.5px drift is
|
`overflow:hidden` (html+body) + transparent-bg injection on every nav — nothing else, no
|
||||||
ignored). `CaptureFrame` is unchanged (aspect-preserving Physical-size capture).
|
layout/CSS meddling. Immediate capture on `NavigationCompleted`. `FramePump` unchanged.
|
||||||
- `MainWindow.xaml` — web `<Image>` uses `Stretch="Fill"` (capture aspect == box aspect by
|
- `MainWindow.xaml` — web `<Image>` uses `Stretch="Fill"`: any box the user draws is covered
|
||||||
construction, so Fill == Uniform with zero dead space; during a transient stale frame it just
|
edge-to-edge, zero dead space, zero crop (OBS free-transform: fixed browser resolution,
|
||||||
holds aspect briefly). Everything else still `UniformToFill`.
|
scene-item scales the texture). Aspect distortion only when the box aspect ≠ viewport aspect.
|
||||||
- Selection rect == content rect by construction → flush edges, no padding, works for any widget
|
- Invariants: **page viewport is stable; display Fill-scales texture to the box.**
|
||||||
geometry the creator throws at it. FramePump still reads `GetLatestFrame` via `ResolveOutputFrame`.
|
|
||||||
- Transparent-background JS injection retained (background only — no layout/CSS-reset meddling).
|
|
||||||
|
|
||||||
Build: 0 warnings; WebView2Manager tests 4/4 (size tracks source, sub-pixel drift ignored,
|
Build: 0 warnings; tests 4/4 (snapshot at register, no tracking after, min 1, transparent bg).
|
||||||
minimum 1, transparent bg).
|
|
||||||
|
|
||||||
**Landmines (earlier attempts — do not repeat):** fixed-viewport (letterbox dead space),
|
**Rejected for the record:** fixed 1920×1080+`Uniform` (letterbox), CSS margin/100% reset
|
||||||
CSS reset/margin juggling (enlarged the gap), control-tracking-box combined with
|
(enlarged gap), viewport==box (reflow/truncation/scrollbars).
|
||||||
`UniformToFill` display (truncation), ZoomFactor meddling. The working invariant is:
|
|
||||||
**viewport rect == selection rect, display = Fill.**
|
|
||||||
|
|
||||||
## Key facts / landmines
|
## Key facts / landmines
|
||||||
|
|
||||||
|
|||||||
+12
-22
@@ -40,10 +40,12 @@ public sealed class WebView2Manager : IDisposable
|
|||||||
{
|
{
|
||||||
if (_sessions.ContainsKey(source.Id)) return;
|
if (_sessions.ContainsKey(source.Id)) return;
|
||||||
|
|
||||||
// Geometry-agnostic OBS model: the browser viewport IS the element box.
|
// OBS model: the page renders ONCE at a stable viewport — the size the
|
||||||
// The page renders at exactly Source.Width × Source.Height, so the
|
// element was created at (snapshot, never tracked, so dragging the box
|
||||||
// captured frame and the selection rect are always the same rectangle —
|
// never reflows/truncates the page and no scrollbars appear). The
|
||||||
// no fixed render size, no aspect assumptions, no dead space or padding.
|
// captured texture is then stretched by the display (Stretch=Fill) into
|
||||||
|
// whatever box the user draws, so the frame always bounds content with
|
||||||
|
// zero dead space. Geometry-agnostic: any widget, any box.
|
||||||
var webView = new WebView2
|
var webView = new WebView2
|
||||||
{
|
{
|
||||||
Width = Math.Max(1, source.Width),
|
Width = Math.Max(1, source.Width),
|
||||||
@@ -69,22 +71,6 @@ public sealed class WebView2Manager : IDisposable
|
|||||||
|
|
||||||
_sessions[source.Id] = session;
|
_sessions[source.Id] = session;
|
||||||
|
|
||||||
// Interop sync: keep the viewport locked to the element box so the
|
|
||||||
// selection rect always bounds the actual rendered content.
|
|
||||||
source.PropertyChanged += (_, e) =>
|
|
||||||
{
|
|
||||||
if (e.PropertyName is not (nameof(Source.Width) or nameof(Source.Height))) return;
|
|
||||||
if (session.Disposed) return;
|
|
||||||
if (Math.Abs(session.Control.Width - source.Width) > 0.5 ||
|
|
||||||
Math.Abs(session.Control.Height - source.Height) > 0.5)
|
|
||||||
{
|
|
||||||
session.Control.Width = Math.Max(1, source.Width);
|
|
||||||
session.Control.Height = Math.Max(1, source.Height);
|
|
||||||
if (session.Initialized)
|
|
||||||
_ = CaptureFrame(source.Id);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
timer.Tick += (_, _) =>
|
timer.Tick += (_, _) =>
|
||||||
{
|
{
|
||||||
if (!session.Disposed && session.Initialized)
|
if (!session.Disposed && session.Initialized)
|
||||||
@@ -129,14 +115,18 @@ public sealed class WebView2Manager : IDisposable
|
|||||||
{
|
{
|
||||||
_ = cws.ExecuteScriptAsync(
|
_ = cws.ExecuteScriptAsync(
|
||||||
"document.documentElement.style.background='transparent';" +
|
"document.documentElement.style.background='transparent';" +
|
||||||
"document.body.style.background='transparent';");
|
"document.documentElement.style.overflow='hidden';" +
|
||||||
|
"document.body.style.background='transparent';" +
|
||||||
|
"document.body.style.overflow='hidden';");
|
||||||
};
|
};
|
||||||
|
|
||||||
cws.NavigationCompleted += (_, _) =>
|
cws.NavigationCompleted += (_, _) =>
|
||||||
{
|
{
|
||||||
_ = cws.ExecuteScriptAsync(
|
_ = cws.ExecuteScriptAsync(
|
||||||
"document.documentElement.style.background='transparent';" +
|
"document.documentElement.style.background='transparent';" +
|
||||||
"document.body.style.background='transparent';");
|
"document.documentElement.style.overflow='hidden';" +
|
||||||
|
"document.body.style.background='transparent';" +
|
||||||
|
"document.body.style.overflow='hidden';");
|
||||||
_ = CaptureFrame(source.Id);
|
_ = CaptureFrame(source.Id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -917,15 +917,17 @@ capturing the primary display (which causes the infinity mirror effect).
|
|||||||
8. ✅ Preview shows live web content (not just a blank rectangle)
|
8. ✅ Preview shows live web content (not just a blank rectangle)
|
||||||
9. ☐ Handle navigation errors, invalid URIs, timeout gracefully
|
9. ☐ Handle navigation errors, invalid URIs, timeout gracefully
|
||||||
|
|
||||||
**Rendering model (2026-08-28, geometry-agnostic — supersedes the earlier fixed-viewport model):**
|
**Rendering model (2026-08-28, stable-viewport + fill — the working invariant):**
|
||||||
the WebView2 viewport IS the element box (`Source.Width × Source.Height`); the page renders at
|
the page renders ONCE at the size the element was created at (snapshot in `Register`, never
|
||||||
exactly the box size and the captured frame is displayed with `Stretch="Fill"`, so the selection
|
tracked) — so dragging the box never reflows the page, never truncates it, and scrollbars are
|
||||||
rect always bounds the actual rendered content — no fixed render size, no aspect assumptions,
|
suppressed (`overflow:hidden` on html/body injection; background-transparent only otherwise,
|
||||||
no dead space/padding for any widget geometry. A `PropertyChanged` hook keeps the viewport
|
no layout meddling). The captured texture is displayed with `Stretch="Fill"`, so any box the
|
||||||
locked to the box; capture is re-triggered on resize. Graceful-error handling (item 9) is a
|
user draws is covered edge-to-edge with zero dead space (OBS free-transform behavior:
|
||||||
follow-up. Earlier attempts for the record: (a) fixed 1920×1080 viewport + `Stretch="Uniform"`
|
browser renders at a fixed resolution, the scene-item scales the texture). The invariants:
|
||||||
fixed the truncation but left box-vs-content gaps (box aspect ≠ capture aspect); (b) CSS reset
|
the page viewport is stable; the display Fill-scales it to the box. Graceful-error handling
|
||||||
injection made gaps larger (belayed by creator).
|
(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).
|
||||||
|
|
||||||
### Design decisions
|
### Design decisions
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ public sealed class WebView2ManagerTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void WebView2_Control_Size_Tracks_Source_Dimensions()
|
public void WebView2_Control_Size_Snapshots_Source_At_Register()
|
||||||
{
|
{
|
||||||
_app.Run(() =>
|
_app.Run(() =>
|
||||||
{
|
{
|
||||||
@@ -38,17 +38,11 @@ public sealed class WebView2ManagerTests
|
|||||||
var webView = panel.Children.OfType<WebView2>().First();
|
var webView = panel.Children.OfType<WebView2>().First();
|
||||||
Assert.Equal(640.0, webView.Width);
|
Assert.Equal(640.0, webView.Width);
|
||||||
Assert.Equal(480.0, webView.Height);
|
Assert.Equal(480.0, webView.Height);
|
||||||
|
|
||||||
source.Width = 1024;
|
|
||||||
source.Height = 768;
|
|
||||||
|
|
||||||
Assert.Equal(1024.0, webView.Width);
|
|
||||||
Assert.Equal(768.0, webView.Height);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void WebView2_Control_Size_Ignores_Sub_One_Pixel_Drift()
|
public void WebView2_Control_Size_Does_Not_Track_Box_After_Register()
|
||||||
{
|
{
|
||||||
_app.Run(() =>
|
_app.Run(() =>
|
||||||
{
|
{
|
||||||
@@ -65,10 +59,10 @@ public sealed class WebView2ManagerTests
|
|||||||
|
|
||||||
var webView = panel.Children.OfType<WebView2>().First();
|
var webView = panel.Children.OfType<WebView2>().First();
|
||||||
|
|
||||||
source.Width = 640.4;
|
source.Width = 1024;
|
||||||
Assert.Equal(640.0, webView.Width);
|
source.Height = 768;
|
||||||
|
|
||||||
source.Height = 480.4;
|
Assert.Equal(640.0, webView.Width);
|
||||||
Assert.Equal(480.0, webView.Height);
|
Assert.Equal(480.0, webView.Height);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user