Round clip: true circle (Viewbox/1x1 ellipse) + grabbable corner handle (transparent source grid, non-hit-testable selection overlay); resize locks square in round mode
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Shapes;
|
||||
using System.Windows.Threading;
|
||||
using Xunit;
|
||||
using ytLive.Models;
|
||||
using ytLive.ViewModels;
|
||||
|
||||
namespace ytLive.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Reproduces the real MainWindow preview to check the round-clip corner
|
||||
/// handle: whether a click at the corner actually stays inside the preview
|
||||
/// (doesn't fall through and deselect the source) and whether the round clip
|
||||
/// renders as a circle rather than an oval.
|
||||
/// </summary>
|
||||
public sealed class RoundClipInteractionTests
|
||||
{
|
||||
[Fact]
|
||||
public void Round_Clip_Corner_Is_Grabbable_And_Shape_Is_Circle()
|
||||
{
|
||||
Exception? failure = null;
|
||||
var thread = new Thread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
Run();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
failure = ex;
|
||||
}
|
||||
});
|
||||
thread.SetApartmentState(ApartmentState.STA);
|
||||
thread.Start();
|
||||
thread.Join();
|
||||
|
||||
if (failure != null)
|
||||
throw new Xunit.Sdk.XunitException("Round-clip interaction failed: " + failure);
|
||||
}
|
||||
|
||||
private void Run()
|
||||
{
|
||||
var app = new App();
|
||||
app.InitializeComponent();
|
||||
|
||||
var window = new MainWindow();
|
||||
var vm = (MainViewModel)window.DataContext;
|
||||
try
|
||||
{
|
||||
window.Show();
|
||||
window.UpdateLayout();
|
||||
|
||||
var scene = vm.ActiveScene!;
|
||||
var source = new Source
|
||||
{
|
||||
Name = "Webcam",
|
||||
Type = SourceType.Webcam,
|
||||
DeviceId = "test-camera",
|
||||
X = 1408,
|
||||
Y = 778,
|
||||
Width = 480,
|
||||
Height = 270,
|
||||
};
|
||||
scene.Sources.Add(source);
|
||||
vm.SelectedSource = source;
|
||||
window.UpdateLayout();
|
||||
|
||||
var previewGrid = (Grid)window.FindName("PreviewGrid")!;
|
||||
var canvasGrid = (Grid)window.FindName("CanvasGrid")!;
|
||||
var toWindow = canvasGrid.TransformToVisual(window);
|
||||
|
||||
var corner = new Point(source.X + source.Width, source.Y + source.Height);
|
||||
|
||||
foreach (var shape in new[] { ClipShape.Traditional, ClipShape.Round })
|
||||
{
|
||||
source.ClipShape = shape;
|
||||
window.UpdateLayout();
|
||||
|
||||
var cornerInWindow = toWindow.Transform(corner);
|
||||
|
||||
// A click on the corner must land inside the preview (descendant
|
||||
// of PreviewGrid); otherwise Window_PreviewMouseLeftButtonDown
|
||||
// deselects the source and the handle can never be grabbed.
|
||||
var hit = VisualTreeHelper.HitTest(window, cornerInWindow);
|
||||
var grabbable = hit != null && IsDescendantOf(hit.VisualHit, previewGrid);
|
||||
|
||||
Assert.True(grabbable,
|
||||
$"{shape}: corner click fell through to '{hit?.VisualHit.GetType().Name ?? "null"}' " +
|
||||
"so the source gets deselected before the resize handler runs");
|
||||
}
|
||||
|
||||
// The round clip must render as a circle (square bounding box), not an oval.
|
||||
source.ClipShape = ClipShape.Round;
|
||||
window.UpdateLayout();
|
||||
var ellipse = FindRoundEllipse(window);
|
||||
Assert.NotNull(ellipse);
|
||||
Assert.Equal(ellipse!.RenderSize.Width, ellipse.RenderSize.Height, 1.0);
|
||||
}
|
||||
finally
|
||||
{
|
||||
window.Close();
|
||||
}
|
||||
}
|
||||
|
||||
private static Ellipse? FindRoundEllipse(Window window)
|
||||
{
|
||||
return Walk(window, element => element is Ellipse e && e.IsVisible) as Ellipse;
|
||||
}
|
||||
|
||||
private static DependencyObject? Walk(DependencyObject parent, Func<DependencyObject, bool> predicate)
|
||||
{
|
||||
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
|
||||
{
|
||||
var child = VisualTreeHelper.GetChild(parent, i);
|
||||
if (predicate(child)) return child;
|
||||
var found = Walk(child, predicate);
|
||||
if (found != null) return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool IsDescendantOf(DependencyObject? child, DependencyObject ancestor)
|
||||
{
|
||||
while (child != null && !ReferenceEquals(child, ancestor))
|
||||
child = VisualTreeHelper.GetParent(child);
|
||||
return child != null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user