UI/Foundation/OMRUIRuntimeStyle.cs
using Sandbox;
using Sandbox.UI;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
/// <summary>
/// Applies GameResource values as typed Panel styles. SCSS remains responsible
/// for structure, transitions and responsive rules; this layer supplies the
/// values designers are expected to tune from the Inspector.
/// </summary>
public static class OMRUIRuntimeStyle
{
public static void ApplyMenu(
Panel root,
OMRUITheme theme,
OMRMainMenuLayout layout
)
{
if ( root is null || theme is null || layout is null )
return;
root.Style.FontFamily = theme.BodyFont;
root.Style.FontColor = theme.PrimaryText;
foreach ( Panel panel in Panels( root ) )
{
ApplyTextFormatting( panel, theme );
ApplyDisplayTextEffect( panel, theme );
if ( panel.HasClass( "menu-screen" ) )
panel.Style.BackgroundColor = theme.MenuCanvasColor;
if ( panel.HasClass( "menu-background" ) )
{
if ( panel is Image background )
background.Texture = theme.GetBackgroundImage();
panel.Style.ObjectFit = Fit( layout.BackgroundFit );
panel.Style.Opacity = theme.MenuBackgroundOpacity;
// Keep the image box locked to the viewport and zoom it using a real
// transform. Growing Width/Height caused the engine to effectively
// zoom from the top-left corner and made pan/focus hard to reason about.
panel.Style.Width = Length.Percent( 100f );
panel.Style.Height = Length.Percent( 100f );
panel.Style.Left = Length.Pixels( layout.BackgroundOffsetX );
panel.Style.Top = Length.Pixels( layout.BackgroundOffsetY );
float originX = Math.Clamp( layout.BackgroundPositionXPercent, 0f, 100f );
float originY = Math.Clamp( layout.BackgroundPositionYPercent, 0f, 100f );
panel.Style.TransformOriginX = Length.Percent( originX );
panel.Style.TransformOriginY = Length.Percent( originY );
float zoomScale = Math.Clamp( layout.BackgroundZoomPercent, 100f, 200f ) / 100f;
PanelTransform backgroundTransform = new();
backgroundTransform.AddScale( zoomScale );
panel.Style.Transform = backgroundTransform;
}
if ( panel.HasClass( "menu-overlay" ) )
{
panel.Style.BackgroundColor = theme.ScreenOverlay;
panel.Style.Opacity = layout.ShowScreenOverlay ? layout.ScreenOverlayOpacity : 0f;
}
if ( panel.HasClass( "menu-shell" ) )
{
if ( layout.ShellMode == OMRMenuShellMode.FullScreenCanvas )
{
// In canvas mode the shell stops being a layout constraint. It remains
// only as a structural parent so brand/navigation offsets are relative
// to the full screen rather than a centered panel.
panel.Style.Width = Length.Percent( 100f );
panel.Style.Height = Length.Percent( 100f );
panel.Style.MaxHeight = Length.Percent( 100f );
SetPadding( panel, 0f );
panel.Style.Left = Length.Pixels( 0f );
panel.Style.Top = Length.Pixels( 0f );
}
else
{
panel.Style.Width = Length.Pixels( layout.ShellWidth );
panel.Style.Height = Length.Auto;
panel.Style.MaxHeight = Length.Percent( layout.ShellMaxHeightPercent );
SetPadding( panel, layout.ShellPadding );
panel.Style.Left = Length.Pixels( layout.ShellOffsetX );
panel.Style.Top = Length.Pixels( layout.ShellOffsetY );
}
}
if ( panel.HasClass( "menu-shell-chrome" ) )
{
panel.Style.BackgroundColor = theme.MenuShellBackground;
panel.Style.BorderColor = theme.StrongBorder;
panel.Style.BorderWidth = Length.Pixels( layout.BorderWidth );
bool shellChromeVisible =
layout.ShellMode == OMRMenuShellMode.Panel &&
layout.ShowShellChrome;
panel.Style.Opacity = shellChromeVisible ? layout.ShellChromeOpacity : 0f;
SetCornerRadius( panel, theme.MenuShellCornerRadius );
ApplyBackdropBlur( panel, theme.PrimaryPanelBackdropBlur );
ApplyPrimaryPanelShadow( panel, theme );
}
if ( panel.HasClass( "session-notice" ) )
{
panel.Style.BackgroundColor = theme.NoticeBackground;
panel.Style.BorderColor = theme.SoftBorder;
panel.Style.FontColor = theme.SecondaryText;
SetCornerRadius( panel, theme.TagCornerRadius );
}
if ( panel.HasClass( "menu-brand" ) )
{
panel.Style.Display = layout.ShowBrand ? DisplayMode.Flex : DisplayMode.None;
panel.Style.Opacity = layout.BrandOpacity;
panel.Style.MarginBottom = Length.Pixels( layout.BrandBottomSpacing );
panel.Style.Left = Length.Pixels( layout.BrandOffsetX );
panel.Style.Top = Length.Pixels( layout.BrandOffsetY );
}
if ( panel.HasClass( "menu-logo" ) )
{
panel.Style.Display = layout.ShowLogo ? DisplayMode.Flex : DisplayMode.None;
panel.Style.ObjectFit = Fit( layout.LogoFit );
panel.Style.Width = Length.Pixels( layout.LogoWidth );
panel.Style.Height = Length.Pixels( layout.LogoHeight );
panel.Style.Left = Length.Pixels( layout.LogoOffsetX );
panel.Style.Top = Length.Pixels( layout.LogoOffsetY );
panel.Style.MarginBottom = Length.Pixels( layout.LogoBottomSpacing );
panel.Style.Opacity = layout.LogoOpacity;
if ( panel is Image logo ) logo.Texture = theme.GetLogoImage();
}
if ( panel.HasClass( "menu-title" ) )
{
panel.Style.FontFamily = theme.DisplayFont;
panel.Style.FontColor = theme.OverrideMenuTitleColor ? theme.MenuTitleColor : theme.PrimaryText;
panel.Style.FontSize = Length.Pixels( layout.TitleSize );
panel.Style.LetterSpacing = Length.Pixels( layout.TitleLetterSpacing );
panel.Style.Width = Length.Percent( 100f );
panel.Style.Left = Length.Pixels( layout.TitleOffsetX );
panel.Style.Top = Length.Pixels( layout.TitleOffsetY );
panel.Style.Opacity = layout.TitleOpacity;
ApplyTextLayout( panel, layout.TitleAlignment, layout.TitleIndent );
}
if ( panel.HasClass( "menu-subtitle" ) )
{
panel.Style.FontFamily = theme.BodyFont;
panel.Style.FontColor = theme.OverrideSubtitleColor ? theme.SubtitleColor : theme.SecondaryText;
panel.Style.FontSize = Length.Pixels( layout.SubtitleSize );
panel.Style.LetterSpacing = Length.Pixels( layout.SubtitleLetterSpacing );
panel.Style.Width = Length.Percent( 100f );
panel.Style.Left = Length.Pixels( layout.SubtitleOffsetX );
panel.Style.Top = Length.Pixels( layout.SubtitleOffsetY );
panel.Style.Opacity = layout.SubtitleOpacity;
ApplyTextLayout( panel, layout.SubtitleAlignment, layout.SubtitleIndent );
}
if ( panel.HasClass( "menu-navigation" ) )
{
panel.Style.Left = Length.Pixels( layout.NavigationOffsetX );
panel.Style.Top = Length.Pixels( layout.NavigationOffsetY );
panel.Style.Display = layout.ShowNavigation ? DisplayMode.Flex : DisplayMode.None;
panel.Style.Opacity = layout.NavigationOpacity;
}
if ( panel.HasClass( "menu-page" ) )
{
panel.Style.RowGap = Length.Pixels( layout.ButtonGap );
panel.Style.AlignItems = layout.NavigationContentAlignment switch
{
OMRTextHorizontalAlignment.Left => Align.FlexStart,
OMRTextHorizontalAlignment.Right => Align.FlexEnd,
_ => Align.Center
};
}
if ( panel.HasClass( "menu-button" ) )
{
panel.Style.Width = Length.Pixels( layout.ButtonWidth );
panel.Style.MinHeight = Length.Pixels( layout.ButtonHeight );
panel.Style.FontColor = theme.PrimaryText;
panel.Style.TransformOriginX = Length.Percent( 50f );
panel.Style.TransformOriginY = Length.Percent( 50f );
float buttonScale = 1f;
float hoverX = 0f;
float hoverY = 0f;
if ( panel.HasHovered )
{
buttonScale = Math.Clamp( layout.ButtonHoverScalePercent, 100f, 120f ) / 100f;
hoverX = layout.ButtonHoverOffsetX;
hoverY = layout.ButtonHoverOffsetY;
}
if ( panel.HasActive )
buttonScale = Math.Clamp( layout.ButtonPressedScalePercent, 80f, 100f ) / 100f;
PanelTransform buttonTransform = new();
buttonTransform.AddTranslate( Length.Pixels( hoverX ), Length.Pixels( hoverY ), null );
buttonTransform.AddScale( buttonScale );
panel.Style.Transform = buttonTransform;
bool accent =
panel.HasClass( "primary" ) ||
panel.HasClass( "matchmaking-button" );
if ( layout.ShowButtonChrome )
{
panel.Style.BorderColor = accent ? theme.AccentBorder : theme.Border;
panel.Style.BorderWidth = Length.Pixels( layout.BorderWidth );
panel.Style.BackgroundColor = accent
? InteractiveColor( panel, theme.Accent, theme.AccentHover, theme.AccentPressed )
: InteractiveColor( panel, theme.Button, theme.ButtonHover, theme.ButtonPressed );
SetCornerRadius( panel, theme.ButtonCornerRadius );
}
else
{
panel.Style.BorderWidth = Length.Pixels( 0f );
panel.Style.BackgroundColor = Color.Transparent;
}
}
if ( panel.HasClass( "button-title" ) )
{
panel.Style.FontFamily = theme.DisplayFont;
panel.Style.FontSize = Length.Pixels( layout.ButtonTitleSize );
panel.Style.FontColor = theme.OverrideButtonTitleColor ? theme.ButtonTitleColor : theme.PrimaryText;
panel.Style.Width = Length.Percent( 100f );
panel.Style.Left = Length.Pixels( layout.ButtonTitleOffsetX );
panel.Style.Top = Length.Pixels( layout.ButtonTitleOffsetY );
ApplyTextLayout( panel, layout.ButtonTitleAlignment, layout.ButtonTitleIndent );
}
if ( panel.HasClass( "button-description" ) )
{
panel.Style.Display = layout.ShowButtonDescriptions ? DisplayMode.Flex : DisplayMode.None;
panel.Style.FontFamily = theme.BodyFont;
panel.Style.FontSize = Length.Pixels( layout.ButtonDescriptionSize );
panel.Style.FontColor = theme.OverrideButtonDescriptionColor ? theme.ButtonDescriptionColor : theme.SecondaryText;
panel.Style.Width = Length.Percent( 100f );
panel.Style.Left = Length.Pixels( layout.ButtonDescriptionOffsetX );
panel.Style.Top = Length.Pixels( layout.ButtonDescriptionOffsetY );
ApplyTextLayout( panel, layout.ButtonDescriptionAlignment, layout.ButtonDescriptionIndent );
}
if ( panel.HasClass( "page-heading" ) )
{
panel.Style.FontFamily = theme.DisplayFont;
panel.Style.FontSize = Length.Pixels( layout.PageHeadingSize );
panel.Style.FontColor = theme.OverridePageHeadingColor ? theme.PageHeadingColor : theme.PrimaryText;
panel.Style.Left = Length.Pixels( layout.PageHeadingOffsetX );
panel.Style.Top = Length.Pixels( layout.PageHeadingOffsetY );
ApplyTextLayout( panel, layout.PageHeadingAlignment, layout.PageHeadingIndent );
}
if (
panel.HasClass( "page-copy" ) ||
panel.HasClass( "setting-description" ) ||
panel.HasClass( "lobby-note" )
)
{
panel.Style.FontFamily = theme.BodyFont;
panel.Style.FontColor = theme.SecondaryText;
}
if ( panel.HasClass( "page-copy" ) )
{
panel.Style.Display = layout.ShowPageCopy ? DisplayMode.Flex : DisplayMode.None;
panel.Style.FontSize = Length.Pixels( layout.PageCopySize );
panel.Style.FontColor = theme.OverridePageCopyColor ? theme.PageCopyColor : theme.SecondaryText;
panel.Style.Left = Length.Pixels( layout.PageCopyOffsetX );
panel.Style.Top = Length.Pixels( layout.PageCopyOffsetY );
ApplyTextLayout( panel, layout.PageCopyAlignment, layout.PageCopyIndent );
}
if ( panel.HasClass( "player-card" ) )
{
panel.Style.Width = Length.Pixels( layout.PlayerCardWidth );
panel.Style.MinHeight = Length.Pixels( layout.PlayerCardHeight );
if ( layout.ShowPlayerCardChrome )
{
panel.Style.BackgroundColor = theme.SoftPanelBackground;
panel.Style.BorderColor = theme.SoftBorder;
panel.Style.BorderWidth = Length.Pixels( layout.BorderWidth );
if ( panel.HasClass( "ready" ) )
{
panel.Style.BackgroundColor = theme.PositiveSoft;
panel.Style.BorderColor = theme.PositiveBorder;
}
else if ( panel.HasClass( "opponent-card" ) )
{
panel.Style.BackgroundColor = theme.DangerSoft;
panel.Style.BorderColor = theme.DangerBorder;
}
else if ( panel.HasClass( "practicing" ) )
{
panel.Style.BackgroundColor = theme.AccentSoft;
panel.Style.BorderColor = theme.AccentBorder;
}
SetCornerRadius( panel, theme.SoftPanelCornerRadius );
}
else
{
panel.Style.BackgroundColor = Color.Transparent;
panel.Style.BorderWidth = Length.Pixels( 0f );
}
}
if (
panel.HasClass( "setup-section" ) ||
panel.HasClass( "lobby-weapon-panel" ) ||
panel.HasClass( "setting-row" )
)
{
if ( layout.ShowSettingsChrome )
{
panel.Style.BackgroundColor = theme.SoftPanelBackground;
panel.Style.BorderColor = theme.SoftBorder;
panel.Style.BorderWidth = Length.Pixels( layout.BorderWidth );
SetCornerRadius( panel, theme.SoftPanelCornerRadius );
}
else
{
panel.Style.BackgroundColor = Color.Transparent;
panel.Style.BorderWidth = Length.Pixels( 0f );
}
}
if ( panel.HasClass( "setting-row" ) )
panel.Style.MinHeight = Length.Pixels( layout.SettingRowHeight );
if (
panel.HasClass( "setting-control" ) ||
panel.HasClass( "setup-selector" )
)
{
panel.Style.BackgroundColor = theme.ControlBackground;
panel.Style.BorderColor = theme.Border;
SetCornerRadius( panel, theme.ControlCornerRadius );
}
if ( panel.HasClass( "menu-back" ) || panel.HasClass( "menu-primary" ) )
{
ApplyActionAlignment( panel, layout.ActionTextAlignment, layout.ActionTextIndent );
}
if (
panel.HasClass( "menu-back" ) ||
panel.HasClass( "setting-step" ) ||
panel.HasClass( "setting-cycle" ) ||
panel.HasClass( "settings-reset" ) ||
panel.HasClass( "selector-arrow" )
)
{
panel.Style.BackgroundColor = InteractiveColor( panel, theme.Button, theme.ButtonHover, theme.ButtonPressed );
panel.Style.BorderColor = theme.Border;
panel.Style.FontColor = theme.PrimaryText;
SetCornerRadius( panel, theme.ControlCornerRadius );
}
if ( panel.HasClass( "menu-primary" ) )
{
if ( panel.HasClass( "secondary-action" ) )
{
panel.Style.BackgroundColor = InteractiveColor( panel, theme.Button, theme.ButtonHover, theme.ButtonPressed );
panel.Style.BorderColor = theme.Border;
}
else if ( panel.HasClass( "warmup-action" ) )
{
panel.Style.BackgroundColor = InteractiveColor( panel, theme.Accent, theme.AccentHover, theme.AccentPressed );
panel.Style.BorderColor = theme.AccentBorder;
}
else
{
panel.Style.BackgroundColor = panel.HasClass( "active" )
? theme.PositiveSelected
: InteractiveColor( panel, theme.Positive, theme.PositiveHover, theme.PositivePressed );
panel.Style.BorderColor = theme.PositiveBorder;
}
SetCornerRadius( panel, theme.ButtonCornerRadius );
}
if ( panel.HasClass( "online-subheading" ) )
panel.Style.FontColor = theme.AccentText;
if ( panel.HasClass( "lobby-ready-summary" ) )
panel.Style.FontColor = theme.SecondaryText;
if ( panel.HasClass( "setting-value" ) )
panel.Style.FontColor = theme.PrimaryText;
if (
panel.HasClass( "error" ) ||
panel.HasClass( "error-copy" )
)
panel.Style.FontColor = theme.ErrorText;
}
}
/// <summary>
/// Applies OMRTHEME to the developer reference board. Unlike menu/HUD layout
/// styling this method intentionally contains no gameplay geometry; its only
/// job is to expose the theme's semantic roles in one place.
/// </summary>
public static void ApplyThemePreview( Panel root, OMRUITheme theme )
{
if ( root is null || theme is null )
return;
root.Style.FontFamily = theme.BodyFont;
root.Style.FontColor = theme.PrimaryText;
foreach ( Panel panel in Panels( root ) )
{
ApplyTextFormatting( panel, theme );
ApplyDisplayTextEffect( panel, theme );
if ( panel.HasClass( "theme-background-sample" ) )
{
if ( panel is Image background )
background.Texture = theme.GetBackgroundImage();
panel.Style.Opacity = theme.MenuBackgroundOpacity;
}
if ( panel.HasClass( "theme-logo-sample" ) )
{
panel.Style.Display = theme.HasLogo ? DisplayMode.Flex : DisplayMode.None;
if ( panel is Image logo )
logo.Texture = theme.GetLogoImage();
}
if ( panel.HasClass( "theme-preview-title" ) )
{
panel.Style.FontFamily = theme.DisplayFont;
panel.Style.FontColor = theme.OverrideMenuTitleColor ? theme.MenuTitleColor : theme.PrimaryText;
}
if ( panel.HasClass( "theme-preview-subtitle" ) )
panel.Style.FontColor = theme.OverrideSubtitleColor ? theme.SubtitleColor : theme.SecondaryText;
if ( panel.HasClass( "theme-preview-heading" ) )
{
panel.Style.FontFamily = theme.DisplayFont;
panel.Style.FontColor = theme.OverridePageHeadingColor ? theme.PageHeadingColor : theme.PrimaryText;
}
if ( panel.HasClass( "theme-preview-body" ) )
panel.Style.FontColor = theme.OverridePageCopyColor ? theme.PageCopyColor : theme.SecondaryText;
if ( panel.HasClass( "theme-preview-meta" ) )
panel.Style.FontColor = theme.MutedText;
if ( panel.HasClass( "theme-preview-number" ) )
{
panel.Style.FontFamily = theme.NumericFont;
panel.Style.FontColor = theme.PrimaryText;
}
if ( panel.HasClass( "theme-text-primary" ) ) panel.Style.FontColor = theme.PrimaryText;
if ( panel.HasClass( "theme-text-secondary" ) ) panel.Style.FontColor = theme.SecondaryText;
if ( panel.HasClass( "theme-text-muted" ) ) panel.Style.FontColor = theme.MutedText;
if ( panel.HasClass( "theme-text-accent" ) ) panel.Style.FontColor = theme.AccentText;
if ( panel.HasClass( "theme-text-positive" ) ) panel.Style.FontColor = theme.PositiveText;
if ( panel.HasClass( "theme-text-danger" ) ) panel.Style.FontColor = theme.DangerText;
if ( panel.HasClass( "theme-text-error" ) ) panel.Style.FontColor = theme.ErrorText;
if ( panel.HasClass( "theme-surface-menu-shell" ) )
StylePreviewSurface( panel, theme.MenuShellBackground, theme.StrongBorder, theme.MenuShellCornerRadius, theme.PrimaryPanelBackdropBlur );
if ( panel.HasClass( "theme-surface-soft" ) )
StylePreviewSurface( panel, theme.SoftPanelBackground, theme.SoftBorder, theme.SoftPanelCornerRadius );
if ( panel.HasClass( "theme-surface-control" ) )
StylePreviewSurface( panel, theme.ControlBackground, theme.Border, theme.ControlCornerRadius );
if ( panel.HasClass( "theme-surface-notice" ) )
StylePreviewSurface( panel, theme.NoticeBackground, theme.SoftBorder, theme.TagCornerRadius );
if ( panel.HasClass( "theme-surface-pause" ) )
StylePreviewSurface( panel, theme.PausePanelBackground, theme.StrongBorder, theme.PanelCornerRadius, theme.PrimaryPanelBackdropBlur );
if ( panel.HasClass( "theme-surface-hud" ) )
StylePreviewSurface( panel, theme.HudPanel, theme.SoftBorder, theme.HudCornerRadius, theme.HudBackdropBlur, theme.HudBorderWidth );
if ( panel.HasClass( "theme-surface-timer" ) )
StylePreviewSurface( panel, theme.TimerPanel, theme.Border, theme.HudCornerRadius, theme.HudBackdropBlur, theme.HudBorderWidth );
if ( panel.HasClass( "theme-surface-pot" ) )
StylePreviewSurface( panel, theme.PotPanel, theme.Border, theme.HudCornerRadius, theme.HudBackdropBlur, theme.HudBorderWidth );
if ( panel.HasClass( "theme-surface-ammo" ) )
StylePreviewSurface( panel, theme.AmmoPanel, theme.Border, theme.HudCornerRadius, theme.HudBackdropBlur, theme.HudBorderWidth );
if ( panel.HasClass( "theme-surface-decision" ) )
StylePreviewSurface( panel, theme.DecisionPanelBackground, theme.StrongBorder, theme.PanelCornerRadius, theme.PrimaryPanelBackdropBlur, theme.HudBorderWidth );
if ( panel.HasClass( "theme-surface-sandbox" ) )
StylePreviewSurface( panel, theme.SandboxLabel, theme.Border, theme.HudCornerRadius, theme.HudBackdropBlur, theme.HudBorderWidth );
if ( panel.HasClass( "theme-surface-warmup" ) )
StylePreviewSurface( panel, theme.WarmupLabel, theme.AccentBorder, theme.HudCornerRadius, theme.HudBackdropBlur, theme.HudBorderWidth );
if ( panel.HasClass( "theme-surface-track" ) )
StylePreviewSurface( panel, theme.HealthTrack, theme.SoftBorder, theme.HealthBarCornerRadius, 0f, theme.HudBorderWidth );
if ( panel.HasClass( "theme-surface-sudden" ) )
StylePreviewSurface( panel, theme.SuddenDeath, theme.DangerBorder, theme.HudCornerRadius, theme.HudBackdropBlur, theme.HudBorderWidth );
if ( panel.HasClass( "theme-surface-menu-shell" ) || panel.HasClass( "theme-surface-pause" ) || panel.HasClass( "theme-surface-decision" ) )
ApplyPrimaryPanelShadow( panel, theme );
else if ( panel.HasClass( "theme-surface-hud" ) || panel.HasClass( "theme-surface-timer" ) || panel.HasClass( "theme-surface-pot" ) || panel.HasClass( "theme-surface-ammo" ) || panel.HasClass( "theme-surface-sandbox" ) || panel.HasClass( "theme-surface-warmup" ) || panel.HasClass( "theme-surface-sudden" ) )
ApplyHudPanelShadow( panel, theme );
if ( panel.HasClass( "theme-overlay-menu-canvas" ) ) panel.Style.BackgroundColor = theme.MenuCanvasColor;
if ( panel.HasClass( "theme-overlay-menu" ) ) panel.Style.BackgroundColor = theme.ScreenOverlay;
if ( panel.HasClass( "theme-overlay-pause" ) ) panel.Style.BackgroundColor = theme.PauseOverlay;
if ( panel.HasClass( "theme-overlay-decision" ) ) panel.Style.BackgroundColor = theme.DecisionOverlay;
if ( panel.HasClass( "theme-health-local" ) )
{
panel.Style.BackgroundColor = theme.LocalHealth;
SetCornerRadius( panel, theme.HealthBarCornerRadius );
}
if ( panel.HasClass( "theme-health-opponent" ) )
{
panel.Style.BackgroundColor = theme.OpponentHealth;
SetCornerRadius( panel, theme.HealthBarCornerRadius );
}
if ( panel.HasClass( "theme-border-primary" ) ) panel.Style.BorderColor = theme.Border;
if ( panel.HasClass( "theme-border-soft" ) ) panel.Style.BorderColor = theme.SoftBorder;
if ( panel.HasClass( "theme-border-strong" ) ) panel.Style.BorderColor = theme.StrongBorder;
if ( panel.HasClass( "theme-border-accent" ) ) panel.Style.BorderColor = theme.AccentBorder;
if ( panel.HasClass( "theme-border-positive" ) ) panel.Style.BorderColor = theme.PositiveBorder;
if ( panel.HasClass( "theme-border-danger" ) ) panel.Style.BorderColor = theme.DangerBorder;
if ( panel.HasClass( "theme-border-sample" ) )
{
panel.Style.BorderWidth = Length.Pixels( Math.Max( 1f, theme.HudBorderWidth ) );
SetCornerRadius( panel, theme.ControlCornerRadius );
}
if ( panel.HasClass( "theme-interactive-neutral-normal" ) ) panel.Style.BackgroundColor = theme.Button;
if ( panel.HasClass( "theme-interactive-neutral-hover" ) ) panel.Style.BackgroundColor = theme.ButtonHover;
if ( panel.HasClass( "theme-interactive-neutral-pressed" ) ) panel.Style.BackgroundColor = theme.ButtonPressed;
if ( panel.HasClass( "theme-interactive-accent-normal" ) ) panel.Style.BackgroundColor = theme.Accent;
if ( panel.HasClass( "theme-interactive-accent-hover" ) ) panel.Style.BackgroundColor = theme.AccentHover;
if ( panel.HasClass( "theme-interactive-accent-pressed" ) ) panel.Style.BackgroundColor = theme.AccentPressed;
if ( panel.HasClass( "theme-interactive-accent-selected" ) ) panel.Style.BackgroundColor = theme.AccentSelected;
if ( panel.HasClass( "theme-interactive-accent-soft" ) ) panel.Style.BackgroundColor = theme.AccentSoft;
if ( panel.HasClass( "theme-interactive-positive-normal" ) ) panel.Style.BackgroundColor = theme.Positive;
if ( panel.HasClass( "theme-interactive-positive-hover" ) ) panel.Style.BackgroundColor = theme.PositiveHover;
if ( panel.HasClass( "theme-interactive-positive-pressed" ) ) panel.Style.BackgroundColor = theme.PositivePressed;
if ( panel.HasClass( "theme-interactive-positive-selected" ) ) panel.Style.BackgroundColor = theme.PositiveSelected;
if ( panel.HasClass( "theme-interactive-positive-soft" ) ) panel.Style.BackgroundColor = theme.PositiveSoft;
if ( panel.HasClass( "theme-interactive-danger-normal" ) ) panel.Style.BackgroundColor = theme.Danger;
if ( panel.HasClass( "theme-interactive-danger-hover" ) ) panel.Style.BackgroundColor = theme.DangerHover;
if ( panel.HasClass( "theme-interactive-danger-pressed" ) ) panel.Style.BackgroundColor = theme.DangerPressed;
if ( panel.HasClass( "theme-interactive-danger-soft" ) ) panel.Style.BackgroundColor = theme.DangerSoft;
if ( panel.HasClass( "theme-interactive-chip" ) )
{
panel.Style.BorderColor = theme.SoftBorder;
panel.Style.BorderWidth = Length.Pixels( theme.HudBorderWidth );
SetCornerRadius( panel, theme.ButtonCornerRadius );
}
}
}
public static void ApplyPause(
Panel root,
OMRUITheme theme,
OMRMainMenuLayout layout
)
{
if ( root is null || theme is null || layout is null )
return;
root.Style.FontFamily = theme.BodyFont;
root.Style.FontColor = theme.PrimaryText;
foreach ( Panel panel in Panels( root ) )
{
ApplyTextFormatting( panel, theme );
ApplyDisplayTextEffect( panel, theme );
if ( panel.HasClass( "pause-screen-overlay" ) )
{
panel.Style.BackgroundColor = theme.PauseOverlay;
panel.Style.Opacity = layout.ShowPauseOverlay ? layout.PauseOverlayOpacity : 0f;
}
if ( panel.HasClass( "pause-panel" ) )
{
panel.Style.Width = Length.Pixels( layout.PauseWidth );
SetPadding( panel, layout.PausePadding );
panel.Style.Left = Length.Pixels( layout.PauseOffsetX );
panel.Style.Top = Length.Pixels( layout.PauseOffsetY );
}
if ( panel.HasClass( "pause-panel-chrome" ) )
{
panel.Style.BackgroundColor = theme.PausePanelBackground;
panel.Style.BorderColor = theme.StrongBorder;
panel.Style.BorderWidth = Length.Pixels( layout.BorderWidth );
panel.Style.Opacity = layout.ShowPauseChrome ? layout.PauseChromeOpacity : 0f;
SetCornerRadius( panel, theme.PanelCornerRadius );
ApplyBackdropBlur( panel, theme.PrimaryPanelBackdropBlur );
ApplyPrimaryPanelShadow( panel, theme );
}
if ( panel.HasClass( "pause-title" ) )
{
panel.Style.FontFamily = theme.DisplayFont;
panel.Style.FontSize = Length.Pixels( layout.PauseTitleSize );
}
if (
panel.HasClass( "pause-note" ) ||
panel.HasClass( "pause-button-copy" ) ||
panel.HasClass( "pause-summary" ) ||
panel.HasClass( "setting-description" )
)
panel.Style.FontColor = theme.SecondaryText;
if ( panel.HasClass( "pause-eyebrow" ) )
panel.Style.FontColor = theme.AccentText;
if ( panel.HasClass( "setting-value" ) )
panel.Style.FontColor = theme.PrimaryText;
if ( panel.HasClass( "pause-button" ) )
{
panel.Style.MinHeight = Length.Pixels( layout.PauseButtonHeight );
panel.Style.BorderColor = theme.Border;
panel.Style.BorderWidth = Length.Pixels( layout.BorderWidth );
if ( panel.HasClass( "primary" ) )
{
panel.Style.BackgroundColor = InteractiveColor( panel, theme.Positive, theme.PositiveHover, theme.PositivePressed );
panel.Style.BorderColor = theme.PositiveBorder;
}
else if ( panel.HasClass( "danger" ) )
{
panel.Style.BackgroundColor = InteractiveColor( panel, theme.Danger, theme.DangerHover, theme.DangerPressed );
panel.Style.BorderColor = theme.DangerBorder;
}
else
{
panel.Style.BackgroundColor = InteractiveColor( panel, theme.Button, theme.ButtonHover, theme.ButtonPressed );
}
SetCornerRadius( panel, theme.ButtonCornerRadius );
}
if (
panel.HasClass( "pause-settings-back" ) ||
panel.HasClass( "setting-step" ) ||
panel.HasClass( "setting-cycle" ) ||
panel.HasClass( "settings-reset" )
)
{
panel.Style.BackgroundColor = InteractiveColor( panel, theme.Button, theme.ButtonHover, theme.ButtonPressed );
panel.Style.BorderColor = theme.Border;
SetCornerRadius( panel, theme.ControlCornerRadius );
}
if ( panel.HasClass( "setting-row" ) )
{
panel.Style.MinHeight = Length.Pixels( layout.SettingRowHeight );
panel.Style.BackgroundColor = theme.SoftPanelBackground;
panel.Style.BorderColor = theme.SoftBorder;
SetCornerRadius( panel, theme.SoftPanelCornerRadius );
}
if ( panel.HasClass( "setting-control" ) )
{
panel.Style.BackgroundColor = theme.ControlBackground;
panel.Style.BorderColor = theme.Border;
SetCornerRadius( panel, theme.ControlCornerRadius );
}
}
}
public static void ApplyHud(
Panel root,
OMRUITheme theme,
OMRHUDLayout layout
)
{
if ( root is null || theme is null || layout is null )
return;
root.Style.FontFamily = theme.BodyFont;
root.Style.FontColor = theme.PrimaryText;
foreach ( Panel panel in Panels( root ) )
{
ApplyTextFormatting( panel, theme );
ApplyDisplayTextEffect( panel, theme );
// ------------------------------------------------------------
// HEALTH
// ------------------------------------------------------------
if ( panel.HasClass( "combat-health" ) )
{
panel.Style.Top = Length.Pixels( layout.HealthTop );
panel.Style.Left = Length.Pixels( layout.HealthHorizontalInset );
panel.Style.Right = Length.Pixels( layout.HealthHorizontalInset );
panel.Style.Display = layout.ShowLocalHealth || layout.ShowOpponentHealth
? DisplayMode.Flex : DisplayMode.None;
}
if ( panel.HasClass( "health-side" ) )
{
bool opponent = panel.HasClass( "opponent" );
panel.Style.Width = Length.Percent( layout.HealthSideWidthPercent );
panel.Style.MaxWidth = Length.Pixels( layout.HealthMaximumWidth );
panel.Style.Display = opponent
? (layout.ShowOpponentHealth ? DisplayMode.Flex : DisplayMode.None)
: (layout.ShowLocalHealth ? DisplayMode.Flex : DisplayMode.None);
panel.Style.Left = Length.Pixels( opponent ? layout.OpponentHealthOffsetX : layout.LocalHealthOffsetX );
panel.Style.Top = Length.Pixels( opponent ? layout.OpponentHealthOffsetY : layout.LocalHealthOffsetY );
panel.Style.Opacity = opponent ? layout.OpponentHealthOpacity : layout.LocalHealthOpacity;
}
if ( panel.HasClass( "health-heading" ) )
{
bool opponent = panel.Ancestors.Any( x => x.HasClass( "health-side" ) && x.HasClass( "opponent" ) );
OMRHUDPairAlignment headingAlignment = opponent
? layout.OpponentHealthHeadingAlignment
: layout.LocalHealthHeadingAlignment;
float headingGap = opponent
? layout.OpponentHealthHeadingGap
: layout.LocalHealthHeadingGap;
panel.Style.MarginBottom = Length.Pixels( layout.HealthHeadingBottomSpacing );
// SpaceBetween owns spacing in Split mode. A manual gap only has
// meaning when both items are grouped to one side or the center.
panel.Style.ColumnGap = Length.Pixels(
headingAlignment == OMRHUDPairAlignment.Split ? 0f : headingGap
);
panel.Style.FlexDirection = opponent ? FlexDirection.RowReverse : FlexDirection.Row;
ApplyPairAlignment( panel, headingAlignment );
}
if ( panel.HasClass( "health-label" ) )
{
panel.Style.Display = layout.ShowHealthLabels ? DisplayMode.Flex : DisplayMode.None;
panel.Style.FontSize = Length.Pixels( layout.HealthLabelSize );
panel.Style.FontColor = theme.SecondaryText;
}
if ( panel.HasClass( "health-number" ) )
{
bool opponent = panel.Ancestors.Any( x => x.HasClass( "health-side" ) && x.HasClass( "opponent" ) );
panel.Style.Display = layout.ShowHealthNumbers ? DisplayMode.Flex : DisplayMode.None;
panel.Style.FontFamily = theme.NumericFont;
panel.Style.FontSize = Length.Pixels( layout.HealthNumberSize );
panel.Style.FontColor = opponent
? (theme.OverrideOpponentHealthTextColor ? theme.OpponentHealthTextColor : theme.PrimaryText)
: (theme.OverrideLocalHealthTextColor ? theme.LocalHealthTextColor : theme.PrimaryText);
}
if ( panel.HasClass( "health-track" ) )
{
panel.Style.Display = layout.ShowHealthBars ? DisplayMode.Flex : DisplayMode.None;
panel.Style.Height = Length.Pixels( layout.HealthBarHeight );
panel.Style.BackgroundColor = Color.Transparent;
panel.Style.BorderWidth = Length.Pixels( 0f );
SetCornerRadius( panel, theme.HealthBarCornerRadius );
}
if ( panel.HasClass( "health-track-chrome" ) )
{
panel.Style.BackgroundColor = theme.HealthTrack;
panel.Style.BorderColor = theme.SoftBorder;
panel.Style.BorderWidth = Length.Pixels( theme.HudBorderWidth );
panel.Style.Opacity = layout.ShowHealthTrackChrome ? layout.HealthTrackChromeOpacity : 0f;
SetCornerRadius( panel, theme.HealthBarCornerRadius );
}
if ( panel.HasClass( "health-fill" ) )
{
bool opponent = panel.Ancestors.Any( x => x.HasClass( "health-side" ) && x.HasClass( "opponent" ) );
OMRHUDFillDirection direction = opponent
? layout.OpponentHealthFillDirection
: layout.LocalHealthFillDirection;
panel.Style.BackgroundColor = opponent ? theme.OpponentHealth : theme.LocalHealth;
panel.Style.Opacity = layout.HealthBarOpacity;
panel.Style.Left = direction == OMRHUDFillDirection.LeftToRight ? Length.Pixels( 0f ) : null;
panel.Style.Right = direction == OMRHUDFillDirection.RightToLeft ? Length.Pixels( 0f ) : null;
SetCornerRadius( panel, theme.HealthBarCornerRadius );
}
// ------------------------------------------------------------
// MATCH HEADER
// ------------------------------------------------------------
if ( panel.HasClass( "match-header" ) )
{
panel.Style.Display = layout.ShowMatchHeader ? DisplayMode.Flex : DisplayMode.None;
panel.Style.Top = Length.Pixels( layout.MatchHeaderTop + layout.MatchHeaderOffsetY );
panel.Style.Left = Length.Pixels( layout.MatchHeaderHorizontalInset + layout.MatchHeaderOffsetX );
panel.Style.Right = Length.Pixels( layout.MatchHeaderHorizontalInset - layout.MatchHeaderOffsetX );
panel.Style.Width = Length.Auto;
panel.Style.Opacity = layout.MatchHeaderOpacity;
panel.Style.RowGap = Length.Pixels( layout.MatchHeaderElementGap );
ApplyHorizontalContainerAlignment( panel, layout.MatchHeaderAlignment );
}
if ( panel.HasClass( "round" ) )
{
panel.Style.Display = layout.ShowRoundTitle ? DisplayMode.Flex : DisplayMode.None;
panel.Style.Top = Length.Pixels( layout.RoundTitleOffsetY );
panel.Style.MarginBottom = Length.Pixels( 0f );
panel.Style.Opacity = layout.RoundTitleOpacity;
panel.Style.FontFamily = theme.DisplayFont;
panel.Style.FontSize = Length.Pixels( layout.RoundTitleSize );
panel.Style.FontColor = theme.OverrideRoundTextColor ? theme.RoundTextColor : theme.PrimaryText;
ApplyTextLayout( panel, layout.MatchHeaderAlignment, 0f );
}
if ( panel.HasClass( "round-timer" ) )
{
panel.Style.Display = layout.ShowTimer ? DisplayMode.Flex : DisplayMode.None;
panel.Style.Top = Length.Pixels( layout.TimerOffsetY );
panel.Style.MarginBottom = Length.Pixels( 0f );
panel.Style.MinWidth = Length.Pixels( layout.TimerMinimumWidth );
panel.Style.PaddingLeft = Length.Pixels( layout.TimerPaddingX );
panel.Style.PaddingRight = Length.Pixels( layout.TimerPaddingX );
panel.Style.PaddingTop = Length.Pixels( layout.TimerPaddingY );
panel.Style.PaddingBottom = Length.Pixels( layout.TimerPaddingY );
}
if ( panel.HasClass( "round-timer-chrome" ) )
{
bool sudden = panel.Ancestors.Any( x => x.HasClass( "round-timer" ) && x.HasClass( "sudden" ) );
panel.Style.BackgroundColor = sudden ? theme.SuddenDeath : theme.TimerPanel;
panel.Style.BorderColor = sudden ? theme.DangerBorder : theme.Border;
panel.Style.BorderWidth = Length.Pixels( theme.HudBorderWidth );
panel.Style.Opacity = layout.ShowTimerChrome ? layout.TimerChromeOpacity : 0f;
SetCornerRadius( panel, theme.HudCornerRadius );
ApplyBackdropBlur( panel, theme.HudBackdropBlur );
ApplyHudPanelShadow( panel, theme );
}
if ( panel.HasClass( "timer-number" ) )
{
bool sudden = panel.Ancestors.Any( x => x.HasClass( "round-timer" ) && x.HasClass( "sudden" ) );
panel.Style.FontFamily = theme.NumericFont;
panel.Style.FontColor = sudden
? (theme.OverrideSuddenDeathTextColor ? theme.SuddenDeathTextColor : theme.PrimaryText)
: (theme.OverrideTimerTextColor ? theme.TimerTextColor : theme.PrimaryText);
panel.Style.FontSize = Length.Pixels( sudden ? layout.SuddenDeathTimerSize : layout.TimerSize );
}
if ( panel.HasClass( "timer-mode" ) )
{
panel.Style.Display = layout.ShowTimerMode ? DisplayMode.Flex : DisplayMode.None;
panel.Style.FontSize = Length.Pixels( layout.TimerMetaSize );
}
if ( panel.HasClass( "timer-note" ) )
{
panel.Style.Display = layout.ShowTimerNote ? DisplayMode.Flex : DisplayMode.None;
panel.Style.FontSize = Length.Pixels( layout.TimerMetaSize );
}
if ( panel.HasClass( "score-label" ) )
{
panel.Style.Display = layout.ShowScore && layout.ShowScoreHeading ? DisplayMode.Flex : DisplayMode.None;
panel.Style.FontSize = Length.Pixels( layout.ScoreHeadingSize );
panel.Style.MarginBottom = Length.Pixels( 0f );
ApplyTextLayout( panel, layout.MatchHeaderAlignment, 0f );
}
if ( panel.HasClass( "score" ) )
{
panel.Style.Display = layout.ShowScore ? DisplayMode.Flex : DisplayMode.None;
panel.Style.Top = Length.Pixels( layout.ScoreOffsetY );
panel.Style.ColumnGap = Length.Pixels( layout.ScoreColumnGap );
}
if ( panel.HasClass( "score-number" ) )
{
panel.Style.FontFamily = theme.NumericFont;
panel.Style.FontColor = theme.OverrideScoreTextColor ? theme.ScoreTextColor : theme.PrimaryText;
panel.Style.FontSize = Length.Pixels( layout.ScoreNumberSize );
}
if ( panel.HasClass( "score-player-label" ) )
{
panel.Style.Display = layout.ShowScorePlayerLabels ? DisplayMode.Flex : DisplayMode.None;
panel.Style.FontSize = Length.Pixels( layout.ScoreLabelSize );
panel.Style.FontColor = theme.SecondaryText;
}
if ( panel.HasClass( "separator" ) )
{
panel.Style.Display = layout.ShowScoreSeparator ? DisplayMode.Flex : DisplayMode.None;
panel.Style.FontSize = Length.Pixels( layout.ScoreSeparatorSize );
panel.Style.FontColor = theme.MutedText;
}
if ( panel.HasClass( "pot-display" ) )
{
panel.Style.Display = layout.ShowPot ? DisplayMode.Flex : DisplayMode.None;
panel.Style.MarginTop = Length.Pixels( 0f );
panel.Style.PaddingLeft = Length.Pixels( layout.PotPaddingX );
panel.Style.PaddingRight = Length.Pixels( layout.PotPaddingX );
panel.Style.PaddingTop = Length.Pixels( layout.PotPaddingY );
panel.Style.PaddingBottom = Length.Pixels( layout.PotPaddingY );
panel.Style.ColumnGap = Length.Pixels( layout.PotGap );
panel.Style.BackgroundColor = Color.Transparent;
panel.Style.BorderWidth = Length.Pixels( 0f );
}
if ( panel.HasClass( "pot-display-chrome" ) )
{
panel.Style.BackgroundColor = theme.PotPanel;
panel.Style.BorderColor = theme.Border;
panel.Style.BorderWidth = Length.Pixels( theme.HudBorderWidth );
panel.Style.Opacity = layout.ShowPotChrome ? layout.PotChromeOpacity : 0f;
SetCornerRadius( panel, theme.HudCornerRadius );
ApplyBackdropBlur( panel, theme.HudBackdropBlur );
ApplyHudPanelShadow( panel, theme );
}
if ( panel.HasClass( "pot-owner" ) )
{
panel.Style.FontSize = Length.Pixels( layout.PotOwnerSize );
panel.Style.Opacity = layout.PotOpacity;
panel.Style.FontColor = theme.SecondaryText;
}
if ( panel.HasClass( "pot-value" ) )
{
panel.Style.FontSize = Length.Pixels( layout.PotValueSize );
panel.Style.Opacity = layout.PotOpacity;
panel.Style.FontColor = theme.OverridePotTextColor ? theme.PotTextColor : theme.PrimaryText;
}
if ( panel.HasClass( "modifier-label" ) )
{
panel.Style.Display = layout.ShowModifierLabel ? DisplayMode.Flex : DisplayMode.None;
panel.Style.MarginTop = Length.Pixels( 0f );
panel.Style.FontSize = Length.Pixels( layout.ModifierLabelSize );
panel.Style.Opacity = layout.ModifierLabelOpacity;
panel.Style.FontColor = theme.OverrideModifierTextColor ? theme.ModifierTextColor : theme.AccentText;
ApplyTextLayout( panel, layout.MatchHeaderAlignment, 0f );
}
// ------------------------------------------------------------
// CENTER MESSAGE
// ------------------------------------------------------------
if ( panel.HasClass( "center-message" ) )
{
panel.Style.Display = layout.ShowCenterMessage ? DisplayMode.Flex : DisplayMode.None;
panel.Style.Top = Length.Percent( layout.CenterMessageTopPercent );
panel.Style.Left = Length.Pixels( 0f );
panel.Style.MarginTop = Length.Pixels( layout.CenterMessageOffsetY );
panel.Style.Opacity = layout.CenterMessageOpacity;
panel.Style.FontFamily = theme.DisplayFont;
panel.Style.FontSize = Length.Pixels( layout.CenterMessageSize );
panel.Style.FontColor = theme.OverrideCenterMessageColor ? theme.CenterMessageColor : theme.PrimaryText;
PanelTransform messageTransform = new();
messageTransform.AddTranslate( Length.Pixels( layout.CenterMessageOffsetX ), Length.Pixels( 0f ), null );
panel.Style.Transform = messageTransform;
ApplyTextLayout( panel, layout.CenterMessageAlignment, layout.CenterMessageIndent );
}
// ------------------------------------------------------------
// DECISIONS
// ------------------------------------------------------------
if ( panel.HasClass( "decision-screen-overlay" ) )
{
panel.Style.BackgroundColor = theme.DecisionOverlay;
panel.Style.Opacity = layout.ShowDecisionOverlay ? layout.DecisionScreenOpacity : 0f;
}
if ( panel.HasClass( "decision-panel" ) )
{
panel.Style.Width = Length.Pixels( layout.DecisionPanelWidth );
SetPadding( panel, layout.DecisionPanelPadding );
panel.Style.MarginLeft = Length.Pixels( 0f );
panel.Style.MarginTop = Length.Pixels( 0f );
panel.Style.Left = Length.Pixels( layout.DecisionOffsetX );
panel.Style.Top = Length.Pixels( layout.DecisionOffsetY );
ApplyHorizontalContainerAlignment( panel, layout.DecisionContentAlignment );
}
if ( panel.HasClass( "decision-panel-chrome" ) )
{
panel.Style.BackgroundColor = theme.DecisionPanelBackground;
panel.Style.BorderColor = theme.StrongBorder;
panel.Style.BorderWidth = Length.Pixels( theme.HudBorderWidth );
panel.Style.Opacity = layout.ShowDecisionChrome ? layout.DecisionChromeOpacity : 0f;
SetCornerRadius( panel, theme.PanelCornerRadius );
ApplyBackdropBlur( panel, theme.PrimaryPanelBackdropBlur );
ApplyPrimaryPanelShadow( panel, theme );
}
if ( panel.HasClass( "decision-eyebrow" ) )
{
panel.Style.Display = layout.ShowDecisionEyebrow ? DisplayMode.Flex : DisplayMode.None;
panel.Style.FontSize = Length.Pixels( layout.DecisionEyebrowSize );
panel.Style.FontColor = theme.AccentText;
ApplyTextLayout( panel, layout.DecisionContentAlignment, layout.DecisionTextIndent );
}
if ( panel.HasClass( "decision-title" ) )
{
panel.Style.FontFamily = theme.DisplayFont;
panel.Style.FontSize = Length.Pixels( layout.DecisionTitleSize );
panel.Style.FontColor = theme.OverrideDecisionTitleColor ? theme.DecisionTitleColor : theme.PrimaryText;
ApplyTextLayout( panel, layout.DecisionContentAlignment, layout.DecisionTextIndent );
}
if ( panel.HasClass( "decision-pot" ) )
{
panel.Style.Display = layout.ShowDecisionPot ? DisplayMode.Flex : DisplayMode.None;
panel.Style.FontSize = Length.Pixels( layout.DecisionPotSize );
ApplyTextLayout( panel, layout.DecisionContentAlignment, layout.DecisionTextIndent );
}
if ( panel.HasClass( "decision-timer" ) )
{
panel.Style.Display = layout.ShowDecisionTimer ? DisplayMode.Flex : DisplayMode.None;
panel.Style.FontFamily = theme.NumericFont;
panel.Style.FontSize = Length.Pixels( layout.DecisionTimerSize );
ApplyTextLayout( panel, layout.DecisionContentAlignment, layout.DecisionTextIndent );
}
if ( panel.HasClass( "final-score" ) )
{
panel.Style.Display = layout.ShowDecisionFinalScore ? DisplayMode.Flex : DisplayMode.None;
panel.Style.FontFamily = theme.NumericFont;
panel.Style.FontSize = Length.Pixels( layout.DecisionFinalScoreSize );
ApplyTextLayout( panel, layout.DecisionContentAlignment, layout.DecisionTextIndent );
}
if ( panel.HasClass( "decision-buttons" ) )
panel.Style.ColumnGap = Length.Pixels( layout.DecisionButtonGap );
if ( panel.HasClass( "decision-button" ) )
{
panel.Style.MinHeight = Length.Pixels( layout.DecisionButtonHeight );
SetPadding( panel, layout.DecisionButtonPadding );
panel.Style.TransformOriginX = Length.Percent( 50f );
panel.Style.TransformOriginY = Length.Percent( 50f );
float scale = 1f;
float hoverX = 0f;
float hoverY = 0f;
if ( panel.HasHovered )
{
scale = Math.Clamp( layout.DecisionButtonHoverScalePercent, 100f, 120f ) / 100f;
hoverX = layout.DecisionButtonHoverOffsetX;
hoverY = layout.DecisionButtonHoverOffsetY;
}
if ( panel.HasActive )
scale = Math.Clamp( layout.DecisionButtonPressedScalePercent, 80f, 100f ) / 100f;
PanelTransform buttonTransform = new();
buttonTransform.AddTranslate( Length.Pixels( hoverX ), Length.Pixels( hoverY ), null );
buttonTransform.AddScale( scale );
panel.Style.Transform = buttonTransform;
if ( layout.ShowDecisionButtonChrome )
{
panel.Style.BorderWidth = Length.Pixels( theme.HudBorderWidth );
if ( panel.HasClass( "safe" ) )
{
panel.Style.BorderColor = theme.PositiveBorder;
panel.Style.BackgroundColor = InteractiveColor( panel, theme.Positive, theme.PositiveHover, theme.PositivePressed );
}
else if ( panel.HasClass( "risk" ) )
{
panel.Style.BorderColor = theme.DangerBorder;
panel.Style.BackgroundColor = InteractiveColor( panel, theme.Danger, theme.DangerHover, theme.DangerPressed );
}
else
{
panel.Style.BorderColor = theme.AccentBorder;
panel.Style.BackgroundColor = InteractiveColor( panel, theme.Accent, theme.AccentHover, theme.AccentPressed );
}
SetCornerRadius( panel, theme.ButtonCornerRadius );
}
else
{
panel.Style.BorderWidth = Length.Pixels( 0f );
panel.Style.BackgroundColor = Color.Transparent;
}
}
if ( panel.HasClass( "decision-button-title" ) )
{
panel.Style.FontFamily = theme.DisplayFont;
panel.Style.FontSize = Length.Pixels( layout.DecisionButtonTitleSize );
Panel action = panel.Ancestors.FirstOrDefault( x => x.HasClass( "decision-button" ) );
panel.Style.FontColor = action?.HasClass( "safe" ) == true
? theme.PositiveText
: action?.HasClass( "risk" ) == true
? theme.DangerText
: theme.AccentText;
ApplyTextLayout( panel, layout.DecisionButtonTitleAlignment, layout.DecisionButtonTitleIndent );
}
if ( panel.HasClass( "decision-button-description" ) )
{
panel.Style.Display = layout.ShowDecisionButtonDescriptions ? DisplayMode.Flex : DisplayMode.None;
panel.Style.FontSize = Length.Pixels( layout.DecisionButtonDescriptionSize );
ApplyTextLayout( panel, layout.DecisionButtonDescriptionAlignment, layout.DecisionButtonDescriptionIndent );
}
// ------------------------------------------------------------
// AMMO
// ------------------------------------------------------------
if ( panel.HasClass( "ammo-hud" ) )
{
panel.Style.Display = layout.ShowAmmo ? DisplayMode.Flex : DisplayMode.None;
panel.Style.MinWidth = Length.Pixels( layout.AmmoMinimumWidth );
panel.Style.RowGap = Length.Pixels( layout.AmmoElementGap );
SetPadding( panel, layout.AmmoPadding );
ApplyScreenAnchor( panel, layout.AmmoAnchor, layout.AmmoRight, layout.AmmoBottom, layout.AmmoOffsetX, layout.AmmoOffsetY );
ApplyHorizontalContainerAlignment( panel, layout.AmmoAlignment );
}
if ( panel.HasClass( "ammo-hud-chrome" ) )
{
panel.Style.BackgroundColor = theme.AmmoPanel;
panel.Style.BorderColor = theme.Border;
panel.Style.BorderWidth = Length.Pixels( theme.HudBorderWidth );
panel.Style.Opacity = layout.ShowAmmoChrome ? layout.AmmoChromeOpacity : 0f;
SetCornerRadius( panel, theme.HudCornerRadius );
ApplyBackdropBlur( panel, theme.HudBackdropBlur );
ApplyHudPanelShadow( panel, theme );
}
if ( panel.HasClass( "ammo-weapon" ) )
{
panel.Style.Display = layout.ShowAmmoWeaponName ? DisplayMode.Flex : DisplayMode.None;
panel.Style.FontSize = Length.Pixels( layout.AmmoWeaponSize );
panel.Style.FontColor = theme.SecondaryText;
panel.Style.Opacity = layout.AmmoOpacity;
ApplyTextLayout( panel, layout.AmmoAlignment, 0f );
}
if ( panel.HasClass( "ammo-state" ) )
{
panel.Style.Display = layout.ShowAmmoReloadState ? DisplayMode.Flex : DisplayMode.None;
panel.Style.FontSize = Length.Pixels( layout.AmmoReloadSize );
panel.Style.Opacity = layout.AmmoOpacity;
ApplyTextLayout( panel, layout.AmmoAlignment, 0f );
}
if ( panel.HasClass( "ammo-count" ) )
{
panel.Style.ColumnGap = Length.Pixels( layout.AmmoCountGap );
panel.Style.Opacity = layout.AmmoOpacity;
}
if ( panel.HasClass( "ammo-current" ) )
{
panel.Style.FontFamily = theme.NumericFont;
panel.Style.FontSize = Length.Pixels( layout.AmmoCurrentSize );
panel.Style.FontColor = theme.OverrideAmmoCurrentColor ? theme.AmmoCurrentColor : theme.PrimaryText;
}
if ( panel.HasClass( "ammo-divider" ) )
{
panel.Style.Display = layout.ShowAmmoDivider ? DisplayMode.Flex : DisplayMode.None;
panel.Style.FontSize = Length.Pixels( layout.AmmoDividerSize );
panel.Style.FontColor = theme.MutedText;
}
if ( panel.HasClass( "ammo-capacity" ) )
{
panel.Style.FontFamily = theme.NumericFont;
panel.Style.FontSize = Length.Pixels( layout.AmmoCapacitySize );
panel.Style.FontColor = theme.SecondaryText;
}
// ------------------------------------------------------------
// SANDBOX / WARMUP
// ------------------------------------------------------------
if ( panel.HasClass( "sandbox-label" ) )
{
panel.Style.Display = layout.ShowSandboxLabel ? DisplayMode.Flex : DisplayMode.None;
panel.Style.MinWidth = layout.SandboxLabelMinimumWidth > 0f ? Length.Pixels( layout.SandboxLabelMinimumWidth ) : null;
SetPadding( panel, layout.SandboxLabelPadding );
panel.Style.RowGap = Length.Pixels( layout.SandboxElementGap );
panel.Style.BackgroundColor = Color.Transparent;
ApplyScreenAnchor( panel, layout.SandboxLabelAnchor, layout.SandboxLabelLeft, layout.SandboxLabelTop, layout.SandboxLabelOffsetX, layout.SandboxLabelOffsetY );
ApplyHorizontalContainerAlignment( panel, layout.SandboxLabelAlignment );
}
if ( panel.HasClass( "sandbox-label-chrome" ) )
{
bool warmup = panel.Ancestors.Any( x => x.HasClass( "sandbox-label" ) && x.HasClass( "warmup-label" ) );
panel.Style.BackgroundColor = warmup ? theme.WarmupLabel : theme.SandboxLabel;
panel.Style.BorderColor = warmup ? theme.AccentBorder : theme.Border;
panel.Style.BorderWidth = Length.Pixels( theme.HudBorderWidth );
panel.Style.Opacity = layout.ShowSandboxLabelChrome ? layout.SandboxLabelChromeOpacity : 0f;
SetCornerRadius( panel, theme.HudCornerRadius );
ApplyBackdropBlur( panel, theme.HudBackdropBlur );
ApplyHudPanelShadow( panel, theme );
}
if ( panel.HasClass( "sandbox-title" ) )
{
bool warmup = panel.Ancestors.Any( x => x.HasClass( "sandbox-label" ) && x.HasClass( "warmup-label" ) );
panel.Style.FontFamily = theme.DisplayFont;
panel.Style.FontSize = Length.Pixels( layout.SandboxTitleSize );
panel.Style.FontColor = warmup
? (theme.OverrideWarmupTitleColor ? theme.WarmupTitleColor : theme.AccentText)
: (theme.OverrideSandboxTitleColor ? theme.SandboxTitleColor : theme.PrimaryText);
panel.Style.Opacity = layout.SandboxLabelOpacity;
ApplyTextLayout( panel, layout.SandboxLabelAlignment, 0f );
}
if ( panel.HasClass( "sandbox-status" ) )
{
panel.Style.Display = layout.ShowSandboxStatus ? DisplayMode.Flex : DisplayMode.None;
panel.Style.FontSize = Length.Pixels( layout.SandboxStatusSize );
panel.Style.Opacity = layout.SandboxLabelOpacity;
ApplyTextLayout( panel, layout.SandboxLabelAlignment, 0f );
}
if ( panel.HasClass( "sandbox-hint" ) )
{
panel.Style.Display = layout.ShowSandboxHint ? DisplayMode.Flex : DisplayMode.None;
panel.Style.FontSize = Length.Pixels( layout.SandboxHintSize );
panel.Style.Opacity = layout.SandboxLabelOpacity;
ApplyTextLayout( panel, layout.SandboxLabelAlignment, 0f );
}
// The developer preview toolbar is intentionally themed too, making the
// generic HUD surface and selected accent state visible while tuning.
if ( panel.HasClass( "hud-preview-toolbar" ) )
{
panel.Style.BackgroundColor = theme.HudPanel;
panel.Style.BorderColor = theme.SoftBorder;
panel.Style.BorderWidth = Length.Pixels( theme.HudBorderWidth );
SetCornerRadius( panel, theme.HudCornerRadius );
ApplyBackdropBlur( panel, theme.HudBackdropBlur );
ApplyHudPanelShadow( panel, theme );
}
if ( panel.HasClass( "hud-preview-state" ) )
{
bool selected = panel.HasClass( "active" );
panel.Style.BackgroundColor = selected
? theme.AccentSelected
: InteractiveColor( panel, theme.Button, theme.ButtonHover, theme.ButtonPressed );
panel.Style.BorderColor = selected ? theme.AccentBorder : theme.SoftBorder;
SetCornerRadius( panel, theme.ControlCornerRadius );
}
// Semantic text roles. Keep this explicit rather than using generic class
// names so one theme control cannot accidentally recolor the entire HUD.
if ( panel.HasClass( "decision-eyebrow" ) )
panel.Style.FontColor = theme.AccentText;
else if ( (panel.HasClass( "timer-mode" ) || panel.HasClass( "timer-note" )) && panel.Ancestors.Any( x => x.HasClass( "round-timer" ) && x.HasClass( "sudden" ) ) )
panel.Style.FontColor = theme.DangerText;
else if ( panel.HasClass( "ammo-state" ) )
panel.Style.FontColor = theme.AccentText;
else if ( panel.HasClass( "sandbox-hint" ) )
panel.Style.FontColor = theme.MutedText;
else if (
panel.HasClass( "decision-pot" ) ||
panel.HasClass( "decision-button-description" ) ||
panel.HasClass( "timer-mode" ) ||
panel.HasClass( "timer-note" ) ||
panel.HasClass( "score-label" ) ||
panel.HasClass( "score-player-label" ) ||
panel.HasClass( "pot-owner" ) ||
panel.HasClass( "sandbox-status" ) ||
panel.HasClass( "ammo-weapon" )
)
panel.Style.FontColor = theme.SecondaryText;
}
}
private static void ApplyTextFormatting( Panel panel, OMRUITheme theme )
{
if ( panel.HasClass( "menu-title" ) )
OMRUITextFormat.Apply( panel, theme.TitleFormat );
else if ( panel.HasClass( "menu-subtitle" ) )
OMRUITextFormat.Apply( panel, theme.SubtitleFormat );
else if (
panel.HasClass( "health-number" ) ||
panel.HasClass( "timer-number" ) ||
panel.HasClass( "score-number" ) ||
panel.HasClass( "ammo-current" ) ||
panel.HasClass( "ammo-capacity" ) ||
panel.HasClass( "decision-timer" ) ||
panel.HasClass( "final-score" ) ||
panel.HasClass( "pot-value" ) ||
panel.HasClass( "theme-preview-number" )
)
OMRUITextFormat.Apply( panel, theme.NumberFormat );
else if (
panel.HasClass( "page-heading" ) ||
panel.HasClass( "pause-title" ) ||
panel.HasClass( "round" ) ||
panel.HasClass( "center-message" ) ||
panel.HasClass( "decision-title" ) ||
panel.HasClass( "sandbox-title" ) ||
panel.HasClass( "modifier-label" ) ||
panel.HasClass( "online-status-label" ) ||
panel.HasClass( "setting-name" )
)
OMRUITextFormat.Apply( panel, theme.HeadingFormat );
else if (
panel.HasClass( "button-title" ) ||
panel.HasClass( "menu-primary" ) ||
panel.HasClass( "menu-back" ) ||
panel.HasClass( "pause-button-title" ) ||
panel.HasClass( "decision-button-title" ) ||
panel.HasClass( "theme-interactive-chip" )
)
OMRUITextFormat.Apply( panel, theme.ButtonFormat );
else if (
panel.HasClass( "health-label" ) ||
panel.HasClass( "score-label" ) ||
panel.HasClass( "score-player-label" ) ||
panel.HasClass( "timer-mode" ) ||
panel.HasClass( "timer-note" ) ||
panel.HasClass( "decision-eyebrow" ) ||
panel.HasClass( "decision-pot" ) ||
panel.HasClass( "pot-owner" ) ||
panel.HasClass( "sandbox-status" ) ||
panel.HasClass( "sandbox-hint" ) ||
panel.HasClass( "ammo-weapon" ) ||
panel.HasClass( "ammo-state" ) ||
panel.HasClass( "session-notice" ) ||
panel.HasClass( "pause-eyebrow" ) ||
panel.HasClass( "online-subheading" ) ||
panel.HasClass( "lobby-ready-summary" ) ||
panel.HasClass( "setting-value" ) ||
panel.HasClass( "hud-preview-toolbar-title" ) ||
panel.HasClass( "theme-preview-meta" )
)
OMRUITextFormat.Apply( panel, theme.MetaFormat );
else if (
panel.HasClass( "page-copy" ) ||
panel.HasClass( "button-description" ) ||
panel.HasClass( "lobby-note" ) ||
panel.HasClass( "setting-description" ) ||
panel.HasClass( "pause-note" ) ||
panel.HasClass( "pause-summary" ) ||
panel.HasClass( "pause-button-copy" ) ||
panel.HasClass( "decision-button-description" ) ||
panel.HasClass( "theme-preview-body" )
)
OMRUITextFormat.Apply( panel, theme.BodyFormat );
}
private static IEnumerable<Panel> Panels( Panel root )
{
yield return root;
foreach ( Panel panel in root.Descendants )
yield return panel;
}
private static ObjectFit Fit( OMRImageFit fit ) => fit switch
{
OMRImageFit.Contain => ObjectFit.Contain,
OMRImageFit.Fill => ObjectFit.Fill,
_ => ObjectFit.Cover
};
private static void ApplyTextLayout(
Panel panel,
OMRTextHorizontalAlignment alignment,
float indent
)
{
// Configurable UI text is authored as Label panels. Give every text
// element a real horizontal layout area instead of letting it collapse
// to the intrinsic width of its glyphs. Without this, text-align can be
// technically applied while producing almost no visible movement.
panel.Style.Width = Length.Percent( 100f );
panel.Style.AlignSelf = Align.Stretch;
panel.Style.TextAlign = alignment switch
{
OMRTextHorizontalAlignment.Left => TextAlign.Left,
OMRTextHorizontalAlignment.Right => TextAlign.Right,
_ => TextAlign.Center
};
float safeIndent = Math.Max( 0f, indent );
panel.Style.PaddingLeft = Length.Pixels(
alignment == OMRTextHorizontalAlignment.Right ? 0f : safeIndent
);
panel.Style.PaddingRight = Length.Pixels(
alignment == OMRTextHorizontalAlignment.Left ? 0f : safeIndent
);
}
private static void ApplyActionAlignment(
Panel panel,
OMRTextHorizontalAlignment alignment,
float indent
)
{
panel.Style.JustifyContent = alignment switch
{
OMRTextHorizontalAlignment.Left => Justify.FlexStart,
OMRTextHorizontalAlignment.Right => Justify.FlexEnd,
_ => Justify.Center
};
const float baseHorizontalPadding = 20f;
float safeIndent = Math.Max( 0f, indent );
panel.Style.PaddingLeft = Length.Pixels(
baseHorizontalPadding +
(alignment == OMRTextHorizontalAlignment.Left ? safeIndent : 0f)
);
panel.Style.PaddingRight = Length.Pixels(
baseHorizontalPadding +
(alignment == OMRTextHorizontalAlignment.Right ? safeIndent : 0f)
);
}
private static void ApplyHorizontalContainerAlignment(
Panel panel,
OMRTextHorizontalAlignment alignment
)
{
panel.Style.AlignItems = alignment switch
{
OMRTextHorizontalAlignment.Left => Align.FlexStart,
OMRTextHorizontalAlignment.Right => Align.FlexEnd,
_ => Align.Center
};
}
private static void ApplyPairAlignment(
Panel panel,
OMRHUDPairAlignment alignment
)
{
panel.Style.JustifyContent = alignment switch
{
OMRHUDPairAlignment.Left => Justify.FlexStart,
OMRHUDPairAlignment.Center => Justify.Center,
OMRHUDPairAlignment.Right => Justify.FlexEnd,
_ => Justify.SpaceBetween
};
}
private static void ApplyScreenAnchor(
Panel panel,
OMRHUDScreenAnchor anchor,
float horizontalInset,
float verticalInset,
float offsetX,
float offsetY
)
{
panel.Style.Left = null;
panel.Style.Right = null;
panel.Style.Top = null;
panel.Style.Bottom = null;
bool centerX = anchor is OMRHUDScreenAnchor.TopCenter or OMRHUDScreenAnchor.Center or OMRHUDScreenAnchor.BottomCenter;
bool right = anchor is OMRHUDScreenAnchor.TopRight or OMRHUDScreenAnchor.MiddleRight or OMRHUDScreenAnchor.BottomRight;
bool centerY = anchor is OMRHUDScreenAnchor.MiddleLeft or OMRHUDScreenAnchor.Center or OMRHUDScreenAnchor.MiddleRight;
bool bottom = anchor is OMRHUDScreenAnchor.BottomLeft or OMRHUDScreenAnchor.BottomCenter or OMRHUDScreenAnchor.BottomRight;
if ( centerX )
panel.Style.Left = Length.Percent( 50f );
else if ( right )
panel.Style.Right = Length.Pixels( horizontalInset );
else
panel.Style.Left = Length.Pixels( horizontalInset );
if ( centerY )
panel.Style.Top = Length.Percent( 50f );
else if ( bottom )
panel.Style.Bottom = Length.Pixels( verticalInset );
else
panel.Style.Top = Length.Pixels( verticalInset );
PanelTransform transform = new();
if ( centerX )
transform.AddTranslate( Length.Percent( -50f ), Length.Pixels( 0f ), null );
if ( centerY )
transform.AddTranslate( Length.Pixels( 0f ), Length.Percent( -50f ), null );
transform.AddTranslate( Length.Pixels( offsetX ), Length.Pixels( offsetY ), null );
panel.Style.Transform = transform;
}
private static void ApplyPrimaryPanelShadow( Panel panel, OMRUITheme theme )
{
ApplyBoxShadow(
panel,
theme.EnablePrimaryPanelShadow,
theme.PrimaryPanelShadowOffsetX,
theme.PrimaryPanelShadowOffsetY,
theme.PrimaryPanelShadowBlur,
theme.PrimaryPanelShadowSpread,
theme.PrimaryPanelShadowColor
);
}
private static void ApplyHudPanelShadow( Panel panel, OMRUITheme theme )
{
ApplyBoxShadow(
panel,
theme.EnableHudPanelShadow,
theme.HudPanelShadowOffsetX,
theme.HudPanelShadowOffsetY,
theme.HudPanelShadowBlur,
theme.HudPanelShadowSpread,
theme.HudPanelShadowColor
);
}
private static void ApplyBoxShadow( Panel panel, bool enabled, float x, float y, float blur, float spread, Color color )
{
if ( !enabled )
{
panel.Style.Set( "box-shadow", "none" );
return;
}
string value = string.Format(
CultureInfo.InvariantCulture,
"{0:0.###}px {1:0.###}px {2:0.###}px {3:0.###}px {4}",
x, y, Math.Max( 0f, blur ), spread, color.Rgba
);
panel.Style.Set( "box-shadow", value );
}
private static void ApplyTextShadow( Panel panel, bool enabled, float x, float y, float blur, Color color )
{
if ( !enabled )
{
panel.Style.Set( "text-shadow", "none" );
return;
}
string value = string.Format(
CultureInfo.InvariantCulture,
"{0:0.###}px {1:0.###}px {2:0.###}px {3}",
x, y, Math.Max( 0f, blur ), color.Rgba
);
panel.Style.Set( "text-shadow", value );
}
private static void StylePreviewSurface(
Panel panel,
Color background,
Color border,
float radius,
float blur = 0f,
float borderWidth = 1f
)
{
panel.Style.BackgroundColor = background;
panel.Style.BorderColor = border;
panel.Style.BorderWidth = Length.Pixels( Math.Max( 0f, borderWidth ) );
SetCornerRadius( panel, radius );
ApplyBackdropBlur( panel, blur );
}
private static Color InteractiveColor( Panel panel, Color normal, Color hover, Color pressed )
{
if ( panel.HasActive )
return pressed;
if ( panel.HasHovered )
return hover;
return normal;
}
private static void ApplyBackdropBlur( Panel panel, float pixels )
{
panel.Style.BackdropFilterBlur = Length.Pixels( Math.Max( 0f, pixels ) );
}
private static void ApplyDisplayTextEffect( Panel panel, OMRUITheme theme )
{
bool displayText =
panel.HasClass( "menu-title" ) ||
panel.HasClass( "page-heading" ) ||
panel.HasClass( "pause-title" ) ||
panel.HasClass( "round" ) ||
panel.HasClass( "center-message" ) ||
panel.HasClass( "decision-title" ) ||
panel.HasClass( "sandbox-title" );
if ( !displayText )
return;
ApplyTextShadow(
panel,
theme.EnableDisplayTextShadow,
theme.DisplayTextShadowOffsetX,
theme.DisplayTextShadowOffsetY,
theme.DisplayTextShadowBlur,
theme.DisplayTextShadowColor
);
bool enabled = theme.EnableDisplayTextStroke && theme.DisplayTextStrokeWidth > 0f;
panel.Style.TextStrokeColor = enabled ? theme.DisplayTextStrokeColor : Color.Transparent;
panel.Style.TextStrokeWidth = Length.Pixels( enabled
? Math.Max( 0f, theme.DisplayTextStrokeWidth )
: 0f );
}
private static void SetCornerRadius( Panel panel, float pixels )
{
Length? radius = Length.Pixels( Math.Max( 0f, pixels ) );
panel.Style.BorderTopLeftRadius = radius;
panel.Style.BorderTopRightRadius = radius;
panel.Style.BorderBottomLeftRadius = radius;
panel.Style.BorderBottomRightRadius = radius;
}
private static void SetPadding( Panel panel, float pixels )
{
Length? value = Length.Pixels( pixels );
panel.Style.PaddingLeft = value;
panel.Style.PaddingRight = value;
panel.Style.PaddingTop = value;
panel.Style.PaddingBottom = value;
}
}