Files
Simple-Squad-Menu/lua/includes/modules/styled_theme.lua
2025-05-31 10:13:27 +02:00

682 lines
21 KiB
Lua

--[[
StyledStrike's VGUI theme utilities
A collection of functions to create common
UI panels and to apply a custom theme to them.
]]
StyledTheme = StyledTheme or {}
--[[
Setup color constants
]]
do
StyledTheme.colors = StyledTheme.colors or {}
local colors = StyledTheme.colors or {}
colors.accent = Color( 56, 113, 179 )
colors.panelBackground = Color( 46, 46, 46, 240 )
colors.panelDisabledBackground = Color( 90, 90, 90, 255 )
colors.scrollBackground = Color( 0, 0, 0, 200 )
colors.labelText = Color( 255, 255, 255, 255 )
colors.labelTextDisabled = Color( 180, 180, 180, 255 )
colors.buttonHover = Color( 150, 150, 150, 50 )
colors.buttonPress = colors.accent
colors.buttonBorder = Color( 32, 32, 32, 255 )
colors.buttonText = Color( 255, 255, 255, 255 )
colors.buttonTextDisabled = Color( 180, 180, 180, 255 )
colors.entryBackground = Color( 20, 20, 20, 255 )
colors.entryBorder = Color( 80, 80, 80, 255 )
colors.entryHighlight = colors.accent
colors.entryPlaceholder = Color( 150, 150, 150, 255 )
colors.entryText = Color( 255, 255, 255, 255 )
end
--[[
Setup dimensions
]]
StyledTheme.dimensions = StyledTheme.dimensions or {}
hook.Add( "StyledTheme_OnResolutionChange", "StyledTheme.UpdateDimensions", function()
local dimensions = StyledTheme.dimensions
local ScaleSize = StyledTheme.ScaleSize
dimensions.framePadding = ScaleSize( 10 )
dimensions.frameButtonSize = ScaleSize( 36 )
dimensions.buttonHeight = ScaleSize( 40 )
dimensions.headerHeight = ScaleSize( 32 )
dimensions.scrollBarWidth = ScaleSize( 16 )
dimensions.scrollPadding = ScaleSize( 8 )
dimensions.formPadding = ScaleSize( 20 )
dimensions.formSeparator = ScaleSize( 6 )
dimensions.formLabelWidth = ScaleSize( 300 )
dimensions.menuPadding = ScaleSize( 6 )
dimensions.indicatorSize = ScaleSize( 20 )
end )
--[[
Setup fonts
]]
StyledTheme.BASE_FONT_NAME = "Roboto"
StyledTheme.fonts = StyledTheme.fonts or {}
function StyledTheme.RegisterFont( name, screenSize, data )
data = data or {}
data.screenSize = screenSize
data.font = data.font or StyledTheme.BASE_FONT_NAME
data.extended = true
StyledTheme.fonts[name] = data
StyledTheme.forceUpdateResolution = true
end
StyledTheme.RegisterFont( "StyledTheme_Small", 0.018, {
weight = 500,
} )
StyledTheme.RegisterFont( "StyledTheme_Tiny", 0.013, {
weight = 500,
} )
hook.Add( "StyledTheme_OnResolutionChange", "StyledTheme.UpdateFonts", function( _, screenH )
for name, data in pairs( StyledTheme.fonts ) do
data.size = math.floor( screenH * data.screenSize )
surface.CreateFont( name, data )
end
end )
--[[
Watch for changes in screen resolution
]]
do
local screenW, screenH = ScrW(), ScrH()
local Floor = math.floor
--- Scales the given size (in pixels) from a 1080p resolution to
--- the resolution currently being used by the game.
function StyledTheme.ScaleSize( size )
return Floor( ( size / 1080 ) * screenH )
end
local function UpdateResolution()
screenW, screenH = ScrW(), ScrH()
StyledTheme.forceUpdateResolution = false
hook.Run( "StyledTheme_OnResolutionChange", screenW, screenH )
end
-- Only update resolution on gamemode initialization.
hook.Add( "Initialize", "StyledTheme.UpdateResolution", UpdateResolution )
local ScrW, ScrH = ScrW, ScrH
timer.Create( "StyledTheme.CheckResolution", 2, 0, function()
if ScrW() ~= screenW or ScrH() ~= screenH or StyledTheme.forceUpdateResolution then
UpdateResolution()
end
end )
end
--[[
Misc. utility functions
]]
do
--- Gets a localized language string, with the first character being in uppercase.
function StyledTheme.GetUpperLanguagePhrase( text )
text = language.GetPhrase( text )
return text:sub( 1, 1 ):upper() .. text:sub( 2 )
end
local SetDrawColor = surface.SetDrawColor
local DrawRect = surface.DrawRect
--- Draw box, using the specified background color.
--- It allows overriding the alpha while keeping the supplied color table intact.
function StyledTheme.DrawRect( x, y, w, h, color, alpha )
alpha = alpha or 1
SetDrawColor( color.r, color.g, color.b, color.a * alpha )
DrawRect( x, y, w, h )
end
local SetMaterial = surface.SetMaterial
local MAT_BLUR = Material( "pp/blurscreen" )
--- Blur the background of a panel.
function StyledTheme.BlurPanel( panel, alpha, density )
SetDrawColor( 255, 255, 255, alpha or panel:GetAlpha() )
SetMaterial( MAT_BLUR )
MAT_BLUR:SetFloat( "$blur", density or 4 )
MAT_BLUR:Recompute()
render.UpdateScreenEffectTexture()
local x, y = panel:LocalToScreen( 0, 0 )
surface.DrawTexturedRect( -x, -y, ScrW(), ScrH() )
end
local cache = {}
-- Get a material given a path to a material or .png file.
function StyledTheme.GetMaterial( path )
if cache[path] then
return cache[path]
end
cache[path] = Material( path, "smooth ignorez" )
return cache[path]
end
local GetMaterial = StyledTheme.GetMaterial
local DrawTexturedRect = surface.DrawTexturedRect
local COLOR_WHITE = Color( 255, 255, 255, 255 )
--- Draw a icon, using the specified image file path and color.
--- It allows overriding the alpha while keeping the supplied color table intact.
function StyledTheme.DrawIcon( path, x, y, w, h, alpha, color )
color = color or COLOR_WHITE
alpha = alpha or 1
SetMaterial( GetMaterial( path ) )
SetDrawColor( color.r, color.g, color.b, 255 * alpha )
DrawTexturedRect( x, y, w, h )
end
end
--[[
Utility function to apply the theme to existing VGUI panels
]]
do
local ClassFunctions = {}
function StyledTheme.Apply( panel, classOverride )
local funcs = ClassFunctions[classOverride or panel.ClassName]
if not funcs then return end
if funcs.Prepare then
funcs.Prepare( panel )
end
if funcs.Paint then
panel.Paint = funcs.Paint
end
if funcs.UpdateColours then
panel.UpdateColours = funcs.UpdateColours
end
if funcs.Close then
panel.Close = funcs.Close
end
end
local colors = StyledTheme.colors
local dimensions = StyledTheme.dimensions
local DrawRect = StyledTheme.DrawRect
ClassFunctions["DLabel"] = {
Prepare = function( self )
self:SetColor( colors.labelText )
self:SetFont( "StyledTheme_Small" )
end
}
ClassFunctions["DPanel"] = {
Paint = function( self, w, h )
DrawRect( 0, 0, w, h, self:GetBackgroundColor() or colors.panelBackground )
end
}
local function CustomMenuAdd( self, class )
local pnl = self:OriginalAdd( class )
if class == "DButton" then
StyledTheme.Apply( pnl )
timer.Simple( 0, function()
if not IsValid( pnl ) then return end
pnl:SetPaintBackground( true )
pnl:SizeToContentsX( StyledTheme.ScaleSize( 20 ) )
pnl:DockMargin( 0, 0, dimensions.menuPadding, 0 )
end )
end
return pnl
end
ClassFunctions["DMenuBar"] = {
Prepare = function( self )
self:SetTall( dimensions.buttonHeight )
self:DockMargin( 0, 0, 0, 0 )
self:DockPadding( dimensions.menuPadding, dimensions.menuPadding, dimensions.menuPadding, dimensions.menuPadding )
self.OriginalAdd = self.Add
self.Add = CustomMenuAdd
end,
Paint = function( self, w, h )
DrawRect( 0, 0, w, h, self:GetBackgroundColor() or colors.accent )
end
}
local Lerp = Lerp
local FrameTime = FrameTime
ClassFunctions["DButton"] = {
Prepare = function( self )
self:SetFont( "StyledTheme_Small" )
self:SetTall( dimensions.buttonHeight )
self.animHover = 0
self.animPress = 0
end,
Paint = function( self, w, h )
local dt = FrameTime() * 10
local enabled = self:IsEnabled()
self.animHover = Lerp( dt, self.animHover, ( enabled and self.Hovered ) and 1 or 0 )
self.animPress = Lerp( dt, self.animPress, ( enabled and ( self:IsDown() or self.m_bSelected ) ) and 1 or 0 )
DrawRect( 0, 0, w, h, ( self.isToggle and self.isChecked ) and colors.buttonPress or colors.buttonBorder )
DrawRect( 1, 1, w - 2, h - 2, enabled and colors.panelBackground or colors.panelDisabledBackground )
DrawRect( 1, 1, w - 2, h - 2, colors.buttonHover, self.animHover )
DrawRect( 1, 1, w - 2, h - 2, colors.buttonPress, self.animPress )
end,
UpdateColours = function( self )
if self:IsEnabled() then
self:SetTextStyleColor( colors.buttonText )
else
self:SetTextStyleColor( colors.buttonTextDisabled )
end
end
}
ClassFunctions["DBinder"] = ClassFunctions["DButton"]
ClassFunctions["DTextEntry"] = {
Prepare = function( self )
self:SetFont( "StyledTheme_Small" )
self:SetTall( dimensions.buttonHeight )
self:SetDrawBorder( false )
self:SetPaintBackground( false )
self:SetTextColor( colors.entryText )
self:SetCursorColor( colors.entryText )
self:SetHighlightColor( colors.entryHighlight )
self:SetPlaceholderColor( colors.entryPlaceholder )
end,
Paint = function( self, w, h )
local enabled = self:IsEnabled()
DrawRect( 0, 0, w, h, ( self:IsEditing() and enabled ) and colors.entryHighlight or colors.entryBorder )
DrawRect( 1, 1, w - 2, h - 2, enabled and colors.entryBackground or colors.panelDisabledBackground )
derma.SkinHook( "Paint", "TextEntry", self, w, h )
end
}
ClassFunctions["DComboBox"] = {
Prepare = function( self )
self:SetFont( "StyledTheme_Small" )
self:SetTall( dimensions.buttonHeight )
self:SetTextColor( colors.entryText )
self.animHover = 0
end,
Paint = function( self, w, h )
local dt = FrameTime() * 10
local enabled = self:IsEnabled()
self.animHover = Lerp( dt, self.animHover, ( enabled and self.Hovered ) and 1 or 0 )
DrawRect( 0, 0, w, h, ( self:IsMenuOpen() and enabled ) and colors.entryHighlight or colors.buttonBorder )
DrawRect( 1, 1, w - 2, h - 2, enabled and colors.panelBackground or colors.panelDisabledBackground )
DrawRect( 1, 1, w - 2, h - 2, colors.buttonHover, self.animHover )
end
}
ClassFunctions["DNumSlider"] = {
Prepare = function( self )
StyledTheme.Apply( self.TextArea )
StyledTheme.Apply( self.Label )
end
}
ClassFunctions["DScrollPanel"] = {
Prepare = function( self )
StyledTheme.Apply( self.VBar )
local padding = dimensions.scrollPadding
self.pnlCanvas:DockPadding( padding, padding, padding, padding )
self:SetPaintBackground( true )
end,
Paint = function( self, w, h )
if self:GetPaintBackground() then
DrawRect( 0, 0, w, h, colors.scrollBackground )
end
end
}
local Clamp = math.Clamp
local function AddScroll( self, delta )
local oldScroll = self.animTargetScroll or self:GetScroll()
local newScroll = Clamp( oldScroll + delta * 40, 0, self.CanvasSize )
if oldScroll == newScroll then
return false
end
self:Stop()
self.animTargetScroll = newScroll
local anim = self:NewAnimation( 0.4, 0, 0.25, function( _, pnl )
pnl.animTargetScroll = nil
end )
anim.StartPos = oldScroll
anim.TargetPos = newScroll
anim.Think = function( a, pnl, fraction )
pnl:SetScroll( Lerp( fraction, a.StartPos, a.TargetPos ) )
end
return true
end
local function DrawGrip( self, w, h )
local dt = FrameTime() * 10
self.animHover = Lerp( dt, self.animHover, self.Hovered and 1 or 0 )
self.animPress = Lerp( dt, self.animPress, self.Depressed and 1 or 0 )
DrawRect( 0, 0, w, h, colors.buttonBorder )
DrawRect( 1, 1, w - 2, h - 2, colors.panelBackground )
DrawRect( 1, 1, w - 2, h - 2, colors.buttonHover, self.animHover )
DrawRect( 1, 1, w - 2, h - 2, colors.buttonPress, self.animPress )
end
ClassFunctions["DVScrollBar"] = {
Prepare = function( self )
self:SetWide( dimensions.scrollBarWidth )
self:SetHideButtons( true )
self.AddScroll = AddScroll
self.btnGrip.animHover = 0
self.btnGrip.animPress = 0
self.btnGrip.Paint = DrawGrip
end,
Paint = function( _, w, h )
DrawRect( 0, 0, w, h, colors.scrollBackground )
end
}
local function FrameSlideAnim( anim, panel, fraction )
if not anim.StartPos then
anim.StartPos = Vector( panel.x, panel.y + anim.StartOffset, 0 )
anim.TargetPos = Vector( panel.x, panel.y + anim.EndOffset, 0 )
end
local pos = LerpVector( fraction, anim.StartPos, anim.TargetPos )
panel:SetPos( pos.x, pos.y )
panel:SetAlpha( 255 * Lerp( fraction, anim.StartAlpha, anim.EndAlpha ) )
end
local function FramePerformLayout( self, w )
local padding = dimensions.framePadding
local buttonSize = dimensions.frameButtonSize
self.btnClose:SetSize( buttonSize, buttonSize )
self.btnClose:SetPos( w - self.btnClose:GetWide() - padding, padding )
local iconMargin = 0
if IsValid( self.imgIcon ) then
local iconSize = buttonSize * 0.6
self.imgIcon:SetPos( padding, padding + ( buttonSize * 0.5 ) - ( iconSize * 0.5 ) )
self.imgIcon:SetSize( iconSize, iconSize )
iconMargin = iconSize + padding * 0.5
end
self.lblTitle:SetPos( padding + iconMargin, padding )
self.lblTitle:SetSize( w - ( padding * 2 ) - iconMargin, buttonSize )
end
ClassFunctions["DFrame"] = {
Prepare = function( self )
self._OriginalClose = self.Close
self.PerformLayout = FramePerformLayout
StyledTheme.Apply( self.btnClose )
StyledTheme.Apply( self.lblTitle )
local padding = dimensions.framePadding
local buttonSize = dimensions.frameButtonSize
self:DockPadding( padding, buttonSize + padding * 2, padding, padding )
self.btnClose:SetText( "X" )
if IsValid( self.btnMaxim ) then
self.btnMaxim:Remove()
end
if IsValid( self.btnMinim ) then
self.btnMinim:Remove()
end
local anim = self:NewAnimation( 0.4, 0, 0.25 )
anim.StartOffset = -80
anim.EndOffset = 0
anim.StartAlpha = 0
anim.EndAlpha = 1
anim.Think = FrameSlideAnim
end,
Close = function( self )
self:SetMouseInputEnabled( false )
self:SetKeyboardInputEnabled( false )
if self.OnStartClosing then
self.OnStartClosing()
end
local anim = self:NewAnimation( 0.2, 0, 0.5, function()
self:_OriginalClose()
end )
anim.StartOffset = 0
anim.EndOffset = -80
anim.StartAlpha = 1
anim.EndAlpha = 0
anim.Think = FrameSlideAnim
end,
Paint = function( self, w, h )
if self.m_bBackgroundBlur then
Derma_DrawBackgroundBlur( self, self.m_fCreateTime )
else
StyledTheme.BlurPanel( self )
end
DrawRect( 0, 0, w, h, colors.panelBackground, self:GetAlpha() / 255 )
end
}
end
--[[
Utility functions to create "form" panels.
]]
do
local colors = StyledTheme.colors
local dimensions = StyledTheme.dimensions
function StyledTheme.CreateFormHeader( parent, text, mtop, mbottom )
mtop = mtop or dimensions.formSeparator
mbottom = mbottom or dimensions.formSeparator
local panel = vgui.Create( "DPanel", parent )
panel:SetTall( dimensions.headerHeight )
panel:Dock( TOP )
panel:DockMargin( -dimensions.formPadding, mtop, -dimensions.formPadding, mbottom )
panel:SetBackgroundColor( colors.scrollBackground )
StyledTheme.Apply( panel )
local label = vgui.Create( "DLabel", panel )
label:SetText( text )
label:SetContentAlignment( 5 )
label:SizeToContents()
label:Dock( FILL )
StyledTheme.Apply( label )
return panel
end
function StyledTheme.CreateFormLabel( parent, text )
local label = vgui.Create( "DLabel", parent )
label:Dock( TOP )
label:DockMargin( 0, 0, 0, dimensions.formSeparator )
label:SetText( text )
label:SetTall( dimensions.buttonHeight )
StyledTheme.Apply( label )
return label
end
function StyledTheme.CreateFormButton( parent, label, callback )
local button = vgui.Create( "DButton", parent )
button:SetText( label )
button:Dock( TOP )
button:DockMargin( 0, 0, 0, dimensions.formSeparator )
button.DoClick = callback
StyledTheme.Apply( button )
return button
end
function StyledTheme.CreateFormToggle( parent, label, isChecked, callback )
local button = vgui.Create( "DButton", parent )
button:SetIcon( isChecked and "icon16/accept.png" or "icon16/cancel.png" )
button:SetText( label )
button:Dock( TOP )
button:DockMargin( 0, 0, 0, dimensions.formSeparator )
button.isToggle = true
button.isChecked = isChecked
StyledTheme.Apply( button )
button.SetChecked = function( s, value )
value = value == true
s.isChecked = value
button:SetIcon( value and "icon16/accept.png" or "icon16/cancel.png" )
callback( value )
end
button.DoClick = function( s )
s:SetChecked( not s.isChecked )
end
return button
end
function StyledTheme.CreateFormSlider( parent, label, default, min, max, decimals, callback )
local slider = vgui.Create( "DNumSlider", parent )
slider:SetText( label )
slider:SetMin( min )
slider:SetMax( max )
slider:SetValue( default )
slider:SetDecimals( decimals )
slider:Dock( TOP )
slider:DockMargin( 0, 0, 0, dimensions.formSeparator )
slider.PerformLayout = function( s )
s.Label:SetWide( dimensions.formLabelWidth )
end
StyledTheme.Apply( slider )
slider.OnValueChanged = function( _, value )
callback( decimals == 0 and math.floor( value ) or math.Round( value, decimals ) )
end
return slider
end
function StyledTheme.CreateFormCombo( parent, text, options, defaultIndex, callback )
local panel = vgui.Create( "DPanel", parent )
panel:SetTall( dimensions.buttonHeight )
panel:SetPaintBackground( false )
panel:Dock( TOP )
panel:DockMargin( 0, 0, 0, dimensions.formSeparator )
local label = vgui.Create( "DLabel", panel )
label:Dock( LEFT )
label:DockMargin( 0, 0, 0, 0 )
label:SetText( text )
label:SetWide( dimensions.formLabelWidth )
StyledTheme.Apply( label )
local combo = vgui.Create( "DComboBox", panel )
combo:Dock( FILL )
combo:SetSortItems( false )
for _, v in ipairs( options ) do
combo:AddChoice( v )
end
if defaultIndex then
combo:ChooseOptionID( defaultIndex )
end
StyledTheme.Apply( combo )
combo.OnSelect = function( _, index )
callback( index )
end
end
function StyledTheme.CreateFormBinder( parent, text, defaultKey )
local panel = vgui.Create( "DPanel", parent )
panel:SetTall( dimensions.buttonHeight )
panel:SetPaintBackground( false )
panel:Dock( TOP )
panel:DockMargin( 0, 0, 0, dimensions.formSeparator )
local label = vgui.Create( "DLabel", panel )
label:Dock( LEFT )
label:DockMargin( 0, 0, 0, 0 )
label:SetText( text )
label:SetWide( dimensions.formLabelWidth )
StyledTheme.Apply( label )
local binder = vgui.Create( "DBinder", panel )
binder:SetValue( defaultKey or KEY_NONE )
binder:Dock( FILL )
StyledTheme.Apply( binder )
return binder
end
end