602 lines
16 KiB
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.")
|