diff --git a/Controls/LeftPanel.xaml b/Controls/LeftPanel.xaml
index 340b264..b8c1312 100644
--- a/Controls/LeftPanel.xaml
+++ b/Controls/LeftPanel.xaml
@@ -284,6 +284,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Controls/index.md b/Controls/index.md
index 6f1de28..6c587cc 100644
--- a/Controls/index.md
+++ b/Controls/index.md
@@ -12,7 +12,7 @@ See [`schema.md`](../schema.md) for the memory-map conventions.
| `TopBar.xaml(.cs)` | Row 0 session surface (2026-09-22 redesign): brand + gear menu (left), one segmented REC|ON-AIR mode switch + world actions (Record: Start Recording · Stream: Go Live + Test) + reality line while running (dot/word/elapsed) + End (center), account zone — Sign In or avatar w/ Change Account/Logout context menu (right). `RefreshAvatar()`, `GearButton_Click` |
| `PreviewPane.xaml(.cs)` | Row 1 Col 1 center: the 1920×1080 `OverlayCanvas`/`SelectionOverlay`/`BrandFlashLayer`/`SocialBarElement`, text pull-out drawer, and the live-controls row (TRAX / game / mic meters + sliders + speakers + socials) |
| `SceneThumbnailStrip.xaml(.cs)` | Row 0 scene-picker strip (`ItemsControl` over `Scenes`), staged-red / live-green border highlight |
-| `LeftPanel.xaml(.cs)` | Row 1 Col 0 left panel: edit mode → layers+properties; live → YouTube chat. Source add/edit/remove, list drag-to-reorder, snapshot revert-accept |
+| `LeftPanel.xaml(.cs)` | Row 1 Col 0 left panel: edit mode → layers+properties; live → YouTube chat. Source add/edit/remove, list drag-to-reorder, snapshot revert-accept. **TASK 51:** the webcam properties carry the Background section — `RemoveBackgroundPill` / `BlurBackgroundPill` (`PillToggle`, radio-exclusive via `WebcamSceneConfig.RemoveBackground`/`BlurBackground`) + `BackgroundStrengthSlider` (0..1, 5% snaps, shown only while an effect is on) |
| `OverlayHost.xaml(.cs)` | Root-grid overlay dialogs: Settings / Report Bug / Feature Request / About (+ licenses + license-key entry). `BackToAbout_Click` lives here |
| `SceneThumbnailStrip.xaml.cs` | Code-behind for the strip above |
| `LeftPanel.xaml.cs` | Code-behind for the panel above |
diff --git a/HANDOFF.md b/HANDOFF.md
index 37a87ab..b5f90c7 100644
--- a/HANDOFF.md
+++ b/HANDOFF.md
@@ -1,50 +1,47 @@
-# HANDOFF — 2026-10-09 (TASK 51 slice 1 shipped: background-effects engine)
+# HANDOFF — 2026-10-09 (TASK 51 webcam background Remove + Blur — BUILT)
## Branch
-- main (no feature branch). Pushed through `f8dd107`; the TASK 51 slice-1 commit sits on top,
- **not pushed** (push at the user's call).
+- main (no feature branch). Pushed through `f8dd107`; everything after is local, **not pushed**
+ (push at the creator's call).
-## Dirty files (NOT mine — do NOT commit or co-edit)
+## Dirty files (NOT mine — do NOT commit)
- The in-flight "pinned chat" work unit: `Models/YouTube.cs`, `ViewModels/MainViewModel.Chat.cs`,
- `Controls/LeftPanel.xaml`, untracked `Helpers/RelayCommandGeneric.cs`.
+ untracked `Helpers/RelayCommandGeneric.cs`, and **its hunks in `Controls/LeftPanel.xaml`**
+ (lines ~599–610, the chat area). TASK 51 slice 5 edited the SAME file in a different region (the
+ webcam properties, ~line 287); only the TASK 51 hunk was staged (`git apply --cached` of that hunk
+ alone) — the pinned-chat hunks are still unstaged and untouched.
- The creator's untracked logo art in `Assets/` (`llamacasty text.*`, `llamacasty-text-nobg*.png`,
`thin logo wordmark.png`).
-## What just happened — TASK 51 slice 1 (engine)
-- **Model:** Google's official MediaPipe Selfie Segmenter (landscape, 144×256, Apache-2.0),
- converted to ONNX by our own `tools/selfie-seg/convert_tflite_to_onnx.py` and **verified against
- MediaPipe's own runtime** (`verify_against_mediapipe.py`, max |diff| 5e-5). Embedded in the exe
- (`Assets/Models/selfie_segmenter_landscape.onnx`, 430 KB, in git).
-- **Code:** `Services/BackgroundEffects/` — `IPersonSegmenter` seam, `OnnxPersonSegmenter`
- (onnxruntime 1.31.0 CPU, never throws), `BackgroundEffectProcessor` (None passthrough, Remove =
- alpha mask, Blur = passthrough until slice 2). `Models/BackgroundEffect.cs` enum.
-- **Not wired:** nothing calls it yet — no config, UI, or camera hookup. Zero risk to the live path.
-- **Docs:** ai.md (stale "background removal PERMANENTLY OUT" bullet replaced by the reversal + a
- "Webcam background effects" section + a model-provenance licensing guard), THIRD-PARTY-NOTICES
- items 11 (ONNX Runtime, MIT) + 12 (MediaPipe model, Apache-2.0 with §4b change note),
- TASK 51 file + TASKS.md row, Services/Models indexes, MyMistakes conversion recipe.
+## What just happened — TASK 51, all five slices
+1. **Engine** — Google's MediaPipe Selfie Segmenter (landscape, Apache-2.0) converted to ONNX by
+ `tools/selfie-seg/` and verified vs MediaPipe's own runtime; embedded resource; onnxruntime CPU.
+2. **Blur** — background at ¼ res, 3 box passes, mask-blended; Remove gets edge feather.
+3. **Config** — `WebcamSceneConfig.BackgroundEffect` + `BackgroundStrength`, persisted (additive cols).
+4. **Hookup** — `CameraManager` renders per-scene effect **variants** (one segmentation per raw frame
+ on a latest-frame-drop worker; raw path untouched); VM `SyncWebcamEffects()` declares/adopts them.
+ Synthetic 720p Debug: segment 1.6 ms, Remove 3.3 ms, Blur 17.5 ms.
+5. **UI** — Background section in the webcam's Layers properties: Remove / Blur `PillToggle`s,
+ radio-exclusive, + Strength slider (visible while an effect is on).
+Also this session: 64-bit requirement (`PlatformTarget=x64`), GPL-3.0-or-later licensing, README
+rewrite, TASK 55 (disk hygiene) + TASK 56 (secret scan before going public) queued.
## Tests
-- Full suite **408 passed / 0 failed**, 0 build warnings. New: `BackgroundEffectEngineTests` (Remove
- integration test with a fake segmenter; bundled-model contract test vs a MediaPipe reference mask,
- fixtures in `ytLive.Tests/Fixtures/`).
+- Full suite **412 passed / 0 failed**, 0 build warnings. New: `BackgroundEffectEngineTests` (Remove,
+ Blur, bundled-model contract), `CameraManagerTests` (variant reaches compositor + preview, raw
+ stays raw), `LayoutStorePersistenceTests` (per-scene roundtrip + legacy migration),
+ `WebcamBackgroundPillTests` (radio semantics + XAML wiring).
+- Known environment flakes (not regressions): `Mix_HonorsProviderGains…` (fails while system audio
+ plays), `RealMouseDrag…` (needs an idle desktop).
## Known issues / follow-ups
-- **64-bit only (2026-10-09):** `PlatformTarget=x64` in both csproj files (verified: ytLive.dll PE
- machine 0x8664, 408/408 green). TASK 48 item 2 carries the x64-only MSIX + `-r win-x64` publish.
-- `onnxruntime.dll` is ~17 MB; an RID-less build copies every RID's native folder — TASK 48's MSIX
- publish must pin win-x64.
-- **License: GPL-3.0-or-later** (2026-10-09: `LICENSE`, `ADDITIONAL-TERMS.md` with §7 b/c/e, icon +
- logo all rights reserved, AI-generated backgrounds/alert video = GPL placeholders; both files ship
- beside the exe). Source offer = `https://llgit.llamachile.tube/gramps/LlamaCasty` (Gitea; the
- creator toggles it public), in the notices header + README. **Before it goes public: scan git
- history for secrets.** README rewritten (creator's voice, generic comparison, real build steps).
- Still stale and NOT yet rewritten: ai.md "Windows packaging" (Store IAP / branding-flash paid
- delta) and "Monetization" sections, MARCOM.md asset table (`llama-logo.png` doesn't exist).
+- **Creator's real-camera check (TASK 51):** toggle Remove/Blur on a webcam, watch edges/lighting,
+ and read `startup.log` → "background effects avg … ms/frame".
+- `onnxruntime.dll` ~17 MB; TASK 48's MSIX publish must pin `-r win-x64` (x64-only already enforced).
+- Still stale, not rewritten: ai.md "Windows packaging" (Store IAP / branding-flash paid delta) and
+ "Monetization" sections; MARCOM.md asset table (`llama-logo.png` doesn't exist).
+- TASK 56: secret-scan the full history BEFORE the Gitea repo (`gramps/LlamaCasty`) goes public.
- Carried: "Use default background" toggle still not working (from 2026-10-07).
## Next step
-- TASK 51 slice 2: **Blur** in `BackgroundEffectProcessor` (separable box/gaussian, strength), one
- integration test. Then slice 3 config + persistence (`BackgroundEffect` + `Strength`), slice 4
- camera-path hookup + perf check on the creator's box, slice 5 UI: radio-exclusive Remove/Blur pill
- switches + strength slider in the webcam's Layers properties.
+- Creator runs the app: webcam → Layers properties → Background → Remove / Blur + Strength.
diff --git a/Models/WebcamSceneConfig.cs b/Models/WebcamSceneConfig.cs
index 2e3ed75..2a84665 100644
--- a/Models/WebcamSceneConfig.cs
+++ b/Models/WebcamSceneConfig.cs
@@ -26,9 +26,13 @@ public class WebcamSceneConfig : SceneElement
if (!Set(ref _backgroundEffect, value)) return;
Raise(nameof(RemoveBackground));
Raise(nameof(BlurBackground));
+ Raise(nameof(HasBackgroundEffect));
}
}
+ /// True while Remove or Blur is on — shows the strength slider.
+ public bool HasBackgroundEffect => BackgroundEffect != BackgroundEffect.None;
+
private double _backgroundStrength = 0.5;
/// TASK 51: 0..1 — Blur amount, or Remove's edge softness (persisted).
public double BackgroundStrength
diff --git a/Models/index.md b/Models/index.md
index a1915a1..3c7a68e 100644
--- a/Models/index.md
+++ b/Models/index.md
@@ -12,7 +12,7 @@ Plain data types. No logic beyond what a property can carry. See
| `ZoomSceneElement.cs` | **Transient zoom box (TASK 52, creator-corrected 2026-10-08):** carries the box rect AND the anchored `CaptureX`/`CaptureY` (the fixed box/2 capture window set on `ActivateZoom`; moving the box never touches it). `IsZoomActive` flips `Kind` Static→Dynamic (leaves the baked base while active); never persisted (LayoutStore save skips non-Source/non-WebcamSceneConfig rows), never shown in the layers list or the element template (`IsZoomLayer` gates both). The compositor SKIPS the active element in every layer loop and draws its 2x content LAST (layer n+1) via `ZoomMagnifier`. |
| `Webcam.cs` | Singleton webcam identity: `Id`, `DeviceId`, `Name` (one row app-wide) |
| `BackgroundEffect.cs` | **TASK 51:** per-webcam background effect enum `None` / `Remove` / `Blur`, carried by `WebcamSceneConfig` |
-| `WebcamSceneConfig.cs` | Per-scene webcam placement (subclass of `SceneElement`): geometry + `IsVisible` + border (`BorderColor`/`BorderOpacity`/`BorderWidth`/`BorderAnimation`) + `VideoImageSource`; `WebcamId` links to `Webcam`; **TASK 51:** `BackgroundEffect` + `BackgroundStrength` (0..1, default 0.5) + the radio-pill surface `RemoveBackground`/`BlurBackground` (one on turns the other off; both off = None) |
+| `WebcamSceneConfig.cs` | Per-scene webcam placement (subclass of `SceneElement`): geometry + `IsVisible` + border (`BorderColor`/`BorderOpacity`/`BorderWidth`/`BorderAnimation`) + `VideoImageSource`; `WebcamId` links to `Webcam`; **TASK 51:** `BackgroundEffect` + `BackgroundStrength` (0..1, default 0.5) + the radio-pill surface `RemoveBackground`/`BlurBackground` (one on turns the other off; both off = None) + `HasBackgroundEffect` (shows the strength slider) |
| `QualityOption.cs` | Resolution tier record `(Display, Fps, Bitrate, Width, Height)`; `Label` formats as `1080p60 (8Mbps)` — drives the bottom-bar dropdown and the output-rect math |
| `StreamConfig.cs` | `StreamConfig` (note: `TargetBitrate`/`Resolution` defaults are stale — not yet wired to the dropdown), `StreamHealth` (bitrate/FPS/dropped/duration), `StreamStatus` enum |
| `MicStatus.cs` | Footer mic indicator: `NotConnected` / `Connected` / `Problem` — the state behind the MIC button's status dot (red = no device/capture off, green = capturing, yellow = a requested mic failed: in use/unplugged) |
diff --git a/Services/CameraManager.cs b/Services/CameraManager.cs
index 1b4e156..ad6fbff 100644
--- a/Services/CameraManager.cs
+++ b/Services/CameraManager.cs
@@ -380,14 +380,24 @@ public sealed class CameraManager : IDisposable
}, DispatcherPriority.Render);
}
- // Caller holds _gate.
+ // Caller holds _gate. A new key inherits the last frame + bitmap of a dropped variant with
+ // the SAME effect (a strength change), so neither the stream nor the preview blanks for a
+ // frame while the creator drags the strength slider.
private static void SyncVariants(CameraSession session, HashSet keys)
{
- foreach (var stale in session.Effects.Keys.Where(k => !keys.Contains(k)).ToList())
+ var dropped = session.Effects.Where(kv => !keys.Contains(kv.Key)).ToList();
+ foreach (var (stale, _) in dropped)
session.Effects.Remove(stale);
foreach (var key in keys)
- if (!session.Effects.ContainsKey(key))
- session.Effects[key] = new EffectVariant();
+ {
+ if (session.Effects.ContainsKey(key)) continue;
+ var heir = dropped.FirstOrDefault(kv => kv.Key.Effect == key.Effect).Value;
+ session.Effects[key] = new EffectVariant
+ {
+ LatestFrame = heir?.LatestFrame,
+ PreviewBitmap = heir?.PreviewBitmap,
+ };
+ }
}
private void ScheduleEffects(CameraSession session)
diff --git a/TASKS.md b/TASKS.md
index caf4314..4b07233 100644
--- a/TASKS.md
+++ b/TASKS.md
@@ -66,7 +66,7 @@
| 48 | Distribution & packaging: **MSIX + Store IAP** | 🔶 In progress — **✅ route decided 2026-09-27 (Store MSIX + Store IAP)**; **item 1 (bundle ffmpeg) SHIPPED 2026-09-28** | [`TASKS/task-48-distribution-msix.md`](TASKS/task-48-distribution-msix.md) |
| 49 | Chat profanity filter (local, opt-in, non-persistent, user word list) | ☐ Queued (2026-09-27) | [`TASKS/task-49-chat-profanity-filter.md`](TASKS/task-49-chat-profanity-filter.md) |
| 50 | One-button "Publish this video" (resumable upload of the local recording) | ☐ Queued (2026-09-28) | [`TASKS/task-50-publish-recording.md`](TASKS/task-50-publish-recording.md) |
-| 51 | **Webcam background effects: Remove + Blur** | 🔶 **v1 requirement — in progress**: slice 1 engine shipped 2026-10-09 (bundled MediaPipe Selfie Segmenter → ONNX, verified vs Google's runtime; Remove); next: Blur → config → camera hookup → UI | [`TASKS/task-51-webcam-bg-effects.md`](TASKS/task-51-webcam-bg-effects.md) |
+| 51 | **Webcam background effects: Remove + Blur** | ✅ **Built 2026-10-09** (v1 requirement) — bundled MediaPipe Selfie Segmenter → ONNX (verified vs Google's runtime), Remove + Blur with strength, per-scene config persisted, one processing point in CameraManager, radio-pill UI in the webcam's Layers properties. Pending: creator's real-camera check | [`TASKS/task-51-webcam-bg-effects.md`](TASKS/task-51-webcam-bg-effects.md) |
| 52 | **Zoom box (anchored 2× magnifier)** | ✅ Done — shipped 2026-10-08 (3 creator corrections during the build: loupe → anchored capture over the full composite, drawn last → never shows the dim shading) | [`TASKS/task-52-zoom-box.md`](TASKS/task-52-zoom-box.md) |
| 53 | **Output-format verification: Shorts/TikTok geometry (9:16)** | ☐ Queued (2026-10-08) — output formats never tested end-to-end; pin + prove the vertical geometry | [`TASKS/task-53-output-formats.md`](TASKS/task-53-output-formats.md) |
| 54 | **Marcom round: free-forever positioning + merch storefront** | 🔶 In progress — llamacasty.com copy live 2026-10-08 (pricing stripped); merch POD + app About links queued | [`TASKS/task-54-marcom-free-merch.md`](TASKS/task-54-marcom-free-merch.md) |
diff --git a/TASKS/task-51-webcam-bg-effects.md b/TASKS/task-51-webcam-bg-effects.md
index 72161c3..6fcad08 100644
--- a/TASKS/task-51-webcam-bg-effects.md
+++ b/TASKS/task-51-webcam-bg-effects.md
@@ -4,8 +4,9 @@
> **Booked 2026-10-08 by creator ruling** — *"we're doing the bg removal and blur for v1 - book it, Danno"*.
> This **reverses** the 2026-10-05 "background removal: out of product" exclusion (TASK 3 item 19).
-**Status:** 🔶 In progress — **slice 1 (engine: bundled model + runner + Remove) shipped 2026-10-09**.
-Next: slice 2 (Blur), then config + persistence, then the camera-path hookup + UI.
+**Status:** ✅ **Built 2026-10-09 — all five slices shipped** (engine, Blur, config + persistence,
+camera hookup, UI). Remaining: the creator's manual check on a real camera (quality, lighting,
+`startup.log` perf line).
**Goal:** per-webcam **Background Remove** and **Background Blur** on the live webcam layer,
selected per element in its properties panel, composited into stream + recording like any
@@ -72,7 +73,7 @@ delivery shape changed:
- ✅ `WebcamSceneConfig` gains `BackgroundEffect` (`None | Remove | Blur`) + `BackgroundStrength` (slice 3).
- ✅ Persist via the existing `WebcamSceneConfig` table path in `LayoutStore.Save.cs`/
`Load.cs` (new columns + migration). Follow `LayoutStore.Migrations.cs` conventions.
-- ☐ **Properties UI (creator spec 2026-10-09):** in the webcam's properties in the **Layers
+- ✅ **Properties UI (creator spec 2026-10-09, slice 5):** in the webcam's properties in the **Layers
panel**, two **pill switches** — "Remove background" and "Blur background" — using the existing
`PillToggle` style (`Themes/Controls.xaml`, already used in `Controls/LeftPanel.xaml`). They act
as **radio buttons**: turning one on turns the other off; at most one is active; both off =
@@ -104,7 +105,7 @@ delivery shape changed:
- ✅ Separate work unit after the zoom build (zoom shipped 2026-10-08).
- Slice order: 1 engine ✅ → 2 Blur ✅ → 3 config + persistence ✅ → 4 camera-path hookup ✅ (one
- processing point, both consumers) + perf check → 5 properties-panel UI.
+ processing point, both consumers) + perf check → 5 properties-panel UI ✅.
## Related (not duplicated here)
diff --git a/ViewModels/MainViewModel.Webcam.cs b/ViewModels/MainViewModel.Webcam.cs
index f5389aa..a2cd6b1 100644
--- a/ViewModels/MainViewModel.Webcam.cs
+++ b/ViewModels/MainViewModel.Webcam.cs
@@ -244,11 +244,18 @@ public partial class MainViewModel
config.PropertyChanged += OnWebcamConfigEffectChanged;
var key = EffectKeyFor(config);
var bitmap = _cameraManager.GetPreviewBitmap(_webcam.DeviceId, key);
- if (bitmap != null || key.Effect != BackgroundEffect.None)
- config.VideoImageSource = bitmap;
+ // A strength change within the same effect keeps showing the previous variant's
+ // last frame until the new one renders (no blank flicker while dragging the slider);
+ // switching INTO an effect shows nothing first, so Remove never flashes the room.
+ var sameEffect = _shownEffect.TryGetValue(config, out var shown) && shown.Effect == key.Effect;
+ _shownEffect[config] = key;
+ if (bitmap != null) config.VideoImageSource = bitmap;
+ else if (key.Effect != BackgroundEffect.None && !sameEffect) config.VideoImageSource = null;
}
}
+ private readonly Dictionary _shownEffect = new();
+
private void OnWebcamConfigEffectChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName is not (nameof(WebcamSceneConfig.BackgroundEffect) or nameof(WebcamSceneConfig.BackgroundStrength)))
diff --git a/ai.md b/ai.md
index a47f0d6..ebcbc46 100644
--- a/ai.md
+++ b/ai.md
@@ -663,6 +663,13 @@ This replaces the old five-seeder cluster (`Seed{Starting,Brb,Ending,Chat}Backgr
per pixel) — the first naive per-pixel 2-D bilinear cost 46/80 ms. `CameraManager` logs the real
average once per session after 120 processed frames (`startup.log`: "background effects avg …").
`MainViewModel.PersonSegmenterOverride` is the hermetic-test seam.
+- **UI (slice 5, creator spec 2026-10-09):** the webcam's properties in the Layers panel
+ (`Controls/LeftPanel.xaml`) gain a **Background** section — two `PillToggle` switches, Remove and
+ Blur, bound to `RemoveBackground`/`BlurBackground` (radio-exclusive: one on turns the other off,
+ turning the active one off = None) and a Strength slider (0..1, 5% snaps, visible only while an
+ effect is on; Blur = amount, Remove = edge softness). Dragging the slider never blanks the webcam:
+ a new strength variant inherits the dropped same-effect variant's last frame + bitmap
+ (`CameraManager.SyncVariants`), and the VM keeps the shown bitmap until the new one renders.
- **Tests:** `BackgroundEffectEngineTests` — the Remove integration test (fake segmenter), and a
model contract test that runs the BUNDLED model through the C# preprocessing on a fixture and
matches the MediaPipe-produced reference mask (≥99.5% agreement; fixture = MediaPipe's public test
diff --git a/ytLive.Tests/WebcamBackgroundPillTests.cs b/ytLive.Tests/WebcamBackgroundPillTests.cs
new file mode 100644
index 0000000..a374a72
--- /dev/null
+++ b/ytLive.Tests/WebcamBackgroundPillTests.cs
@@ -0,0 +1,63 @@
+using System.Linq;
+using System.Xml.Linq;
+using Xunit;
+using ytLive.Models;
+
+namespace ytLive.Tests;
+
+/// TASK 51 slice 5 — the creator's UI spec (2026-10-09): Remove and Blur are two pill
+/// switches in the webcam's Layers properties that act as radio buttons (one on turns the other
+/// off; both off = no effect), plus a strength slider.
+public class WebcamBackgroundPillTests
+{
+ [Fact]
+ public void Remove_and_Blur_pills_are_radio_exclusive_and_wired_in_the_webcam_properties()
+ {
+ // Behaviour: the bools the pills bind to.
+ var config = new WebcamSceneConfig();
+ var raised = new System.Collections.Generic.List();
+ config.PropertyChanged += (_, e) => raised.Add(e.PropertyName);
+
+ config.RemoveBackground = true;
+ Assert.Equal(BackgroundEffect.Remove, config.BackgroundEffect);
+ Assert.True(config.HasBackgroundEffect);
+
+ raised.Clear();
+ config.BlurBackground = true; // turning Blur on turns Remove off
+ Assert.Equal(BackgroundEffect.Blur, config.BackgroundEffect);
+ Assert.False(config.RemoveBackground);
+ Assert.Contains(nameof(WebcamSceneConfig.RemoveBackground), raised); // Remove pill un-checks
+ Assert.Contains(nameof(WebcamSceneConfig.BlurBackground), raised);
+
+ config.RemoveBackground = false; // turning the INACTIVE pill off changes nothing
+ Assert.Equal(BackgroundEffect.Blur, config.BackgroundEffect);
+
+ config.BlurBackground = false; // turning the active pill off = None
+ Assert.Equal(BackgroundEffect.None, config.BackgroundEffect);
+ Assert.False(config.HasBackgroundEffect);
+
+ // Wiring: the two PillToggle switches + the strength slider live in the webcam's
+ // properties section of the Layers panel, bound to those properties.
+ XNamespace x = "http://schemas.microsoft.com/winfx/2006/xaml";
+ var doc = XDocument.Load(SourceAssert.RepoPath("Controls", "LeftPanel.xaml"));
+ var webcamPanel = doc.Descendants()
+ .Single(e => e.Name.LocalName == "StackPanel"
+ && ((string?)e.Attribute("Visibility") ?? "").Contains("SelectedElement.IsWebcam"));
+
+ XElement Named(string name) => webcamPanel.Descendants().Single(e => (string?)e.Attribute(x + "Name") == name);
+
+ foreach (var (pill, property) in new[] { ("RemoveBackgroundPill", "RemoveBackground"), ("BlurBackgroundPill", "BlurBackground") })
+ {
+ var toggle = Named(pill);
+ Assert.Equal("ToggleButton", toggle.Name.LocalName);
+ Assert.Contains("PillToggle", (string?)toggle.Attribute("Style"));
+ Assert.Contains($"SelectedElement.{property}", (string?)toggle.Attribute("IsChecked"));
+ Assert.Contains("TwoWay", (string?)toggle.Attribute("IsChecked"));
+ }
+
+ var slider = Named("BackgroundStrengthSlider");
+ Assert.Contains("SelectedElement.BackgroundStrength", (string?)slider.Attribute("Value"));
+ Assert.Equal("0", (string?)slider.Attribute("Minimum"));
+ Assert.Equal("1", (string?)slider.Attribute("Maximum"));
+ }
+}