diff --git a/SentryReplay/CameraLayoutPanel.cs b/SentryReplay/CameraLayoutPanel.cs
new file mode 100644
index 0000000..023db36
--- /dev/null
+++ b/SentryReplay/CameraLayoutPanel.cs
@@ -0,0 +1,117 @@
+using System.Windows;
+using System.Windows.Controls;
+
+namespace SentryReplay;
+
+///
+/// Arranges the four camera hosts without ever reparenting them: a 2x2 grid, or one primary view filling
+/// the top with the other three as a strip of tiles along the bottom. Switching views only re-arranges,
+/// so each Flyleaf surface resizes in place (no reparent flash). Identify each child with the attached
+/// ; drive the layout with .
+///
+public sealed class CameraLayoutPanel : Panel
+{
+ private const double Gap = 2;
+ private const double TileStripFraction = 0.22;
+
+ private static readonly string[] CameraOrder =
+ [
+ MainWindowViewModel.FrontCameraView,
+ MainWindowViewModel.RearCameraView,
+ MainWindowViewModel.LeftCameraView,
+ MainWindowViewModel.RightCameraView,
+ ];
+
+ public static readonly DependencyProperty CameraProperty = DependencyProperty.RegisterAttached(
+ "Camera",
+ typeof(string),
+ typeof(CameraLayoutPanel),
+ new PropertyMetadata(null));
+
+ public static string GetCamera(DependencyObject element) => (string)element.GetValue(CameraProperty);
+
+ public static void SetCamera(DependencyObject element, string value) => element.SetValue(CameraProperty, value);
+
+ public static readonly DependencyProperty SelectedCameraViewProperty = DependencyProperty.Register(
+ nameof(SelectedCameraView),
+ typeof(string),
+ typeof(CameraLayoutPanel),
+ new FrameworkPropertyMetadata(MainWindowViewModel.FrontCameraView, FrameworkPropertyMetadataOptions.AffectsArrange));
+
+ public string SelectedCameraView
+ {
+ get => (string)GetValue(SelectedCameraViewProperty);
+ set => SetValue(SelectedCameraViewProperty, value);
+ }
+
+ protected override Size MeasureOverride(Size availableSize)
+ {
+ foreach (UIElement child in InternalChildren)
+ {
+ child.Measure(availableSize);
+ }
+
+ return availableSize;
+ }
+
+ protected override Size ArrangeOverride(Size finalSize)
+ {
+ if (SelectedCameraView == MainWindowViewModel.GridCameraView)
+ {
+ ArrangeGrid(finalSize);
+ }
+ else
+ {
+ ArrangeSingle(finalSize);
+ }
+
+ return finalSize;
+ }
+
+ private void ArrangeGrid(Size size)
+ {
+ var cellWidth = (size.Width - Gap) / 2;
+ var cellHeight = (size.Height - Gap) / 2;
+ var right = cellWidth + Gap;
+ var bottom = cellHeight + Gap;
+
+ ArrangeCamera(MainWindowViewModel.FrontCameraView, new Rect(0, 0, cellWidth, cellHeight));
+ ArrangeCamera(MainWindowViewModel.RearCameraView, new Rect(right, 0, cellWidth, cellHeight));
+ ArrangeCamera(MainWindowViewModel.LeftCameraView, new Rect(0, bottom, cellWidth, cellHeight));
+ ArrangeCamera(MainWindowViewModel.RightCameraView, new Rect(right, bottom, cellWidth, cellHeight));
+ }
+
+ private void ArrangeSingle(Size size)
+ {
+ var stripHeight = size.Height * TileStripFraction;
+ var primaryHeight = Math.Max(0, size.Height - stripHeight - Gap);
+
+ ArrangeCamera(SelectedCameraView, new Rect(0, 0, size.Width, primaryHeight));
+
+ var tiles = CameraOrder.Where(camera => camera != SelectedCameraView).ToArray();
+ if (tiles.Length == 0)
+ {
+ return;
+ }
+
+ var tileWidth = (size.Width - (Gap * (tiles.Length - 1))) / tiles.Length;
+ var tileTop = primaryHeight + Gap;
+
+ for (var i = 0; i < tiles.Length; i++)
+ {
+ ArrangeCamera(tiles[i], new Rect(i * (tileWidth + Gap), tileTop, tileWidth, stripHeight));
+ }
+ }
+
+ private void ArrangeCamera(string camera, Rect rect)
+ {
+ foreach (UIElement child in InternalChildren)
+ {
+ if (GetCamera(child) == camera)
+ {
+ child.Arrange(rect);
+ return;
+ }
+ }
+ }
+}
diff --git a/SentryReplay/MainWindow.xaml b/SentryReplay/MainWindow.xaml
index bb1dad9..1fbbb39 100644
--- a/SentryReplay/MainWindow.xaml
+++ b/SentryReplay/MainWindow.xaml
@@ -417,14 +417,6 @@
-
-
-
-
-
-
-
@@ -439,113 +431,16 @@
BorderThickness="1"
ClipToBounds="True"
CornerRadius="6">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
+
+ CornerRadius="3" />
-
+
+ CornerRadius="3" />
-
+
+ CornerRadius="3" />
-
+
+ CornerRadius="3" />
-/// Main WPF window. Owns only view concerns: window lifecycle, Flyleaf host layout, the seek
-/// slider input plumbing, and search-box focus. All state, commands, and orchestration live in
-/// .
+/// Main WPF window. Owns only view concerns: window lifecycle, the seek-slider input plumbing, and
+/// search-box focus. The camera layout is handled by (no reparenting);
+/// all state, commands, and orchestration live in .
///
public partial class MainWindow : Window
{
@@ -27,10 +27,8 @@ public MainWindow()
_viewModel = new MainWindowViewModel(
() => VideoPlayerController.Create(FrontFlyleafHost, BackFlyleafHost, LeftFlyleafHost, RightFlyleafHost));
_viewModel.SearchBoxFocusRequested += OnSearchBoxFocusRequested;
- _viewModel.PropertyChanged += ViewModelOnPropertyChanged;
DataContext = _viewModel;
- UpdateCameraHostLayout();
}
private async void Window_ContentRendered(object sender, EventArgs e)
@@ -99,102 +97,12 @@ private void SeekSlider_ValueChanged(object sender, RoutedPropertyChangedEventAr
_viewModel.OnSeekSliderValueChanged();
}
- private void ViewModelOnPropertyChanged(object sender, PropertyChangedEventArgs e)
- {
- if (e.PropertyName == nameof(MainWindowViewModel.SelectedCameraView))
- {
- UpdateCameraHostLayout();
- }
- }
-
private void OnSearchBoxFocusRequested(object sender, EventArgs e)
{
SearchBox.Focus();
SearchBox.SelectAll();
}
- private void UpdateCameraHostLayout()
- {
- if (PrimaryCameraHostSlot is null)
- return;
-
- foreach (var (host, slot) in GetCameraHostLayout(_viewModel.SelectedCameraView))
- {
- MoveHostToSlot(host, slot);
- }
-
- // Force a synchronous layout pass so each moved host reaches its final bounds promptly. Note: a
- // brief flash of the reparented Flyleaf surface at its old size is a known limitation here and is
- // accepted (eliminating it would require not reparenting the hosts at all).
- UpdateLayout();
- }
-
- private IReadOnlyList<(FlyleafHost Host, ContentControl Slot)> GetCameraHostLayout(string view) => view switch
- {
- MainWindowViewModel.GridCameraView =>
- [
- (FrontFlyleafHost, GridFrontHostSlot),
- (BackFlyleafHost, GridRearHostSlot),
- (LeftFlyleafHost, GridLeftHostSlot),
- (RightFlyleafHost, GridRightHostSlot),
- ],
- MainWindowViewModel.RearCameraView =>
- [
- (BackFlyleafHost, PrimaryCameraHostSlot),
- (FrontFlyleafHost, FrontTileHostSlot),
- (LeftFlyleafHost, LeftTileHostSlot),
- (RightFlyleafHost, RightTileHostSlot),
- ],
- MainWindowViewModel.LeftCameraView =>
- [
- (LeftFlyleafHost, PrimaryCameraHostSlot),
- (FrontFlyleafHost, FrontTileHostSlot),
- (BackFlyleafHost, RearTileHostSlot),
- (RightFlyleafHost, RightTileHostSlot),
- ],
- MainWindowViewModel.RightCameraView =>
- [
- (RightFlyleafHost, PrimaryCameraHostSlot),
- (FrontFlyleafHost, FrontTileHostSlot),
- (BackFlyleafHost, RearTileHostSlot),
- (LeftFlyleafHost, LeftTileHostSlot),
- ],
- _ =>
- [
- (FrontFlyleafHost, PrimaryCameraHostSlot),
- (BackFlyleafHost, RearTileHostSlot),
- (LeftFlyleafHost, LeftTileHostSlot),
- (RightFlyleafHost, RightTileHostSlot),
- ],
- };
-
- private static void MoveHostToSlot(FlyleafHost host, ContentControl slot)
- {
- if (ReferenceEquals(slot.Content, host))
- return;
-
- RemoveHostFromParent(host);
- slot.Content = host;
- }
-
- private static void RemoveHostFromParent(FlyleafHost host)
- {
- switch (host.Parent)
- {
- case ContentControl contentControl when ReferenceEquals(contentControl.Content, host):
- contentControl.Content = null;
- break;
-
- case Panel panel:
- panel.Children.Remove(host);
- break;
-
- case Decorator decorator when ReferenceEquals(decorator.Child, host):
- decorator.Child = null;
- break;
- }
- }
-
private void Hyperlink_RequestNavigate(object sender, RequestNavigateEventArgs e)
{
if (e.Uri is null)