feat(audio dock): the shell — 168px, auto-collapsing, replacing the empty bottom bar
Unit 4 of the sound-bar refactor. `Controls/AudioDock` takes the bottom row that held the empty `BottomBar` spacer (deleted): four bay containers (MIC / GAME / TRAX / LIP-SYNC) above a 36px master strip, all token-styled. Height comes from `Models/AudioDockLayout`, not from a setting: 168px normally, 36px below a 900px window height. At the 1366×768 minimum a full dock leaves ~398px for the preview, so the collapse is mandatory, not cosmetic. The dock listens to its window's SizeChanged and re-applies. Nothing here pretends to be a meter yet — the bays and the master readouts fill in over the next units, each with its own test. Guard: AudioDockLayoutTests pins both sides of the threshold (768 collapsed, 1040 expanded), the boundary itself, and the two heights as the design's numbers.
This commit is contained in:
@@ -0,0 +1,58 @@
|
|||||||
|
<UserControl x:Class="ytLive.Controls.AudioDock"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
<!-- The sound bars. Replaces the empty bottom-bar spacer; collapses to the telemetry strip
|
||||||
|
on short windows (AudioDockLayout decides the height, not this file). -->
|
||||||
|
<Border x:Name="DockRoot" Background="{StaticResource Brush.SurfaceContainer}"
|
||||||
|
BorderBrush="{StaticResource Brush.Outline}" BorderThickness="0,1,0,0">
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="36"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- Four bays: MIC | GAME | TRAX | SYNC. Filled in by the bay units. -->
|
||||||
|
<Grid x:Name="Bays" Grid.Row="0" Margin="8,6,8,0">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<Border Grid.Column="0" Background="{StaticResource Brush.SurfaceContainerLow}" CornerRadius="{StaticResource Radius.Sm}" Margin="0,0,4,0" Padding="10,6">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="MIC" FontSize="10" FontWeight="SemiBold" Foreground="{StaticResource Brush.OnSurfaceVariant}"/>
|
||||||
|
<TextBlock x:Name="MicBayBody" Text="—" FontSize="11" Foreground="{StaticResource Brush.OnSurfaceDisabled}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
<Border Grid.Column="1" Background="{StaticResource Brush.SurfaceContainerLow}" CornerRadius="{StaticResource Radius.Sm}" Margin="4,0" Padding="10,6">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="GAME / PC" FontSize="10" FontWeight="SemiBold" Foreground="{StaticResource Brush.OnSurfaceVariant}"/>
|
||||||
|
<TextBlock x:Name="GameBayBody" Text="—" FontSize="11" Foreground="{StaticResource Brush.OnSurfaceDisabled}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
<Border Grid.Column="2" Background="{StaticResource Brush.SurfaceContainerLow}" CornerRadius="{StaticResource Radius.Sm}" Margin="4,0" Padding="10,6">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="TRAX" FontSize="10" FontWeight="SemiBold" Foreground="{StaticResource Brush.OnSurfaceVariant}"/>
|
||||||
|
<TextBlock x:Name="TraxBayBody" Text="—" FontSize="11" Foreground="{StaticResource Brush.OnSurfaceDisabled}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
<Border Grid.Column="3" Background="{StaticResource Brush.SurfaceContainerLow}" CornerRadius="{StaticResource Radius.Sm}" Margin="4,0,0,0" Padding="10,6">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="LIP-SYNC" FontSize="10" FontWeight="SemiBold" Foreground="{StaticResource Brush.OnSurfaceVariant}"/>
|
||||||
|
<TextBlock x:Name="SyncBayBody" Text="—" FontSize="11" Foreground="{StaticResource Brush.OnSurfaceDisabled}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Master strip: always visible, even collapsed — it is the telemetry line. -->
|
||||||
|
<Border x:Name="MasterStrip" Grid.Row="1" Background="{StaticResource Brush.SurfaceContainerLowest}" Padding="12,0">
|
||||||
|
<DockPanel VerticalAlignment="Center">
|
||||||
|
<TextBlock DockPanel.Dock="Left" Text="MASTER" FontSize="10" FontWeight="SemiBold"
|
||||||
|
Foreground="{StaticResource Brush.OnSurfaceVariant}" VerticalAlignment="Center"/>
|
||||||
|
</DockPanel>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using ytLive.Models;
|
||||||
|
|
||||||
|
namespace ytLive.Controls;
|
||||||
|
|
||||||
|
/// <summary>The sound bars (creator ruling 2026-10-30). This unit owns the SHELL: the piece of the
|
||||||
|
/// window it occupies and the collapse behaviour. The bays and the master readouts fill in over
|
||||||
|
/// the following units, so nothing here pretends to be a working meter.
|
||||||
|
///
|
||||||
|
/// <para>Collapse is driven by the window height, not a user setting: at 1366×768 a full dock
|
||||||
|
/// squeezes the preview, and the creator should not have to manage that.</para></summary>
|
||||||
|
public partial class AudioDock : UserControl
|
||||||
|
{
|
||||||
|
private Window? _window;
|
||||||
|
|
||||||
|
public AudioDock()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
Loaded += OnLoaded;
|
||||||
|
Unloaded += OnUnloaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Is the dock showing only the telemetry strip?</summary>
|
||||||
|
internal bool IsCollapsed { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>Test seam: the same code the window's SizeChanged runs, without a window.</summary>
|
||||||
|
internal void ApplyWindowHeight(double windowHeight)
|
||||||
|
{
|
||||||
|
var collapsed = AudioDockLayout.IsCollapsed(windowHeight);
|
||||||
|
IsCollapsed = collapsed;
|
||||||
|
Height = AudioDockLayout.HeightFor(windowHeight);
|
||||||
|
Bays.Visibility = collapsed ? Visibility.Collapsed : Visibility.Visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_window = Window.GetWindow(this);
|
||||||
|
if (_window == null)
|
||||||
|
{
|
||||||
|
ApplyWindowHeight(AudioDockLayout.CollapseBelowHeight); // no host: show the dock
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_window.SizeChanged += OnWindowSizeChanged;
|
||||||
|
ApplyWindowHeight(_window.ActualHeight > 0 ? _window.ActualHeight : _window.Height);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnUnloaded(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_window != null) _window.SizeChanged -= OnWindowSizeChanged;
|
||||||
|
_window = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnWindowSizeChanged(object sender, SizeChangedEventArgs e) => ApplyWindowHeight(e.NewSize.Height);
|
||||||
|
}
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<UserControl x:Class="ytLive.Controls.BottomBar"
|
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
|
||||||
<Border Background="#0f3460" Padding="16,4">
|
|
||||||
<Grid>
|
|
||||||
<!-- Intentionally empty. The hidden label keeps the bar's height unchanged. -->
|
|
||||||
<TextBlock Style="{StaticResource YtLabel}" Text=" " Visibility="Hidden"/>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
</UserControl>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
using System.Windows.Controls;
|
|
||||||
|
|
||||||
namespace ytLive.Controls;
|
|
||||||
|
|
||||||
public partial class BottomBar : UserControl
|
|
||||||
{
|
|
||||||
public BottomBar()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-1
@@ -148,7 +148,7 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
|
||||||
<controls:BottomBar Grid.Row="3"/>
|
<controls:AudioDock Grid.Row="3"/>
|
||||||
<!-- ═══ OVERLAY: Settings / Bug / Feature / About ═══ -->
|
<!-- ═══ OVERLAY: Settings / Bug / Feature / About ═══ -->
|
||||||
<controls:OverlayHost Grid.RowSpan="3"/>
|
<controls:OverlayHost Grid.RowSpan="3"/>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
namespace ytLive.Models;
|
||||||
|
|
||||||
|
/// <summary>How tall the audio dock should be for a given window height.
|
||||||
|
///
|
||||||
|
/// <para>Creator ruling (2026-10-30): the dock is 168px on a normal window and collapses to a 36px
|
||||||
|
/// telemetry strip when the window is short, because at 1366×768 a full dock leaves ~398px for the
|
||||||
|
/// preview and the composition starts letterboxing into a postage stamp.</para>
|
||||||
|
///
|
||||||
|
/// <para>Pure, so the switch is testable without a window.</para></summary>
|
||||||
|
public static class AudioDockLayout
|
||||||
|
{
|
||||||
|
/// <summary>Below this window height the dock collapses. The app's minimum is 768; the
|
||||||
|
/// default window is 1040, so a normal launch shows the full dock.</summary>
|
||||||
|
public const double CollapseBelowHeight = 900;
|
||||||
|
|
||||||
|
public const double ExpandedHeight = 168;
|
||||||
|
public const double CollapsedHeight = 36;
|
||||||
|
|
||||||
|
public static bool IsCollapsed(double windowHeight) => windowHeight < CollapseBelowHeight;
|
||||||
|
|
||||||
|
public static double HeightFor(double windowHeight)
|
||||||
|
=> IsCollapsed(windowHeight) ? CollapsedHeight : ExpandedHeight;
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using Xunit;
|
||||||
|
using ytLive.Models;
|
||||||
|
|
||||||
|
namespace ytLive.Tests;
|
||||||
|
|
||||||
|
/// <summary>The dock's height is the whole reason it is allowed to take 168px: on a short window it
|
||||||
|
/// must get out of the way instead of squeezing the preview. The threshold is the creator's
|
||||||
|
/// minimum (768) and default (1040) — both sides are pinned here so a future "tidy" cannot leave a
|
||||||
|
/// 768px window with a full dock.
|
||||||
|
/// </summary>
|
||||||
|
public class AudioDockLayoutTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void The_dock_collapses_on_short_windows_and_expands_on_normal_ones()
|
||||||
|
{
|
||||||
|
// Minimum window: collapsed, so the preview keeps its room.
|
||||||
|
Assert.True(AudioDockLayout.IsCollapsed(768));
|
||||||
|
Assert.Equal(AudioDockLayout.CollapsedHeight, AudioDockLayout.HeightFor(768));
|
||||||
|
|
||||||
|
// Default window: the full dock.
|
||||||
|
Assert.False(AudioDockLayout.IsCollapsed(1040));
|
||||||
|
Assert.Equal(AudioDockLayout.ExpandedHeight, AudioDockLayout.HeightFor(1040));
|
||||||
|
|
||||||
|
// The boundary itself belongs to the expanded side (>= is the friendlier read).
|
||||||
|
Assert.False(AudioDockLayout.IsCollapsed(AudioDockLayout.CollapseBelowHeight));
|
||||||
|
Assert.True(AudioDockLayout.IsCollapsed(AudioDockLayout.CollapseBelowHeight - 1));
|
||||||
|
|
||||||
|
// The two heights are the design's numbers, not whatever looked fine.
|
||||||
|
Assert.Equal(168, AudioDockLayout.ExpandedHeight);
|
||||||
|
Assert.Equal(36, AudioDockLayout.CollapsedHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user