using System; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using Microsoft.Data.Sqlite; using Xunit; using ytLive.Controls; using ytLive.Models; using ytLive.Services; using ytLive.ViewModels; namespace ytLive.Tests; [Collection("RealApp")] public sealed class SelectedLayerPreviewHitTestingTests { private readonly RealAppHost _app; public SelectedLayerPreviewHitTestingTests(RealAppHost app) => _app = app; [Fact] public void ListSelectedBrandLogo_WinsPreviewHitTesting_UntilDeselected() { _app.Run(Run); } private static void Run() { var tempDb = System.IO.Path.Combine(System.IO.Path.GetTempPath(), $"ytLlive-selected-layer-{Guid.NewGuid():N}.db"); MainViewModel.LayoutPathOverride = tempDb; MainWindow? window = null; try { using (var schema = new LayoutStore(tempDb)) { } SqliteConnection.ClearAllPools(); window = new MainWindow(); var vm = (MainViewModel)window.DataContext; var scene = vm.StagedScene!; var logo = scene.Elements.OfType().Single(source => source.IsBrandLogo); logo.X = 420; logo.Y = 300; logo.Width = 300; logo.Height = 180; var webcam = new WebcamSceneConfig { WebcamId = "test-camera", Name = "Webcam", X = logo.X, Y = logo.Y, Width = logo.Width, Height = logo.Height, }; scene.Elements.Add(webcam); var originalOrder = scene.Elements.ToArray(); window.Show(); window.Activate(); window.Topmost = true; window.UpdateLayout(); var leftPanel = (LeftPanel)window.FindName("LeftPanel")!; var sourceList = (ListBox)leftPanel.FindName("SourceList")!; var preview = (PreviewPane)window.FindName("PreviewPane")!; var canvasGrid = (FrameworkElement)preview.FindName("CanvasGrid")!; var overlayElements = (ItemsControl)preview.FindName("OverlayElements")!; var logoContainer = (UIElement)overlayElements.ItemContainerGenerator.ContainerFromItem(logo)!; var webcamContainer = (UIElement)overlayElements.ItemContainerGenerator.ContainerFromItem(webcam)!; var pointInCanvas = new Point(logo.X + 100, logo.Y + 60); var pointInWindow = canvasGrid.TransformToVisual(window).Transform(pointInCanvas); // The brand logo is drawn in BrandLogoOverlay above the blur host (2026-11-01), so its // canvas presenter is collapsed and the overlay is what the eye sees. Hit-testing // follows the eye: the logo wins a click inside its bounds. Assert.Equal(Visibility.Collapsed, logoContainer.Visibility); Assert.Same(logo, preview.HitElementAt(vm, pointInCanvas)); sourceList.SelectedItem = logo; window.UpdateLayout(); Assert.True(ReferenceEquals(logo, vm.SelectedElement), "Selecting the layer-list row must select the brand logo."); Assert.Same(logo, preview.HitElementAt(vm, pointInCanvas)); // …and with nothing selected the logo still wins inside its own bounds, because it is // drawn above every layer. The webcam shares that rect in this test, so it is the // covered case: you reach it from the layer list, not by clicking through the logo. vm.SelectedElement = null; window.UpdateLayout(); Assert.Same(logo, preview.HitElementAt(vm, new Point(logo.X + 5, logo.Y + 5))); Assert.Equal(originalOrder, scene.Elements); sourceList.SelectedItem = null; window.UpdateLayout(); Assert.Null(vm.SelectedElement); // The logo still owns a click inside its bounds after deselection (it is the topmost // layer in both the compositor and the preview), and the canvas presenter stays // collapsed because BrandLogoOverlay is what draws it. Assert.Equal(Visibility.Collapsed, logoContainer.Visibility); Assert.Same(logo, preview.HitElementAt(vm, pointInCanvas)); Assert.Equal(originalOrder, scene.Elements); } finally { window?.Close(); MainViewModel.LayoutPathOverride = null; SqliteConnection.ClearAllPools(); try { System.IO.File.Delete(tempDb); } catch { } } } private static SceneElement? ElementAt(System.Windows.IInputElement? hit) { DependencyObject? current = hit as DependencyObject; while (current != null) { if (current is FrameworkElement { DataContext: SceneElement element }) return element; current = current is Visual or System.Windows.Media.Media3D.Visual3D ? VisualTreeHelper.GetParent(current) : LogicalTreeHelper.GetParent(current); } return null; } }