Skip to content

VGui: Rework #1752

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 86 commits into
base: master
Choose a base branch
from
Draft

VGui: Rework #1752

wants to merge 86 commits into from

Conversation

TimGoll
Copy link
Member

@TimGoll TimGoll commented Jan 30, 2025

This PR has many things yet to do, but I wanted to open a draft so it is open to discussion. There are a few things I want to tackle in this PR. It will probably be rather large, but without any meaningful new features. It is mostly renamed, reordered and cleaned up.

Reviewing this is probably hard, therefore the following list explains the changes. As you can see in the diff, it consists mostly of restructuring, deletions and added documentation. Nothing new was added, only some small improvements.

1. Broken Inheritance

The first issue I want to address in this PR is the broken vgui inheritance in TTT2. There are quite a few places in the codebase with code duplications and also many unnecessary vgui elements.

After some investigation I learned that both PANEL and LABEL are defined in the GMod codebase, while every other element with a D prefix is defined in lua. We could use those elements already existing, but there are so many modifications by now, that is is useful to stick to our copied and modied files.
However, in our code base TTT2:DLabel and TTT2:DPanel have many duplications because they both inherit from their respective engine defined base. In the engine LABEL inherits from PANEL. The decision was made to replace both with a single TTT2:DPanel because the way I implemented it, they share most things anyway.

Another issue came up with what I call parallel paths. There are panels, textboxes and more that all have their own base defined in the engine. So everything implemented for the TTT2:DLabel had to be reimplemented for the TTT2:DTextEntry. The solution was an introduction of modules. New derma elements now have two ways of adding shared code:

  1. Inheritance: This is always preferred because building elements on top of each other is the normal way
  2. Modules: When starting from a different source file, modules are the way to go; a derma file can just define which modules it uses and the file loader handles everything automatically
DPANEL.derma = {
    className = "TTT2:DLabel",
    description = "The basic Label everything in TTT2 is based on",
    baseClassName = "Label",
}

DPANEL.implements = {
    -- core elements contain method chaining, paint hooks and ttt2 dlabel core features
    "core",
    -- box, as the name implies, contains the background box and its outline
    "box",
    -- text has everything related to text and description
    "text",
    -- icon handles the addition of icons
    "icon",
    -- text, description and icon can have different alignments, this is set in this module
    "alignment",
    -- dynamic things contain coloring, translating, rescaling to fit contents and automatic positioning of text/icons
    "dynamic",
    -- functions related to tooltips
    "tooltip",
}

Sidenote: Since all files are now loaded with a file loader (more on this in 9), the derma header is now more in line wiht the way other classes work in TTT2. There is no need anymore for a local PANEL variable and a derma.DefineControl at the end of the file.

2. Repetitive Code

The issue that triggered this rework is the following scenario. This is how it looks if you want to create a button:

local buttonReport = vgui.Create("DButtonTTT2", buttonArea)
buttonReport:SetText("search_call")
buttonReport:SetSize(self.sizes.widthButton, self.sizes.heightButton)
buttonReport:SetPos(0, self.sizes.padding + 1)
buttonReport:SetIcon(roles.DETECTIVE.iconMaterial, true, 16)
buttonReport.DoClick = function(btn)
    bodysearch.ClientReportsCorpse(data.rag)
end

When using method chaining this can be cleaned up a lot:

local buttonReport = vgui.Create("TTT2:DButton", buttonArea)
    :SetText("search_call")
    :SetSize(self.sizes.widthButton, self.sizes.heightButton)
    :SetPos(0, self.sizes.padding + 1)
    :SetIcon(roles.DETECTIVE.iconMaterial, true, 16)
    :On("Click", function(btn)
        bodysearch.ClientReportsCorpse(data.rag)
    end)

This makes it way more clean and less cluttered.

Note: I chose to do it this way because it adds new possibilities without breaking old code. The old UI code will work as before.

3. Events and Hook Overwrites

It is typical that our custom elements have something like this in their initialize function:

local oldClick = self.DoClick

self.DoClick = function(slf)
    sound.ConditionalPlay(soundClick, SOUND_TYPE_BUTTONS)

    oldClick(slf)
end

While this does work, it is rather tedious and prone to errors. This has to be done since there is no real inheritance in vgui elements that could use BaseClasses, at least not as far as I could tell. Additionally one might want to attach a function to an event after a element was already created. The developer here has to make sure that they don't overwrite an already existing hook.
The way the sound was added to buttons is a good example of the issue at hand.

