cbox/lua/cbox/cl_chatbox.lua

602 lines
16 KiB
Lua

local ScrW = ScrW
local ScrH = ScrH
local Color = Color
local STENCIL_EQUAL = STENCIL_EQUAL
local STENCIL_KEEP = STENCIL_KEEP
local STENCIL_NEVER = STENCIL_NEVER
local STENCIL_REPLACE = STENCIL_REPLACE
local draw = draw
local math = math
local render = render
local surface = surface
local draw_NoTexture = draw.NoTexture
local math_rad = math.rad
local math_sin = math.sin
local math_cos = math.cos
local render_ClearStencil = render.ClearStencil
local render_SetStencilCompareFunction = render.SetStencilCompareFunction
local render_SetStencilEnable = render.SetStencilEnable
local render_SetStencilFailOperation = render.SetStencilFailOperation
local render_SetStencilPassOperation = render.SetStencilPassOperation
local render_SetStencilReferenceValue = render.SetStencilReferenceValue
local render_SetStencilTestMask = render.SetStencilTestMask
local render_SetStencilWriteMask = render.SetStencilWriteMask
local render_SetStencilZFailOperation = render.SetStencilZFailOperation
local render_UpdateScreenEffectTexture = render.UpdateScreenEffectTexture
local surface_DrawPoly = surface.DrawPoly
local surface_DrawRect = surface.DrawRect
local surface_DrawTexturedRect = surface.DrawTexturedRect
local surface_SetDrawColor = surface.SetDrawColor
local surface_SetMaterial = surface.SetMaterial
cbox.chatbox = cbox.chatbox or {}
cbox.chatbox.panels = cbox.chatbox.panels or {}
cbox.chatbox.modes = cbox.chatbox.modes or {}
local CHATBOX_COLOR = CreateClientConVar("cbox_chatbox_color", "160 160 160", true, false, "Chatbox background color")
local CHATBOX_ALPHA = CreateClientConVar("cbox_chatbox_alpha", "128", true, false, "Chatbox background alpha")
local CHATBOX_BLUR = CreateClientConVar("cbox_chatbox_blur", "0", true, false, "Chatbox background is blurred")
local CHATBOX_FADE = CreateClientConVar("cbox_chatbox_fade", "1", true, false, "Chatbox fades in and out")
local CHATBOX_MODE1 = CreateClientConVar("cbox_chatbox_mode1", "say", true, false, "Default mode when pressing messagemode")
local CHATBOX_MODE2 = CreateClientConVar("cbox_chatbox_mode2", "say_team", true, false, "Default mode when pressing messagemode2")
local INPUT_TEXT_COLOR = Color(221, 221, 221)
local INPUT_HIGHLIGHT_COLOR = Color(192, 28, 0, 140)
---@param height number
---@return number
local function _ScreenScaleH(height)
return height * (ScrH() / 480)
end
local ScreenScaleH = ScreenScaleH or _ScreenScaleH
---@return number x
---@return number y
---@return number w
---@return number h
local function GetDefaultBounds()
return ScreenScaleH(10), ScreenScaleH(275), ScreenScaleH(320), ScreenScaleH(120)
end
---Add a new chatbox send mode
---@param key string Internal unique name
---@param name string Display name
---@param callback fun(text) What to do when enter is pressed on this mode
function cbox.chatbox.AddMode(key, name, callback)
cbox.chatbox.modes[key] = {
name = name,
callback = callback,
}
end
local MATERIAL_BLUR = Material("pp/blurscreen")
local function add_rounded_poly(poly, x, y, rad, seg, offset)
offset = offset or 0
for i = 0, seg do
local r = math_rad(((i + offset) / seg) * -90)
poly[#poly + 1] = {
x = x + math_sin(r) * rad,
y = y + math_cos(r) * rad,
u = math_sin(r) / 2 + 0.5,
v = math_cos(r) / 2 + 0.5,
}
end
end
local function RoundedBoxPoly(w, h, rad, seg)
local poly = {}
local x, y = 0, 0
add_rounded_poly(poly, x + rad, y + rad, rad, seg, seg)
poly[#poly + 1] = {
x = x + (w - rad),
y = y,
u = 0.5,
v = 0.5,
}
add_rounded_poly(poly, x + (w - rad), y + rad, rad, seg, seg * 2)
poly[#poly + 1] = {
x = x + w,
y = y + (h - rad),
u = 0.5,
v = 0.5,
}
add_rounded_poly(poly, x + (w - rad), y + (h - rad), rad, seg, seg * 3)
poly[#poly + 1] = {
x = x + rad,
y = y + h,
u = 0.5,
v = 0.5,
}
add_rounded_poly(poly, x + rad, y + (h - rad), rad, seg)
return poly
end
local PREV_W, PREV_H, BACKGROUND_POLY
local function CreateChatbox()
if IsValid(cbox.chatbox.panels.frame) then
cbox.chatbox.panels.frame:Remove()
end
local frame = vgui.Create("DFrame", nil, "cbox.chatbox")
frame:SetCookieName("cbox_chatbox")
frame:SetDeleteOnClose(false)
frame:SetSizable(true)
frame:SetScreenLock(true)
frame:DockPadding(16, 20, 16, 12)
local dx, dy, dw, dh = GetDefaultBounds()
frame:SetMinWidth(math.min(256, dw))
frame:SetMinHeight(math.min(128, dh))
local x = frame:GetCookie("pos_x", dx)
local y = frame:GetCookie("pos_y", dy)
local w = frame:GetCookie("width", dw)
local h = frame:GetCookie("height", dh)
frame:SetPos(x, y)
frame:SetSize(w, h)
PREV_W = w
PREV_H = h
BACKGROUND_POLY = RoundedBoxPoly(w, h, 8, 24)
frame:SetVisible(false)
function frame:PerformLayout() end
frame.btnClose:Remove()
frame.btnMinim:Remove()
frame.btnMaxim:Remove()
frame.lblTitle:Remove()
function frame:CrossFade(anim, delta, out)
if anim.Finished then
if out then
self:Close()
self:SetVisible(false)
else
self:MakePopup()
end
end
if anim.Started then
if out then
self:SetAlpha(255)
else
self:SetAlpha(0)
end
end
self:SetAlpha(out and 255 * (1 - delta) or 255 * delta)
end
frame.animFade = Derma_Anim("Fade", frame, frame.CrossFade)
frame.oldThink = frame.Think
function frame:Think()
self:oldThink()
self.animFade:Run()
local w, h = self:GetSize()
if w ~= PREV_W or h ~= PREV_H then
PREV_W = w
PREV_H = h
BACKGROUND_POLY = RoundedBoxPoly(w, h, 8, 24)
end
end
function frame:OnMousePressed(button)
local screenX, screenY = self:LocalToScreen( 0, 0 )
if self.m_bSizable and gui.MouseX() > (screenX + self:GetWide() - 20) and gui.MouseY() > (screenY + self:GetTall() - 20) then
self.Sizing = {gui.MouseX() - self:GetWide(), gui.MouseY() - self:GetTall()}
self:MouseCapture(true)
return
end
if self:GetDraggable() and gui.MouseY() < (screenY + 16) then
if button == MOUSE_LEFT then
self.Dragging = {gui.MouseX() - self.x, gui.MouseY() - self.y}
self:MouseCapture(true)
elseif button == MOUSE_RIGHT then
local menu = DermaMenu()
local settings = menu:AddOption("Settings", function() end)
settings:SetIcon("icon16/cog.png")
local metrics = menu:AddOption("Reset Metrics", function()
RunConsoleCommand("cbox_chatbox_reset_metrics")
if IsValid(cbox.chatbox.panels.input) then
cbox.chatbox.panels.input:RequestFocus()
end
end)
metrics:SetIcon("icon16/arrow_out.png")
menu:AddSpacer()
local reload = menu:AddOption("Reload Chatbox", function()
RunConsoleCommand((input.IsKeyDown(KEY_LSHIFT) or input.IsKeyDown(KEY_RSHIFT)) and "_cbox_chatbox_fullreload" or "cbox_chatbox_reload")
end)
reload:SetIcon("icon16/arrow_refresh.png")
menu:AddSpacer()
local close = menu:AddOption("Close", function()
cbox.chatbox.Close()
end)
close:SetIcon("icon16/cross.png")
menu:Open()
end
return
end
end
function frame:Paint(w, h)
local alpha = CHATBOX_ALPHA:GetInt()
-- Reset everything to known good
render_SetStencilWriteMask(0xFF)
render_SetStencilTestMask(0xFF)
render_SetStencilReferenceValue(0)
render_SetStencilPassOperation(STENCIL_KEEP)
render_SetStencilZFailOperation(STENCIL_KEEP)
render_ClearStencil()
-- Enable stencils
render_SetStencilEnable(true)
-- Set everything up everything draws to the stencil buffer instead of the screen
render_SetStencilReferenceValue(1)
render_SetStencilCompareFunction(STENCIL_NEVER)
render_SetStencilFailOperation(STENCIL_REPLACE)
draw_NoTexture()
surface_SetDrawColor(255, 255, 255)
surface_DrawPoly(BACKGROUND_POLY)
-- Only draw things that are in the stencil buffer
render_SetStencilCompareFunction(STENCIL_EQUAL)
render_SetStencilFailOperation(STENCIL_KEEP)
if CHATBOX_BLUR:GetBool() and alpha ~= 255 then
local x, y = self:LocalToScreen(0, 0)
surface_SetMaterial(MATERIAL_BLUR)
surface_SetDrawColor(255, 255, 255, 255)
for i = 0.33, 1, 0.33 do
MATERIAL_BLUR:SetFloat("$blur", 5 * i)
MATERIAL_BLUR:Recompute()
render_UpdateScreenEffectTexture()
surface_DrawTexturedRect(x * -1, y * -1, ScrW(), ScrH())
end
end
local r, g, b = cbox.utils.ParseColorString(CHATBOX_COLOR:GetString())
surface_SetDrawColor(r, g, b, alpha)
surface_DrawRect(0, 0, w, h)
-- Let everything render normally again
render_SetStencilEnable(false)
end
function frame:OnClose()
local x, y = frame:GetPos()
local w, h = frame:GetSize()
frame:SetCookie("pos_x", x)
frame:SetCookie("pos_y", y)
frame:SetCookie("width", w)
frame:SetCookie("height", h)
end
function frame:OnKeyCodeTyped(key)
if key == KEY_ESCAPE then
cbox.chatbox.Close()
gui.HideGameUI()
end
end
cbox.chatbox.panels.frame = frame
local wrapper = vgui.Create("EditablePanel", frame)
wrapper:Dock(FILL)
-- TODO: custom richtext panel
local history = vgui.Create("RichText", wrapper)
history:Dock(FILL)
function history:Paint(w, h)
surface_SetDrawColor(0, 0, 0, 128)
surface_DrawRect(0, 0, w, h)
end
cbox.chatbox.panels.history = history
function history:PerformLayout()
-- TODO: configurable font
history:SetFontInternal("ChatFont")
end
local input_wrapper = vgui.Create("EditablePanel", wrapper)
input_wrapper:SetHeight(20)
input_wrapper:DockMargin(0, 8, 0, 0)
input_wrapper:Dock(BOTTOM)
local input = vgui.Create("DTextEntry", input_wrapper)
input:DockMargin(4, 0, 0, 0)
input:Dock(FILL)
input:SetFont("ChatFont")
cbox.chatbox.panels.input = input
function input:Paint(w, h)
surface_SetDrawColor(0, 0, 0, 128)
surface_DrawRect(0, 0, w, h)
self:DrawTextEntryText(INPUT_TEXT_COLOR, INPUT_HIGHLIGHT_COLOR, INPUT_TEXT_COLOR)
end
local mode_switch = vgui.Create("DButton", input_wrapper)
mode_switch:SetTextColor(INPUT_TEXT_COLOR)
mode_switch:Dock(LEFT)
cbox.chatbox.panels.mode_switch = mode_switch
function mode_switch:UpdateMode(mode)
local mode_data = cbox.chatbox.modes[mode]
if not mode_data then
self.current_mode = "say"
mode_data = cbox.chatbox.modes.say
end
self.current_mode = mode
self:SetText(mode_data.name .. ":")
end
function mode_switch:Paint(w, h)
surface_SetDrawColor(0, 0, 0, 128)
surface_DrawRect(0, 0, w, h)
end
function mode_switch:ApplySchemeSettings()
local ExtraInset = 8
self:SetTextInset(ExtraInset, 0)
local w, h = self:GetContentSize()
self:SetSize(w + 8, 20)
DLabel.ApplySchemeSettings(self)
end
mode_switch:SetFont("ChatFont")
mode_switch:UpdateMode("say")
function mode_switch:NextMode()
local keys = table.GetKeys(cbox.chatbox.modes)
table.sort(keys)
local _, next_mode = next(keys, table.KeyFromValue(keys, self.current_mode))
if not next_mode then next_mode = keys[1] end
self:UpdateMode(next_mode)
end
function mode_switch:DoClick()
self:NextMode()
end
function mode_switch:DoRightClick()
local menu = DermaMenu()
for mode, data in next, cbox.chatbox.modes do
menu:AddOption(data.name, function()
self:UpdateMode(mode)
end)
end
menu:Open()
end
function input:OnKeyCodeTyped(key)
--[[if key == KEY_ESCAPE then
cbox.chatbox.Close()
gui.HideGameUI()
else--]]
if key == KEY_BACKQUOTE then
gui.HideGameUI()
elseif key == KEY_ENTER then
local text = self:GetText():Trim()
if text ~= "" then
cbox.chatbox.modes[mode_switch.current_mode].callback(text)
end
cbox.chatbox.Close()
elseif key == KEY_TAB then
if #self:GetText() == 0 then
mode_switch:NextMode()
else
local tab_text = hook.Run("OnChatTab", self:GetText())
self:SetText(tab_text)
end
timer.Simple(0, function()
self:RequestFocus()
self:SetCaretPos(self:GetText():len())
end)
else
hook.Run("ChatTextChanged", self:GetText())
end
end
end
local function Init()
include("cbox/cl_modes.lua")
if not IsValid(cbox.chatbox.panels.frame) then
CreateChatbox()
end
hook.Add("PlayerBindPress", "cbox.chatbox", function(ply, bind, pressed)
local lply = LocalPlayer()
if IsValid(lply) and ply ~= lply then
cbox.utils.RealmError("Got mismatched player. How???", ply)
return
end
if bind ~= "messagemode" and bind ~= "messagemode2" then return end
if not pressed then return end
if input.IsKeyDown(KEY_LALT) or input.IsKeyDown(KEY_RALT) then return end
cbox.chatbox.Open(bind == "messagemode2")
return true
end)
hook.Add("Think", "cbox.chatbox", function()
if cbox.chatbox.IsOpen() and input.IsKeyDown(KEY_ESCAPE) then
cbox.chatbox.Close()
gui.HideGameUI()
end
end)
hook.Run("cbox.chatbox.Initialize")
end
---Opens the chatbox
---@param alt? boolean Was this request to open made by messagemode2 (team chat)
function cbox.chatbox.Open(alt)
alt = alt ~= nil and alt or false
local frame = cbox.chatbox.panels.frame
if not IsValid(frame) then
CreateChatbox()
end
frame:SetVisible(true)
if frame.animFade ~= nil and CHATBOX_FADE:GetBool() then
frame.animFade:Start(0.1, false)
else
frame:SetAlpha(255)
frame:MakePopup()
end
if not IsValid(cbox.chatbox.panels.input) or not IsValid(cbox.chatbox.panels.history) then
-- attempt to reinit
if IsValid(frame) then
frame:Remove()
end
Init()
end
if not IsValid(cbox.chatbox.panels.input) or not IsValid(cbox.chatbox.panels.history) then
cbox.utils.RealmError("Input or history aren't valid, chat failed to load, bailing!")
if IsValid(frame) then
frame:Remove()
end
hook.Remove("PlayerBindPress", "cbox.chatbox")
return
end
cbox.chatbox.panels.input:RequestFocus()
if IsValid(cbox.chatbox.panels.mode_switch) then
local mode1 = CHATBOX_MODE1:GetString()
local mode2 = CHATBOX_MODE2:GetString()
if not cbox.chatbox.modes[mode1] then
mode1 = "say"
end
if not cbox.chatbox.modes[mode2] then
mode2 = "say_team"
end
cbox.chatbox.panels.mode_switch:UpdateMode(alt and mode2 or mode1)
end
hook.Run("StartChat")
end
---Closes the chatbox
function cbox.chatbox.Close()
local frame = cbox.chatbox.panels.frame
if frame.animFade ~= nil and CHATBOX_FADE:GetBool() then
frame.animFade:Start(0.1, true)
else
frame:Close()
frame:SetVisible(false)
end
hook.Run("FinishChat")
cbox.chatbox.panels.input:SetText("")
hook.Run("ChatTextChanged", "")
end
---Gets if the chatbox is open
---@return boolean
function cbox.chatbox.IsOpen()
local frame = cbox.chatbox.panels.frame
return IsValid(frame) and frame:IsVisible() == true and frame:GetAlpha() == 255
end
hook.Add("Initialize", "cbox.chatbox", Init)
hook.Add("OnChatAddText", "cbox.chatbox.history", function(args)
local history = cbox.chatbox.panels.history
for _, arg in ipairs(args) do
if IsColor(arg) then
history:InsertColorChange(arg.r, arg.g, arg.b, 255)
elseif isstring(arg) then
history:AppendText(arg)
elseif isentity(arg) then
if IsValid(arg) and arg:IsPlayer() then
local col = hook.Run("GetTeamColor", arg)
history:InsertColorChange(col.r, col.g, col.b, 255)
history:AppendText(arg:Name())
else
history:InsertColorChange(160, 160, 160, 255)
history:AppendText("???")
end
end
end
history:AppendText("\n")
end)
concommand.Add("cbox_chatbox_reload", function()
CreateChatbox()
end, nil, "Reloads the chatbox")
concommand.Add("cbox_chatbox_reset_metrics", function()
if IsValid(cbox.chatbox.panels.frame) then
local frame = cbox.chatbox.panels.frame
local dx, dy, dw, dh = GetDefaultBounds()
frame:SetCookie("pos_x", dx)
frame:SetCookie("pos_y", dy)
frame:SetCookie("width", dw)
frame:SetCookie("height", dh)
frame:SetPos(dx, dy)
frame:SetSize(dw, dh)
end
end, nil, "Resets the metrics (size and position) of the chatbox")
concommand.Add("_cbox_chatbox_fullreload", function()
if IsValid(cbox.chatbox.panels.frame) then
cbox.chatbox.panels.frame:Remove()
end
Init()
end, nil, "Fully reinitializes the chatbox, use only in extreme breakage.")