Guide to TGUI

From Paradise Station Wiki
Jump to navigation Jump to search
Github

TGUI is a user interface library that Paradise (and many other ss13) servers use to create modern, responsive user interfaces for players to interact with various in-game objects and mechanics. TGUI is JavaScript based and uses the InfernoJS and ReactJS frameworks. Unlike general code contributions, a contributor will need to go through extra steps to make TGUI changes such as installing package managers and performing extra compilations using yarn. TGUI can be hell to get working and compile the first time, however, it's worth it to keep trying until you can actually make a successful change, I promise!


Note: Please read through the README.md file in the TGUI directory on your copy of the codebase, it will provide all necessary instructions to setup TGUI.

Installation Process

Make sure to follow these steps chronologically and ensure you have proper dependencies installed

Prerequisites

You will need to download and install the following dependencies:

  1. Node v12.13+
  2. Yarn v1.19+

Downloading Yarn

TGUI Downloading Yarn.png

Yarn can be downloaded from their website, you can install via a package manager (like npm, chocolately, etc.) or download an installer directly. The installer is slightly hidden, underneath the Alternatives drop down.

Final Installation

If you are running MSys2, Git Bash, WSL, Linux or macOS please see the README.md for proper installation, for everyone else use this.

You will need to open a terminal, command window, powershell window, etc in the TGUI directory. If you are in vscode and have a terminal open to your codebase directory, you can use cd tgui to set the directory to the TGUI folder of your codebase. Now if your directory is correctly set to the base TGUI folder, you can now run yarn install

This should properly install everything, you can check if this worked by running yarn run build and it should spit out a console log that look something like this:


TGUI Build Success Log.png

If you are getting a bunch of errors, you should count your blessings and go ask somebody in #coding_chat to help you.

Your first TGUI Menu

In order to do basic TGUI you don't need to know much syntax, just basic familiarity with DM and markup language styles (as seen in HTML, XAML, or JSX). You should not attempt TGUI until you have at least a basic understanding of these (a 15-minute youtube tutorial on HTML should be more than enough help if you don't understand markup languages).

Creating a barebones TGUI Menu

For the sake of explaining this, let's imagine we're creating a computer for the chef to use that allows the chef to delete and browse existing recipes that they may use during their shift to cook food items. We'll call this /obj/machinery/computer/recipes and on this recipe computer we'll just have a list var storing our recipes in strings var/list/recipes = list() and set the name name = "Recipe Computer". In order to set up the most barebones TGUI menu possible, we must do three things:

  1. Wherever your copy of the code repository is stored, you will need to find this directory \GitHub\Paradise\tgui\packages\tgui\interfaces, this is where most (and for the sake of this tutorial, your) interface(s) will be stored.
  2. Create a javascript file for your interface in this directory, in this case, it will likely be something like RecipeComputer.js it will be blank but we will add code to it in a moment.
  3. Now in dm for your recipe computer you will need to override the ui_interact proc with this code.
/obj/machinery/computer/recipes/ui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = FALSE, datum/tgui/master_ui = null, datum/ui_state/state = GLOB.default_state)
	ui = SStgui.try_update_ui(user, src, ui_key, ui, force_open)
	if(!ui)
		ui = new(user, src, ui_key, "RecipeComputer", name, 400, 300, master_ui, state)
		ui.open()

What this code is doing is specifying the parameters of the TGUI window you're opening, creating a new window instance, and then opening it. For basic TGUI all you care about is where you're creating the new window using ui = new(). You want to set the string to be the name of your UI window (we will get to this in a bit), as well as the size of the window to be opened, in this case it is opening a 400px wide and 300px tall TGUI window, which for now should be enough.

The final DM code we will need to put in now is a way for our user to open the window using ui_interact(). For now we can use use attack_hand()

/obj/machinery/computer/recipes/attack_hand(mob/user)
	ui_interact(user)

Now we have done everything needed DM wise, next we will need to setup our javascript. Go ahead and open your new js file. In this file, we will do a few things:

  1. We will first import all the existing modules we will need to use. For now, we only need to import the window module from our layouts folder- this is so that we don't need to write the rendering code ourselves, instead, we can just use a prefabricated element.
  2. Now we will create a constant (or const) named after what we specified as our window name in the DM code earlier, in this case, it is just "RecipeComputer." What this allows Reactjs to do is to grab our (exported) const from this file and render it.
  3. We will have this const return our JSX (markup language) code. In this case, we're only going to define the Window and Window.Content elements as we want to just be able to open a window.
import { Window } from '../layouts';

export const RecipeComputer = (props, context) => {
  return (
    <Window>
      <Window.Content>

      </Window.Content>
    </Window>
  );
};
TGUI Empty Component Warning.png

At this point, you should be able to build/compile your dream maker code without errors. You may notice a warning from your code editor that one of your elements cannot have empty content or else it is just self-closing. Your code will run fine if you ignore this, but you can go ahead and put a basic element in there to shut it up. Let's go ahead and put a box element in there with the text "Hello World." You will need to import the box element from the components directory:

import { Box } from '../components';

and then insert it into your returned JSX

  return (
    <Window>
      <Window.Content>
        <Box>Hello World</Box>
      </Window.Content>
    </Window>
  );

Building your TGUI

You can edit the javascript files in your codebase all you want, however, you will see no changes in-game until you compile your TGUI. If you set your TGUI up using this tutorial previously you should have already done this.

TGUI Empty Example Window.png

You will need to open a terminal, command window, powershell window, etc in the TGUI directory. If have a terminal open to your codebase directory, you can use cd tgui to set the directory (or whatever your OS kernel uses) to the TGUI folder of your codebase. Now if your directory is correctly set to the base TGUI folder, you can now run yarn install

