A Blazor-style Razor UI component for a soundboard panel in s&box. It renders the UI for boards/tabs, search, pad grid, voice meter, controls (loop, coop, kill), handles input (click, wheel), caching/filtering of pads, and reacts to various manager services for playback, coop, preload and achievements.
@using System;
@using System.Collections.Generic;
@using Sandbox;
@using Sandbox.UI;
@inherits PanelComponent
@namespace SSoundboard
<root class="sb @LayoutClass @(SoundBoardManager.ActiveVoiceCount > 0 ? "has-voices" : "")">
<div class="sb-shell">
<header class="sb-topbar">
<div class="sb-brand-block">
<span class="sb-status-led @(SoundBoardManager.ActiveVoiceCount > 0 ? "is-live" : "")"></span>
<div class="sb-brand-copy">
<span class="sb-title">S&Soundboard</span>
</div>
</div>
<div class="sb-voice-chip">
<div class="sb-voice-chip-head">
<span class="sb-voice-chip-label">VOICES</span>
<span class="sb-voice-chip-value" style="color: @ActiveVoiceColor">@SoundBoardManager.ActiveVoiceCount</span>
</div>
<div class="sb-voice-meter-stack">
@for ( var tier = 0; tier < VoiceMeterTierCount; tier++ )
{
var state = GetVoiceMeterTier( tier );
<div class="sb-voice-meter-tier">
<div class="sb-voice-meter-fill @(state.IsActive ? "is-active" : "") @(state.IsFull ? "is-full" : "") @(state.IsChaos ? "is-chaos" : "")"
style="width: @(state.FillPercent)%; background-color: @state.Color"></div>
</div>
}
</div>
</div>
</header>
<div class="sb-tabs-bar">
<div class="sb-tabs-arrow @(CanScrollTabsLeft ? "" : "is-disabled")"
onmousedown="@(() => ScrollTabs( -1 ))">‹</div>
<div @ref="_tabsNav" class="sb-tabs-scroll">
<div @ref="_tabsTrack" class="sb-tabs-track"
style="margin-left: @(-_tabsScrollX)px">
@if ( IsSearchTab )
{
<div class="sb-tab-chip is-active is-search">
<span class="sb-tab-chip-icon">⌕</span>
<span class="sb-tab-chip-label">SEARCH</span>
</div>
}
@foreach ( var board in SoundBoardData.Boards )
{
<div class="sb-tab-chip @(!IsSearchTab && ActiveBoardId == board.Id ? "is-active" : "")"
onmousedown="@(() => SetBoard( board.Id ))">
<span class="sb-tab-chip-icon">@board.Icon</span>
<span class="sb-tab-chip-label">@board.Label</span>
</div>
}
</div>
</div>
<div class="sb-tabs-arrow @(CanScrollTabsRight ? "" : "is-disabled")"
onmousedown="@(() => ScrollTabs( 1 ))">›</div>
</div>
<div class="sb-search-bar">
<span class="sb-search-icon">⌕</span>
<TextEntry class="sb-search-input"
Placeholder="Search all tabs… e.g. dolphin, ocean"
Text=@SearchQuery
OnTextEdited=@OnSearchEdited />
@if ( HasSearchQuery )
{
<div class="sb-search-clear" onmousedown="@ClearSearch">✕</div>
<span class="sb-search-count">@ActivePadCount match@(ActivePadCount == 1 ? "" : "es")</span>
}
else
{
<span class="sb-search-count">@BoardPadCount</span>
}
</div>
<section @ref="_boardArea" class="sb-board">
<div @ref="_boardScroll" class="sb-board-scroll">
<div class="sb-pad-content" style="min-height: @(PadGridMinHeight)px">
@if ( ActivePadCount == 0 )
{
<div class="sb-empty">
@if ( HasSearchQuery )
{
<span class="sb-empty-title">No matches</span>
<span class="sb-empty-body">No sounds match “@SearchQuery.Trim()” across all tabs</span>
}
else
{
<span class="sb-empty-title">No sounds</span>
<span class="sb-empty-body">This board is empty</span>
}
</div>
}
else
{
<div class="tile-grid sb-pad-grid">
@foreach ( var pad in ActivePads )
{
var remoteName = "";
var remoteSteamId = 0L;
var hasRemote = false;
if ( pad is not null && SoundBoardCoop.TryGetHighlight( pad.SoundId, out var remote ) )
{
hasRemote = true;
remoteName = remote.PlayerName ?? "";
remoteSteamId = remote.SteamId;
}
<div class="tile @(GetPadClass( pad, hasRemote ))"
onmousedown="@(() => PlayPad( pad ))">
<div class="tile-swatch" style="background-color: @(pad?.Color ?? "#ede19e")">
@if ( hasRemote && remoteSteamId != 0 )
{
<div class="tile-remote-avatar"
style="background-image: url( avatar:@remoteSteamId )"></div>
}
@if ( hasRemote && !string.IsNullOrWhiteSpace( remoteName ) )
{
<div class="tile-remote-name">@ShortPlayerName( remoteName )</div>
}
<div class="tile-vignette"></div>
</div>
<div class="tile-label">@ShortLabel( pad?.Label )</div>
</div>
}
</div>
}
</div>
</div>
</section>
<footer class="sb-controls">
<div class="sb-toggle-row">
<div class="sb-toggle @(SoundBoardManager.LoopEnabled ? "is-on" : "is-off")"
onmousedown="@OnLoopPressed">
LOOP
</div>
<div class="sb-toggle @(SoundBoardCoop.Enabled ? "is-on is-coop" : "is-off")"
onmousedown="@OnCoopPressed">
@(SoundBoardCoop.Enabled ? "CO-OP" : "SINGLE")
</div>
</div>
<div class="sb-now-playing">
<div class="sb-now-playing-dot @(SoundBoardManager.ActiveVoiceCount > 0 ? "is-live" : "")"></div>
<div class="sb-now-playing-text">@FooterStatusText</div>
</div>
<div class="btn danger sb-kill-btn" onmousedown="@OnKillPressed">☠ KILL ALL</div>
</footer>
</div>
@if ( SoundBoardAchievements.ToastVisible )
{
<div class="sb-ach">
<div class="sb-ach-icon">🔥</div>
<div class="sb-ach-copy">
<div class="sb-ach-kicker">ACHIEVEMENT UNLOCKED</div>
<div class="sb-ach-title">@SoundBoardAchievements.ToastTitle</div>
<div class="sb-ach-body">@SoundBoardAchievements.ToastBody</div>
</div>
</div>
}
</root>
@code
{
const int TileLabelHeight = 42;
const int TileBorder = 2;
const int GridGap = 12;
const int GridPadTop = 4;
const int GridPadBottom = 24;
const int VoicesPerTier = 7;
const int VoiceMeterTierCount = 4;
const string SearchBoardId = "search";
string ActiveBoardId { get; set; } = "arnold";
string SearchQuery { get; set; } = "";
string _boardBeforeSearch = "arnold";
int _screenWidth;
int _screenHeight;
Panel _boardArea;
Panel _boardScroll;
Panel _tabsNav;
Panel _tabsTrack;
// Manual horizontal offset for category tabs (ScrollSize is unreliable in s&box UI).
float _tabsScrollX;
const float TabsScrollStep = 200f;
const float TabChipEstimateWidth = 128f;
// Cached pad list for the active tab (meme tab can be 1000+ pads).
string _padsBoardId;
IReadOnlyList<SoundPad> _boardPads = Array.Empty<SoundPad>();
// Search / filtered view cache.
string _filterBoardId;
string _filterQuery;
IReadOnlyList<SoundPad> _filteredPads = Array.Empty<SoundPad>();
bool HasSearchQuery => !string.IsNullOrWhiteSpace( SearchQuery );
bool IsSearchTab => HasSearchQuery || ActiveBoardId == SearchBoardId;
bool CanScrollTabsLeft => _tabsScrollX > 1f;
bool CanScrollTabsRight => _tabsScrollX < GetTabsMaxScroll() - 1f;
IReadOnlyList<SoundPad> BoardPads
{
get
{
if ( IsSearchTab )
return Array.Empty<SoundPad>(); // search uses global pool, not a board
if ( _padsBoardId != ActiveBoardId || _boardPads is null )
{
_padsBoardId = ActiveBoardId;
_boardPads = SoundBoardData.GetPads( ActiveBoardId );
InvalidateFilterCache();
}
return _boardPads;
}
}
IReadOnlyList<SoundPad> ActivePads
{
get
{
EnsureFilterCache();
return _filteredPads;
}
}
int BoardPadCount => IsSearchTab ? SoundBoardData.AllUniquePads.Count : BoardPads.Count;
int ActivePadCount => ActivePads.Count;
Panel BoardViewport => _boardScroll;
int TileWidth => LayoutClass switch
{
"is-tight" => 88,
"is-compact" => 96,
"is-medium" => 104,
"is-wide" => 120,
_ => 112
};
int TileSwatchHeight => LayoutClass switch
{
"is-tight" => 50,
"is-compact" => 56,
_ => 64
};
int ShellPaddingX => LayoutClass switch
{
"is-tight" => 20,
"is-compact" => 28,
"is-medium" => 40,
_ => 48
};
int PadColumns
{
get
{
var innerWidth = Math.Max( _screenWidth - ShellPaddingX - 8, TileWidth );
return Math.Max( 1, (innerWidth + GridGap) / (TileWidth + GridGap) );
}
}
int PadGridMinHeight
{
get
{
var count = ActivePadCount;
if ( count == 0 )
return 0;
var rows = (count + PadColumns - 1) / PadColumns;
var tileHeight = TileSwatchHeight + TileLabelHeight + TileBorder;
return GridPadTop + GridPadBottom + rows * tileHeight + (rows - 1) * GridGap;
}
}
string LayoutClass
{
get
{
if ( _screenWidth < 720 )
return "is-tight";
if ( _screenWidth < 1000 )
return "is-compact";
if ( _screenWidth < 1400 )
return "is-medium";
return "is-wide";
}
}
string ActiveVoiceColor => SoundBoardManager.ActiveVoiceCount == 0
? "#7cf9ff"
: TierColor( ActiveVoiceTierIndex );
int ActiveVoiceTierIndex => SoundBoardManager.ActiveVoiceCount == 0
? 0
: Math.Min( (SoundBoardManager.ActiveVoiceCount - 1) / VoicesPerTier, VoiceMeterTierCount - 1 );
string FooterStatusText
{
get
{
var status = SoundBoardManager.StatusText;
if ( !string.IsNullOrWhiteSpace( status ) && status != "READY · PRESS ANY BUTTON" )
return status;
if ( SoundBoardManager.LoopEnabled )
return "LOOP ON · PRESS A PAD";
if ( SoundBoardCoop.Enabled )
{
var hint = SoundBoardCoop.StatusHint();
return string.IsNullOrWhiteSpace( hint )
? "CO-OP ON · HEAR EVERYONE'S PRESSES"
: $"CO-OP ON · {hint.ToUpperInvariant()}";
}
if ( !SoundPreloadService.IsInitialized )
return "STARTING UP...";
if ( !SoundPreloadService.IsStartupComplete )
return $"LOADING ARNOLD · {SoundPreloadService.LoadedCount} / {SoundBoardData.ArnoldPads.Count}";
return "READY · PRESS ANY BUTTON";
}
}
readonly record struct VoiceMeterTierState( int FillPercent, string Color, bool IsActive, bool IsFull, bool IsChaos );
VoiceMeterTierState GetVoiceMeterTier( int tierIndex )
{
var count = SoundBoardManager.ActiveVoiceCount;
var tierStart = tierIndex * VoicesPerTier;
var tierEnd = (tierIndex + 1) * VoicesPerTier;
var color = TierColor( tierIndex );
if ( count <= tierStart )
return new VoiceMeterTierState( 0, color, false, false, false );
if ( count >= tierEnd )
{
var chaos = tierIndex == VoiceMeterTierCount - 1 && count > tierEnd;
return new VoiceMeterTierState( 100, color, chaos, true, chaos );
}
var fill = (int)Math.Round( (count - tierStart) * 100f / VoicesPerTier );
return new VoiceMeterTierState( fill, color, true, false, false );
}
static string TierColor( int tierIndex ) => tierIndex switch
{
0 => "#7cf9ff",
1 => "#8b5cff",
2 => "#ffd34a",
3 => "#ff5d6a",
_ => "#ff5d6a"
};
protected override void OnStart()
{
_screenWidth = (int)Screen.Width;
_screenHeight = (int)Screen.Height;
SoundPlayTracker.EnsureLoaded();
// Local→platform catch-up (fifty_deep, click_*, etc.). Tick handles delayed retry.
SoundBoardAchievements.ResyncPlatformFromCookies();
SoundBoardManager.StateChanged += OnStateChanged;
SoundPreloadService.Changed += OnStateChanged;
SoundBoardAchievements.StateChanged += OnStateChanged;
SoundBoardCoop.StateChanged += OnStateChanged;
}
protected override void OnDestroy()
{
SoundBoardManager.StateChanged -= OnStateChanged;
SoundPreloadService.Changed -= OnStateChanged;
SoundBoardAchievements.StateChanged -= OnStateChanged;
SoundBoardCoop.StateChanged -= OnStateChanged;
}
protected override void OnUpdate()
{
var width = (int)Screen.Width;
var height = (int)Screen.Height;
if ( width != _screenWidth || height != _screenHeight )
{
_screenWidth = width;
_screenHeight = height;
ClampTabsScroll();
StateHasChanged();
}
// Backup tick (Bootstrap also ticks; manager de-dupes per frame).
SoundBoardManager.Tick();
SoundBoardAchievements.Tick();
SoundBoardCoop.Tick();
TryWheelScrollBoard();
TryWheelScrollTabs();
}
void OnStateChanged() => StateHasChanged();
void SetBoard( string boardId )
{
if ( string.IsNullOrWhiteSpace( boardId ) )
return;
// Leaving search → clear query and open the chosen category.
if ( HasSearchQuery )
{
SearchQuery = "";
_boardBeforeSearch = boardId;
}
if ( ActiveBoardId == boardId && !IsSearchTab )
return;
ActiveBoardId = boardId;
_padsBoardId = null;
InvalidateFilterCache();
ResetBoardScroll();
StateHasChanged();
}
void OnSearchEdited( string value )
{
var next = value ?? "";
var had = HasSearchQuery;
// Entering search: remember which category we left.
if ( !had && !string.IsNullOrWhiteSpace( next ) && ActiveBoardId != SearchBoardId )
_boardBeforeSearch = ActiveBoardId;
SearchQuery = next;
if ( HasSearchQuery )
ActiveBoardId = SearchBoardId;
else if ( ActiveBoardId == SearchBoardId )
ActiveBoardId = string.IsNullOrWhiteSpace( _boardBeforeSearch ) ? "arnold" : _boardBeforeSearch;
_padsBoardId = null;
InvalidateFilterCache();
ResetBoardScroll();
// Jump to start of tab strip so the SEARCH chip is visible.
if ( HasSearchQuery )
_tabsScrollX = 0f;
StateHasChanged();
}
void ClearSearch()
{
if ( string.IsNullOrEmpty( SearchQuery ) && ActiveBoardId != SearchBoardId )
return;
SearchQuery = "";
ActiveBoardId = string.IsNullOrWhiteSpace( _boardBeforeSearch ) ? "arnold" : _boardBeforeSearch;
_padsBoardId = null;
InvalidateFilterCache();
ResetBoardScroll();
StateHasChanged();
}
void InvalidateFilterCache()
{
_filterBoardId = null;
_filterQuery = null;
}
void EnsureFilterCache()
{
var query = SearchQuery ?? "";
var boardKey = IsSearchTab ? SearchBoardId : ActiveBoardId;
if ( _filterBoardId == boardKey
&& string.Equals( _filterQuery, query, StringComparison.Ordinal )
&& _filteredPads is not null )
{
return;
}
_filterBoardId = boardKey;
_filterQuery = query;
// Global SEARCH tab: all boards, multi-term OR (comma-separated).
if ( !string.IsNullOrWhiteSpace( query ) )
{
_filteredPads = SoundBoardData.SearchAll( query );
return;
}
// Normal category browse.
_filteredPads = BoardPads;
}
string GetPadClass( SoundPad pad, bool hasRemoteHighlight = false )
{
if ( pad is null || string.IsNullOrWhiteSpace( pad.SoundId ) )
return hasRemoteHighlight ? "active is-remote" : "";
// O(1) live-id lookup — avoid scanning every pad×voice each rebuild.
if ( SoundBoardManager.IsPadPlaying( pad.SoundId ) || hasRemoteHighlight )
{
if ( SoundPreloadService.IsPendingPlay( pad.SoundId ) )
return hasRemoteHighlight ? "active is-loading is-remote" : "active is-loading";
return hasRemoteHighlight ? "active is-remote" : "active";
}
if ( SoundPreloadService.IsPendingPlay( pad.SoundId ) )
return "is-loading";
return "";
}
void ResetBoardScroll()
{
var viewport = BoardViewport;
if ( viewport is not null && viewport.IsValid() )
viewport.ScrollOffset = Vector2.Zero;
}
void PlayPad( SoundPad pad )
{
if ( pad is null )
return;
try
{
// Local play first for snappy feedback, then share to lobby if CO-OP is on.
SoundBoardManager.Play( pad );
if ( SoundBoardCoop.Enabled )
{
try { SoundBoardCoop.RegisterLocalPress( pad ); }
catch ( Exception e ) { Log.Warning( $"[S&Soundboard] RegisterLocalPress: {e.Message}" ); }
try { SoundBoardNet.SharePadPress( pad ); }
catch ( Exception e ) { Log.Warning( $"[S&Soundboard] SharePadPress: {e.Message}" ); }
}
}
catch ( Exception e )
{
// Last resort — never surface a raw code error from a pad click.
Log.Warning( $"[S&Soundboard] PlayPad failed · {pad.Label}: {e.GetType().Name}: {e.Message}" );
}
}
void OnLoopPressed()
{
SoundBoardManager.ToggleLoop();
StateHasChanged();
}
void OnCoopPressed()
{
// Instant toggle — off stops receiving/showing lobby presses immediately.
SoundBoardCoop.Toggle();
StateHasChanged();
}
void OnKillPressed() => SoundBoardManager.KillAll();
float GetTabsViewportWidth()
{
if ( _tabsNav is not null && _tabsNav.IsValid() )
{
var w = _tabsNav.Box.Rect.Width;
if ( w > 8f )
return w;
}
// Fallback: full width minus two arrows + padding.
return Math.Max( 120f, _screenWidth - 100f );
}
float GetTabsContentWidth()
{
if ( _tabsTrack is not null && _tabsTrack.IsValid() )
{
var w = _tabsTrack.Box.Rect.Width;
if ( w > 8f )
return w;
}
// Estimate until layout measures the track (many tabs now).
var boards = SoundBoardData.Boards?.Count ?? 0;
return Math.Max( boards * TabChipEstimateWidth, GetTabsViewportWidth() );
}
float GetTabsMaxScroll()
{
return Math.Max( 0f, GetTabsContentWidth() - GetTabsViewportWidth() );
}
void ClampTabsScroll()
{
var max = GetTabsMaxScroll();
if ( _tabsScrollX > max )
_tabsScrollX = max;
if ( _tabsScrollX < 0f )
_tabsScrollX = 0f;
}
void ScrollTabs( int direction )
{
if ( direction == 0 )
return;
// Prefer engine scroll if it actually reports horizontal overflow.
var tabs = _tabsNav;
if ( tabs is not null && tabs.IsValid() && tabs.ScrollSize.x > 1f )
{
var maxX = tabs.ScrollSize.x;
var nextEngine = Math.Clamp( tabs.ScrollOffset.x + direction * TabsScrollStep, 0f, maxX );
tabs.ScrollOffset = new Vector2( nextEngine, 0f );
_tabsScrollX = nextEngine;
StateHasChanged();
return;
}
// Reliable path: shift the track with margin (works when ScrollSize stays 0).
_tabsScrollX = Math.Clamp( _tabsScrollX + direction * TabsScrollStep, 0f, GetTabsMaxScroll() );
StateHasChanged();
}
bool IsPointerOverBoard()
{
if ( _boardArea?.HasHovered == true )
return true;
return BoardViewport?.HasHovered == true;
}
float GetMaxScrollY( Panel viewport )
{
var engineScroll = Math.Max( viewport.ScrollSize.y, 0f );
if ( engineScroll > 0.5f )
return engineScroll;
return Math.Max( PadGridMinHeight - viewport.Box.Rect.Height, 0f );
}
void TryWheelScrollBoard()
{
var viewport = BoardViewport;
if ( viewport is null || !viewport.IsValid() || !IsPointerOverBoard() )
return;
if ( _tabsNav is not null && _tabsNav.IsValid() && _tabsNav.HasHovered )
return;
var wheel = Input.MouseWheel;
if ( Math.Abs( wheel.y ) < 0.01f )
return;
if ( viewport.TryScroll( wheel ) )
return;
const float wheelSpeed = 96f;
var maxScroll = GetMaxScrollY( viewport );
if ( maxScroll <= 0.5f )
return;
var nextY = Math.Clamp( viewport.ScrollOffset.y + wheel.y * wheelSpeed, 0f, maxScroll );
viewport.ScrollOffset = new Vector2( viewport.ScrollOffset.x, nextY );
}
void TryWheelScrollTabs()
{
var tabs = _tabsNav;
if ( tabs is null || !tabs.IsValid() || !tabs.HasHovered )
return;
var wheel = Input.MouseWheel;
// Prefer horizontal wheel; fall back to vertical as "scroll tabs sideways".
var delta = Math.Abs( wheel.x ) > 0.01f ? wheel.x : wheel.y;
if ( Math.Abs( delta ) < 0.01f )
return;
if ( tabs.ScrollSize.x > 1f )
{
if ( tabs.TryScroll( new Vector2( delta, 0f ) ) )
{
_tabsScrollX = tabs.ScrollOffset.x;
return;
}
const float wheelSpeed = 72f;
var nextX = Math.Clamp( tabs.ScrollOffset.x + delta * wheelSpeed, 0f, tabs.ScrollSize.x );
tabs.ScrollOffset = new Vector2( nextX, 0f );
_tabsScrollX = nextX;
return;
}
// Margin-offset path (same as arrow buttons).
const float wheelStep = 80f;
var before = _tabsScrollX;
_tabsScrollX = Math.Clamp( _tabsScrollX + delta * wheelStep, 0f, GetTabsMaxScroll() );
if ( Math.Abs( _tabsScrollX - before ) > 0.1f )
StateHasChanged();
}
string ShortLabel( string label )
{
if ( string.IsNullOrWhiteSpace( label ) )
return "???";
var max = _screenWidth < 720 ? 9 : _screenWidth < 1000 ? 11 : _screenWidth < 1400 ? 13 : 16;
if ( label.Length <= max )
return label;
return label[..(max - 1)] + "…";
}
string ShortPlayerName( string name )
{
if ( string.IsNullOrWhiteSpace( name ) )
return "?";
var max = _screenWidth < 720 ? 8 : 10;
if ( name.Length <= max )
return name;
return name[..(max - 1)] + "…";
}
protected override int BuildHash()
{
// Avoid O(pads) scans here — use revision counters from manager/preload.
var hash = new HashCode();
hash.Add( ActiveBoardId );
hash.Add( SearchQuery );
hash.Add( IsSearchTab );
hash.Add( ActivePadCount );
hash.Add( SoundBoardManager.LoopEnabled );
hash.Add( SoundBoardManager.LoopLayerCount );
hash.Add( SoundBoardManager.VoiceRevision );
hash.Add( SoundBoardManager.ActiveVoiceCount );
hash.Add( SoundBoardManager.StatusText );
hash.Add( SoundBoardManager.LastPlayedLabel );
hash.Add( SoundBoardCoop.Enabled );
hash.Add( SoundBoardCoop.Revision );
hash.Add( SoundPreloadService.LoadedCount );
hash.Add( SoundPreloadService.IsStartupComplete );
hash.Add( SoundPreloadService.PendingRevision );
hash.Add( PadGridMinHeight );
hash.Add( _screenWidth );
hash.Add( _screenHeight );
hash.Add( _tabsScrollX );
hash.Add( SoundBoardAchievements.ToastVisible );
hash.Add( SoundBoardAchievements.ToastTitle );
return hash.ToHashCode();
}
}