Files
LlamaCasty/ytLive.Tests/SelectedLayerPreviewHitTestingTests.cs
T
gramps 0fdd9d5faf fix(brand): one logo, not two — and hit-testing follows what you see
The brand exemption for the universal blur put a BrandLogoOverlay above the blur host while the
canvas ItemsControl kept drawing the same element, so two logos sat on top of each other.

- The canvas presenter for a brand logo is collapsed: BrandLogoOverlay is the only render, and it
  now carries the element's Opacity so the canvas copy is not missed.
- Hit-testing follows the eye: a click inside the logo's bounds selects the logo, because it is the
  topmost layer in the compositor too. Without that you saw the logo on top and grabbed the webcam
  underneath it.
- SelectedLayerPreviewHitTestingTests updated to the new contract (collapsed presenter, logo wins
  its bounds, list selection still works). 3 tests pass; Release 0 warnings.
2026-11-01 16:12:38 -08:00

128 lines
5.0 KiB
C#

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<Source>().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;
}
}