The solution is to introduce a simple system, that mimics hooks. Elements can be registered with PANEL:On(name, function). That supports multiple events on a single event. So we can handle our button sounds internally in a clean way, while also exposing the event to the developer.

Similarly PANEL:After(delay, function) was introduced to provide a clean wrapper for timers.

Adding sound to a button would look like this:

local buttonReport = vgui.Create("TTT2:DButton", buttonArea)
    :On("Click", function(slf)
        sound.ConditionalPlay(soundClick, SOUND_TYPE_BUTTONS)
    end)

4. Attach

I also noticed that many of our custom elements are only copies of the panel with added setters and getters. We could probably delete quite a few of them, if we just reused existing ones. One solution I suggest is using PANEL:Attach(identifier, data). This is basically a fancy wrapper for PANEL.identifier = data to be able to attach any data while using method chaining.

5. PerformLayout and Caching

Currently, everything is done every frame. Translating, calculating sizes, changing colors. This is changed in the new basic panel by going back to GMods PANEL:PerformLayout system.

Internally, the base element now looks like this:

function PANEL:PerformLayout(w, h)
    self:TriggerOnWithBase("VSkinUpdate")
    self:TriggerOnWithBase("TranslationUpdate")
    self:TriggerOnWithBase("RebuildLayout", w, h)
end

This triggers three hooks on all inheriting panels. The first hook updates the cached colors, the second hook updates the cached translations, while the third rebuilds the layout. This also means that the panels will now invalidated on every vskin and language change.

This is done in three different hooks to keep the code tidy, while also making sure that subsequent overwrites do not break the core logic.

The paint hook was changed accordingly. Instead on relying on an individual draw function for every flavor of every element, mutual elements were selected:

function PANEL:Paint(w, h)
    if self:PaintBackground() then
        derma.SkinHook("Paint", "ColoredBoxTTT2", self, w, h)
    end

    if self:HasText() then
        derma.SkinHook("Paint", "LabelTTT2", self, w, h)
    end

    if self:HasIcon() then
        derma.SkinHook("Paint", "IconTTT2", self, w, h)
    end

    return true
end

This way many code duplications could be removed, while also vastly improving the rendering performance.

An element with many special features now looks like this:

local plyKarmaBox = vgui.Create("TTT2:DPanel", buttonArea)
    :SetSize(sizes.widthKarma, sizes.heightRow)
    :SetColor(colorTeamLight) -- sets the background color; if not set the vskin color is used
    :SetText(CLSCORE.eventsInfoKarma[ply.sid64] or 0)
    :SetFont("DermaTTT2CatHeader")
    :EnableCornerRadius(true) -- makes it a rounded box
    :EnableFlashColor(true) -- makes it pulsate to highlight this element
    :SetTooltipPanel(plyKarmaTooltipPanel)
    :SetTooltipFixedPosition(0, sizes.heightRow + 1)
    :SetTooltipFixedSize(widthKarmaTooltip, heightKarmaTooltip)

Note: This panel now also supports text align, even in combination with an icon. Use :SetHorizontalTextAlign() and :SetVerticalTextAlign for that. Moreover, :SetFitToContentX(true) and :SetFitToContentY(true) can be used to size this element to the size of the text and icons. This also works for buttons, making it really useful when supporting localization.

Since everything now properly inherits, the same settings are also available for buttons for example.

6. Documentation

We never added any documentation to vgui. The rewrite aims to add a lot of it - at least to the base classes.

7. Got rid of Master/Slave

I replaced master with primary and slave with follower. The old identifiers still work, so external addons won't be broken, but our sourcecode uses the new ones. I did not add a deprecation warning as I'm sure nobody will replace this, we should just promote the new naming going forward

There are now these functions:

  • PANEL:SetPrimary()
  • PANEL:SetFollower()

8. Restructured the whole file structure

Originally I did not want to do this, but the changes in 1 forced a restructure anyway, so I did it the proper way. The new system works as all new TTT2 systems based on folders. There is now lua/terrortown/derma/ with a few loaded folders that can be used by TTT2 (and addons) to register new derma files. This cleans up the whole source code for two reasons:

  1. no more manual file inclusions that had to be managed
  2. split those files into different folders so they could be located easier

9. Other Ideas

While researching this, I also found this library which seems quite nice: https://github.com/Threebow/better-derma-grid

@TimGoll TimGoll added the type/enhancement Enhancement or simple change to existing functionality label Jan 30, 2025
@nike4613
Copy link
Contributor

