diff --git a/HANDOFF.md b/HANDOFF.md index 7504c78..e9e1e09 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -6,28 +6,34 @@ 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 fix (2026-08-28) — USER-CONFIRMED WORKING +## The web-source rendering work (2026-08-28) — CURRENT MODEL: viewport == element box -**The bug:** preloaded web resources rendered truncated (upper-left only) and misbehaved as the box -resized. The WebView2 control was being resized to the element box as it changed. +**Background:** the fixed-viewport fix (1920×1080 control + `Stretch="Uniform"`) made the page +render without truncation but left dead space between the selection box and the content (box +aspect ≠ capture aspect → letterbox; box is sized by the parent layout, capture is a separate +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 fix — one idea, three-line change, the OBS/CEF pattern:** -- `Services/WebView2Manager.cs` — viewport is FIXED at the master canvas size (1920×1080), never - tracks the box. Control lives off-screen at (-5000,-5000). Page renders full at its designed size; - the preview scales the captured frame independently via ``. This is what - OBS obs-browser does (fixed-viewport texture + independent scene-item scale). Removed the - PropertyChanged resize hook. Added an immediate `CaptureFrame` on `NavigationCompleted` so content - appears instantly on load (was waiting for the next 100ms tick). -- `ViewModels/MainViewModel.cs` — `InitWebView2` now calls `RegisterLoadedWebSources()` itself; - before, registration only happened when `LoadLayout` ran after manager init, so preloaded sources - never registered at startup. -- `MainWindow.xaml` — scene-element `` had a LOCAL `Stretch="UniformToFill"` that outranked - the Style DataTrigger (WPF precedence). Removed the local attribute; Style default is - `UniformToFill`; `IsWebSource` trigger overrides to `Stretch="Uniform"` (no crop/truncation). -- `ytLive.Tests/WebView2ManagerTests.cs` — 3 scale-sync tests tested the removed behavior; replaced - with 2 tests asserting the fixed 1920×1080 viewport and that it does not track source dimensions. +**The model that delivers it (implemented, needs visual verification):** +- `Services/WebView2Manager.cs` — the WebView2 viewport IS the element box + (`Source.Width × Source.Height`). The page renders at exactly the box size; a `PropertyChanged` + hook keeps the viewport locked to the box (re-triggers capture on resize, sub-0.5px drift is + ignored). `CaptureFrame` is unchanged (aspect-preserving Physical-size capture). +- `MainWindow.xaml` — web `` uses `Stretch="Fill"` (capture aspect == box aspect by + construction, so Fill == Uniform with zero dead space; during a transient stale frame it just + holds aspect briefly). Everything else still `UniformToFill`. +- Selection rect == content rect by construction → flush edges, no padding, works for any widget + 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, 0 errors. User verified it renders correctly. +Build: 0 warnings; WebView2Manager tests 4/4 (size tracks source, sub-pixel drift ignored, +minimum 1, transparent bg). + +**Landmines (earlier attempts — do not repeat):** fixed-viewport (letterbox dead space), +CSS reset/margin juggling (enlarged the gap), control-tracking-box combined with +`UniformToFill` display (truncation), ZoomFactor meddling. The working invariant is: +**viewport rect == selection rect, display = Fill.** ## Key facts / landmines @@ -44,7 +50,7 @@ Build: 0 warnings, 0 errors. User verified it renders correctly. - No `CODING.md`; conventions live in `AGENTS.md` + `ai.md`. ## Next steps -1. Await creator comments on the committed rendering fix; address what he raises; also testing - the box-vs-content padding fix (CSS reset + (0,0) anchor) just committed. +1. AWAIT creator's verdict on viewport==box (geometry-agnostic) rendering — visual check with a + widget URL at several box sizes; the selection frame must snap flush with no padding. 2. Follow-up only if the creator asks: graceful handling of navigation errors / invalid URIs (TASK 17 ×9). 3. Continue queue: TASK 18 local recording, TASK 21 media source, TASK 22 audio sync offset. \ No newline at end of file diff --git a/MainWindow.xaml b/MainWindow.xaml index 6078750..7a3ad3d 100644 --- a/MainWindow.xaml +++ b/MainWindow.xaml @@ -856,9 +856,7 @@ - - - + diff --git a/Services/WebView2Manager.cs b/Services/WebView2Manager.cs index a0d43dd..3e1dbe0 100644 --- a/Services/WebView2Manager.cs +++ b/Services/WebView2Manager.cs @@ -15,20 +15,6 @@ namespace ytLive.Services; public sealed class WebView2Manager : IDisposable { - // Eliminates the wrapper padding/whitespace between the box and the page: - // clears UA margins/paddings, forces html/body to fill the viewport and - // anchors content at (0,0) so the rendered frame matches the widget's size. - private const string CssResetInjection = - "(function(){" + - "var s=document.createElement('style');" + - "s.textContent='html,body{margin:0!important;padding:0!important;" + - "width:100%!important;height:100%!important;overflow:hidden!important;" + - "background:transparent!important}';" + - "(document.head||document.documentElement).appendChild(s);" + - "if(document.documentElement)document.documentElement.style.background='transparent';" + - "if(document.body)document.body.style.background='transparent';" + - "})();"; - private readonly Panel _hostPanel; private readonly Dispatcher _dispatcher; private readonly Dictionary _sessions = new(); @@ -54,13 +40,14 @@ public sealed class WebView2Manager : IDisposable { if (_sessions.ContainsKey(source.Id)) return; - // OBS pattern: the browser viewport is the canvas size (1920×1080), - // never the element box. The page renders full at its designed size; - // the preview scales the captured frame independently (Stretch=Uniform). + // Geometry-agnostic OBS model: the browser viewport IS the element box. + // The page renders at exactly Source.Width × Source.Height, so the + // captured frame and the selection rect are always the same rectangle — + // no fixed render size, no aspect assumptions, no dead space or padding. var webView = new WebView2 { - Width = 1920, - Height = 1080, + Width = Math.Max(1, source.Width), + Height = Math.Max(1, source.Height), DefaultBackgroundColor = System.Drawing.Color.Transparent, Visibility = System.Windows.Visibility.Visible, }; @@ -82,6 +69,22 @@ public sealed class WebView2Manager : IDisposable _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 += (_, _) => { if (!session.Disposed && session.Initialized) @@ -122,11 +125,18 @@ public sealed class WebView2Manager : IDisposable var cws = session.Control.CoreWebView2!; - await cws.AddScriptToExecuteOnDocumentCreatedAsync(CssResetInjection); + cws.NavigationStarting += (_, _) => + { + _ = cws.ExecuteScriptAsync( + "document.documentElement.style.background='transparent';" + + "document.body.style.background='transparent';"); + }; cws.NavigationCompleted += (_, _) => { - _ = cws.ExecuteScriptAsync(CssResetInjection); + _ = cws.ExecuteScriptAsync( + "document.documentElement.style.background='transparent';" + + "document.body.style.background='transparent';"); _ = CaptureFrame(source.Id); }; diff --git a/TASKS.md b/TASKS.md index c8178cf..b068c6a 100644 --- a/TASKS.md +++ b/TASKS.md @@ -917,18 +917,15 @@ 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, the fix that works):** WebView2 viewport is FIXED at the master -canvas size (1920×1080) in `Register`, never tracking the element box — the OBS/CEF pattern -(fixed render viewport, display box scaled independently via `Stretch="Uniform"`). Capturing the -box-sized viewport was the truncation bug. Display `Stretch`: web → `Uniform` (no crop), -everything else → `UniformToFill`. Graceful-error handling (item 9) is a follow-up. - -**Box-vs-content padding (2026-08-28):** whitespace gaps between the bounding box and the web -content were the page's own UA margins/padding + body sizing to content instead of the viewport. -Fixed by injecting a CSS reset into every document via -`CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync` (margin/padding 0, html/body 100% of -viewport, overflow hidden, transparent bg) and anchoring the web `` at (0,0) -(`HorizontalAlignment/VerticalAlignment=Left/Top`) inside the element box. +**Rendering model (2026-08-28, geometry-agnostic — supersedes the earlier fixed-viewport model):** +the WebView2 viewport IS the element box (`Source.Width × Source.Height`); the page renders at +exactly the box size and the captured frame is displayed with `Stretch="Fill"`, so the selection +rect always bounds the actual rendered content — no fixed render size, no aspect assumptions, +no dead space/padding for any widget geometry. A `PropertyChanged` hook keeps the viewport +locked to the box; capture is re-triggered on resize. Graceful-error handling (item 9) is a +follow-up. Earlier attempts for the record: (a) fixed 1920×1080 viewport + `Stretch="Uniform"` +fixed the truncation but left box-vs-content gaps (box aspect ≠ capture aspect); (b) CSS reset +injection made gaps larger (belayed by creator). ### Design decisions diff --git a/ytLive.Tests/WebView2ManagerTests.cs b/ytLive.Tests/WebView2ManagerTests.cs index af37326..18f6158 100644 --- a/ytLive.Tests/WebView2ManagerTests.cs +++ b/ytLive.Tests/WebView2ManagerTests.cs @@ -20,29 +20,7 @@ public sealed class WebView2ManagerTests } [Fact] - public void WebView2_Control_Size_Is_Fixed_At_Canvas_Resolution() - { - _app.Run(() => - { - var panel = new Canvas(); - var source = new Source - { - Type = SourceType.WebSource, - Width = 640, - Height = 480, - }; - - using var manager = new WebView2Manager(panel, Dispatcher.CurrentDispatcher); - manager.Register(source); - - var webView = panel.Children.OfType().First(); - Assert.Equal(1920.0, webView.Width); - Assert.Equal(1080.0, webView.Height); - }); - } - - [Fact] - public void WebView2_Control_Size_Does_Not_Track_Source_Dimensions() + public void WebView2_Control_Size_Tracks_Source_Dimensions() { _app.Run(() => { @@ -58,12 +36,62 @@ public sealed class WebView2ManagerTests manager.Register(source); var webView = panel.Children.OfType().First(); + Assert.Equal(640.0, webView.Width); + Assert.Equal(480.0, webView.Height); source.Width = 1024; source.Height = 768; - Assert.Equal(1920.0, webView.Width); - Assert.Equal(1080.0, webView.Height); + Assert.Equal(1024.0, webView.Width); + Assert.Equal(768.0, webView.Height); + }); + } + + [Fact] + public void WebView2_Control_Size_Ignores_Sub_One_Pixel_Drift() + { + _app.Run(() => + { + var panel = new Canvas(); + var source = new Source + { + Type = SourceType.WebSource, + Width = 640, + Height = 480, + }; + + using var manager = new WebView2Manager(panel, Dispatcher.CurrentDispatcher); + manager.Register(source); + + var webView = panel.Children.OfType().First(); + + source.Width = 640.4; + Assert.Equal(640.0, webView.Width); + + source.Height = 480.4; + Assert.Equal(480.0, webView.Height); + }); + } + + [Fact] + public void WebView2_Control_Size_Minimum_Is_One() + { + _app.Run(() => + { + var panel = new Canvas(); + var source = new Source + { + Type = SourceType.WebSource, + Width = 0, + Height = 0, + }; + + using var manager = new WebView2Manager(panel, Dispatcher.CurrentDispatcher); + manager.Register(source); + + var webView = panel.Children.OfType().First(); + Assert.Equal(1.0, webView.Width); + Assert.Equal(1.0, webView.Height); }); }