Your code should be built and your TGUI is now ready to run, go ahead and launch a server, spawn in your new console/computer, and interact with it.

Populating a TGUI Interface

It is unlikely that all you need for your computer is a single window that displays a string of text. Generally, you want your menu to dynamically display data to the user and react to user input. This section will cover designing and implementing a TGUI window that will display provided data, iterate through data lists, structure your data in a meaningful way, and read a few specific inputs from the user.

Basic UI Structure

For our Recipe Computer (if you're confused at what is being referred to here, please see the previous section), we need to display two different groups of UI elements, a group displaying the currently viewed recipe and a group displaying a list of recipes we may want to view. Thankfully TGUI has a built in <Section> for this exact purpose. Go ahead and import section from components, you will not need a separate import line for this as we're already importing a <Box> module from components.

import { Box, Section, } from '../components';

We can now put our two sections into our JSX, we will call them "Current Recipe" and "Recipe List." You can set the section title with the title attribute. All attributes have to come after the Element type declaration but still inside the angle brackets containing the Element declaration. Like so:

    <Window>
      <Window.Content>
        <Section label="Current Recipe">
          <Box>This is where our currently viewed recipe will go</Box>
        </Section>
        <Section label="Recipe List">
          <Box>This is where our list of recipes will go</Box>
        </Section>
      </Window.Content>
    </Window>

Advanced UI

Sloth construction.png
Sloth construction.png
This article or section is a Work in Progress.
Assigned to:
Sirryan2002
Please discuss changes with assigned users. If no one is assigned, or if the user is inactive, feel free to edit.

Modals

Modals are temporary popup menus in a TGUI window that contributors generally use to enable the view to see additional information or provide an input. In order to use them in a JavaScript file one will first need to import two packages import { ComplexModal, modalOpen } from './common/ComplexModal';

Implementation
In the constant where you first define the actual window, you'll need to include the <ComplexModal /> element directly after the <Window> declaration. This is mandatory in order for the modal to work. Here is an example usage of it:

export const ComputerUI = (props, context) => {
  const { act, data } = useBackend(context);

  const {
    var1,
    var2,
  } = data;

  return (
    <Window resizable>
      <ComplexModal />
      <Window.Content scrollable className="Layout__content--flexColumn">
        <ComputerNavigation />
        <ComputerPageContent />
      </Window.Content>
    </Window>
  );
};

Once the complex modal element is added, then once can add further elements into the JS file in order to prompt the game to actually open the modal. This can be done through a button onClick event for example. One will need to call modalOpen(context, 'modal_action_name') in the JS file, it will take context as the first paramater, and then this actions unique name which as been set as 'modal_action_name' here but it can be anything within reason. Here is an example where it is in a buttons onclick event.

<Button
  fluid
  textAlign="left"
  icon="pen"
  width="auto"
  content="Example Content"
  onClick={() => modalOpen(context, 'edit_title')} />

You can add these as many times as necessary! However, editing the JS file isn't enough to make these work or interact with the game.
DM Code Implementation
For this part, we'll operate under the assumption that you already have basic knowledge of /ui_act and /ui_data. Now we'll be introducing /ui_act_modal which is where we will do the bulk of our DM logic for modals. Go ahead and define that proc now /obj/machinery/computer/examplemachine/proc/ui_act_modal(action, list/params). Next we will add logic to our ui_act and ui_data procs.
For ui_act we will need to make sure to include all four of these parameters for our proc (action, list/params, datum/tgui/ui, datum/ui_state/state) this is more than most machines will use however we're special (and I promise these are used later). In addition to the new parameters, you will need to add an if statement calling ui_act_modal after the parent function call and before the "action" switch statement is performed. This is demonstrated below:

/obj/machinery/computer/examplemachine/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
	if(..())
		return

	if(ui_act_modal(action, params))
		return

	switch(action)

What we're doing here is we're checking for additional actions as part of the normal ui_act functionality. Opening a modal is the same thing as performing an action, we're just adding another proc to add ease of use and integrate special functionality already created in parent functions. We've now added a way for our DM code to actually know when the TGUI window wants to open a modal. This next part, I have no fucking idea what it does but it is critically important to making these pieces of shit work. We'll need to add one line to our ui_data proc:

data["modal"] = ui_modal_data(src)

Congratulations, we're now ready to add the final logic (and probably implement the code you've been wanting to add for the past few hours). This is where our ui_act_modal proc comes in. I'll do my best to break it down for a text/int input only (I'm sure more types of modals exist dont worry). First you will need to add some initial code, that is . = true and then define your id and arguments vars. Essentially what we're doing here is making sure our proc knows what modal window we're working with (by knowing its id) and then getting all those lovely arguments we snatched in our ui_act proc. Once this is done

/obj/machinery/computer/library/proc/ui_act_modal(action, list/params)
	. = TRUE
	var/id = params["id"] // The modal's ID
	var/list/arguments = istext(params["arguments"]) ? json_decode(params["arguments"]) : params["arguments"]
	switch(ui_modal_act(src, action, params))
		if(UI_MODAL_OPEN)
			switch(id)
				if("edit_title")
					ui_modal_input(src, id, "Please input the new title:", null, arguments, selected_book.title)
				if("edit_author")
					ui_modal_input(src, id, "Please input the new author:", null, arguments, selected_book.author)
				else
					return FALSE
		if(UI_MODAL_ANSWER)
			var/answer = params["answer"]
			switch(id)
				if("action_a")
					if(!length(answer) && length(answer) <= MAX_NAME_LEN)
						return
					selected_book.title = answer
				if("action_b")
					if(!length(answer))
						return
					selected_book.author = answer
				else
					return FALSE
		else
			return FALSE