I've been on-and-off working on a framework to replace VGUI as much as possible myself. It has a lot of work to go (it can't even show anything yet!), but I have a vision for how I want it to be used:

{
    ty = "box",
    margin = 5,
    fill = true,
    {
        ty = "box",
        align = TOP,
        height = 20,
        fill = true,
        {
            ty = "text",
            translate = true,
            text = "some_text_key",
            align = { CENTER, CENTER },
        },
    },
    {
        vgui = "DPiPPanelTTT2",
        set = { Player = LocalPlayer() },
        init = function(pnl) end,
    },
}

Part of the goal is also to remove the pixel-sizing that is currently pervasive, provide automatic UI scaling, more aggressively cache data, etc.

@TimGoll
Copy link
Member Author

TimGoll commented Jan 30, 2025

Uh, this is also really neat! However, this won't stop my project. In fact, they don't interfere with each other. If you ever come around doing your idea, this rework will probably make it simpler!

@nike4613
Copy link
Contributor

Something in the PR description read as asking for ideas or some such, though I can't see it now. I figured it'd be worth mentioning here that I've been working on that as a result.

@TimGoll
Copy link
Member Author

TimGoll commented Jan 30, 2025

Something in the PR description read as asking for ideas or some such, though I can't see it now. I figured it'd be worth mentioning here that I've been working on that as a result.

Yes, I asked for feedback. However, this PR is ambitious as it is - I won't start implementing a new UI framework for now :D

@TimGoll
Copy link
Member Author

TimGoll commented Jan 30, 2025

Also, I want to mention something. The changes in this PR bring minimal issues with compatibility. Hooks, functions etc mostly stayed the same, I only introduced a new layer of interacting with them on top. While cleaning up the backend. If any addon actually uses our vgui, it should work without changes, maybe minimal changes

@TimGoll
Copy link
Member Author

TimGoll commented Feb 2, 2025

stylua sometimes handles these snippets in a bad way, not sure if there is anything that could be done about this. Most of the time it is as it is in the second example, but sometimes it is broken.

image

For vgui elements created with :Add this issue also exists. I worked around this by doing this:

image

@TimGoll
Copy link
Member Author

TimGoll commented Feb 25, 2025

image

It is working now!

@TimGoll TimGoll mentioned this pull request Mar 7, 2025
@TimGoll
Copy link
Member Author

TimGoll commented Mar 13, 2025

image

ever so slowly getting there with the F1 menu

@TimGoll
Copy link
Member Author

TimGoll commented Mar 14, 2025

Still a lot to do, but an end is in sight!

@TimGoll
Copy link
Member Author

TimGoll commented Mar 24, 2025

Note so self: This fix should probably be included in the search bar UI element: #1801

@hubertoschusch
Copy link

hubertoschusch commented Mar 31, 2025

Could you maybe also fix the dbinder? I think a setconvars is missing on the PANEL:makebinder method. When trying to set a key the convars does not get updated automatically

@TimGoll
Copy link
Member Author

TimGoll commented Mar 31, 2025

Could you maybe also fix the dbinder? I think a setconvars is missing on the PANEL:makebinder method. When trying to set a key the convars does not get updated automatically

Ah, that was never needed in core TTT2, hence I did not think of this. You want a bind stored in a convar? You are aware of TTT2's bind system, aren't you?

@hubertoschusch
Copy link

Ah, I guess I wasn't aware, my bad

@TimGoll
Copy link
Member Author

TimGoll commented Apr 1, 2025

@hubertoschusch There could still be a usecase for your request, feel free to explain what you want to do

@hubertoschusch
Copy link

I just wanted to add a keybind configuration option to an addon submenu in addons menu, and thought it would work like sliders, checkbox etc., but I guess I didn't see there is already a whole extra menu for that

@TimGoll
Copy link
Member Author

TimGoll commented Apr 1, 2025

I just wanted to add a keybind configuration option to an addon submenu in addons menu, and thought it would work like sliders, checkbox etc., but I guess I didn't see there is already a whole extra menu for that

if you register a key bind, it will be automatically added to the binds menu. You should probably use that to not confuse the users.

Here's an example that also adds the keyinfo on the bottom right: https://github.com/TTT-2/ttt2-heroes/blob/0d31e137cac4302737fe65a7c4000a7fc7d209d0/lua/terrortown/autorun/client/cl_crystal.lua#L55-L69

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type/enhancement Enhancement or simple change to existing functionality
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants