Files
fulpstation/tgui-next/docs/tutorial-and-examples.md
Aleksej Komarov 5521c8ebee [READY] tgui-next: Initial PR, speedy new Air Alarm interface. (#46764)
* Initial tgui-next commit

* Improve dev server, slight changes to layout.

* Add more components, fix dragging, finalize scrubber list

* Complete an air alarm interface.

* Multiple improvements based on feedback

- LabeledList now has a "buttons" prop, where you can put buttons.
- Improved Box and Flex components
- Whole UI is now dimmable if non-interactive
- Basic documentation of components (Box and Flex so far).
- Icon and Box now accept a "color" prop.
- Routing improved in "Layout" component, you can now specify whether an interface is scrollable.

* Less harsh dimming

* Redux, Toasts

- Fixed inconsistent backend updates with Redux.
- Added Toasts which are currently unused, but are functional.

* acclimator + ai airlock ui

* Add a progress bar component, implement resizing

* Fix a zero in title bar

* Add a linter to keep shit tidy, fix danger level mapping, add some more docs

* better ai door interact ui

* final ai airlock interface

* Fix issues with code, enforce hard 120 line length cap, automerge binaries

* Implement hot module reloading

* Fix progress bar, add color support

* Fix ProgressBar baseline alignment issues

* Remove unwanted padding on the bottom of the LabeledList.

* Component improvements

- Fix baseline issues with Button and ProgressBar components
- Box how inherits props from Box
- Atomic colors and Button colors are now auto-generated, all range of colors supported

* Chem Dispenser UI, animated numbers, more style fixes

* Add an IE8 polyfill

* Intermediate state of fixing IE8

* Lots of shimming and general misery

* Fully working old TGUI chainloader for IE8, more pipeline improvements

* Support title-less Sections

* Delete Yarn, use Lerna for workspace management

* Improve maths and animated numbers

* Fix loss of focus for scrollable content, activate buttons with keyboard

* Attempt to bust the cache, grab focus on keyboard presses too

* Fix hot module replacement by manually pulling replaced modules.

* backend logging

 un-nuke line endings

changes without insane line endings

* helper procs + href support

* slight optimization

* compiles

* Redux, Hotkeys, Kitchen Sink (for UI testing), Tabs component

* Push logs to backend, small kitchen sink changes, tab fixes

* Update component reference in README

* Small fixes

* Next bunch of IE8 fixes, PureComponent optimization for Inferno

* Delete old tgui html

* Log the event of loading the old tgui

* Enable tgui logging on server by default

* Final solution

* Extract routes into a separate file, fix ChemDispenser bug

- Chem dispenser was needlessly disabling transfer amount buttons

* Disable baseline alignment for vertical tabs

* Fix tabs and box units

- Tab content was not taking full page width
- Box can now accepts strings as units, which will be treated as raw style values.

* Fix tgui on Terry

* Fix sending all logs with an "error" flag

* Some macro UI component changes and improvements

- Refer to README.md diff to see what's new

* Tooltip component

* Add support for themes, nanotrasen logo is back

* Clockwork theme hotfix

* Slight adjustment to logo positioning

* Actually proper solution for logo positioning

* Fix color maps

* tgui-next thermomachine interface

* tgui-next canister interface

* Add icon_class_name proc to asset cache

* Lots of stuff backported from actioninja's repo

* Cleanup oopsies

* Cargo UI adjustments

* Nuke lodash

* Minor fixes and cleanup

- Remove local Inferno package in favor of upstream Inferno
- Fix "initial" prop on AnimatedNumber
- Force fancy=0 for IE8
- Click-through dimmer

* Add a bat file for dummies

* podlauncher interface fix

* Update README, assert code dominance

* Clarify usage of event handlers in Inferno

* Document LabeledList.Divider

* Fukken grammar

* fixes cellular emporium readapt button not working

* fixes incorrect action on button in atmos control interface

* remove unneeded data from airlock electronics ui

* Set +x mode on bin/tgui

* Fix filename cases

- They were untracked by git on windows

* Ignore package lock, make batch more robust

- package-lock.json is very random and unreliable as fuck. Yarn was better.

* Build tgui-next in travis

* bruh

- fixes tgui error reporting

* logging cleanup + always log
2019-10-15 17:20:37 +02:00

7.7 KiB

TGUI Backend Documentation

Main concepts

Basic tgui backend code consists of defining a few procs. In these procs you will handle a request to open or update a UI (typically by updating a UI if it exists or setting up and opening it if it does not), a request for data, in which you build a list to be passed as JSON to the UI, and an action handler, which handles any user input.

  • The atom, which UI corresponds to in the game world, is in most cases known as the src_object.
  • Frontend data is built in ui_data proc, which munges whatever complex data your src_object has into a list.
  • The action/topic handler, ui_act, is what recieves input from the user and acts on it.
  • The request/update proc, ui_interact is where you open your UI and set options like title, size, autoupdate, theme, and more.
  • Finally, ui_state (set in ui_interact) dictates under what conditions a UI may be interacted with. This may be the standard checks that check if you are in range and conscious, or more.

Once backend is complete, you create an new interface component on the frontend, which will receive this JSON data and render it on screen.

States are easy to write and extend, and what make tgui interactions so powerful. Because states can be overridden from other procs, you can build powerful interactions for embedded objects or remote access.

Using It

Backend

Let's start with a very basic hello world.

/obj/machinery/my_machine/ui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = 0, datum/tgui/master_ui = null, datum/ui_state/state = default_state)
  ui = SStgui.try_update_ui(user, src, ui_key, ui, force_open)
  if(!ui)
    ui = new(user, src, ui_key, "my_machine", name, 300, 300, master_ui, state)
    ui.open()

This is the proc that defines our interface. There's a bit going on here, so let's break it down. First, we override the ui_interact proc on our object. This will be called by interact for you, which is in turn called by attack_hand (or attack_self for items). ui_interact is also called to update a UI (hence the try_update_ui), so we accept an existing UI to update. The state is a default argument so that a caller can overload it with named arguments (ui_interact(state = overloaded_state)) if needed.

Inside the if(!ui) block (which means we are creating a new UI), we choose our template, title, and size; we can also set various options like style (for themes), or autoupdate. These options will be elaborated on later (as will ui_states).

After ui_interact, we need to define ui_data. This just returns a list of data for our object to use. Let's imagine our object has a few vars:

/obj/machinery/my_machine/ui_data(mob/user)
  var/list/data = list()
  data["health"] = health
  data["color"] = color

  return data

The ui_data proc is what people often find the hardest about tgui, but its really quite simple! You just need to represent your object as numbers, strings, and lists, instead of atoms and datums.

Finally, the ui_act proc is called by the interface whenever the user used an input. The input's action and params are passed to the proc.

/obj/machinery/my_machine/ui_act(action, params)
  if(..())
    return
  switch(action)
    if("change_color")
      var/new_color = params["color"]
      if(!(color in allowed_coors))
        return
      color = new_color
      . = TRUE
  update_icon()

The ..() (parent call) is very important here, as it is how we check that the user is allowed to use this interface (to avoid so-called href exploits). It is also very important to clamp and sanitize all input here. Always assume the user is attempting to exploit the game.

Also note the use of . = TRUE (or FALSE), which is used to notify the UI that this input caused an update. This is especially important for UIs that do not auto-update, as otherwise the user will never see their change.

Frontend

Finally, you have to make a UI component. This is also a source of confusion for many new users. If you got some basic javascript and HTML knowledge, that should ease the learning process, although we recommend getting yourself introduced to React and JSX.

A component is not a regular HTML. A component is a pure function, which accepts a props object (it contains properties passed to a component), and outputs an HTML-like structure consisting of regular HTML elements and other UI components.

Interface component will always receive 1 prop which is called state. This object contains a few special values:

  • config is always the same and is part of core tgui (it will be explained later),
  • data is the data returned from ui_data
  • adata is the same, but with certain values (numbers at this time) interpolated in order to allow animation.
import { Section, LabeledList } from '../components';

const SampleInterface = props => {
  // Extract state from props
  const { state } = props;
  // Extract config and data from the state
  const { config, data } = state;
  // Extract window reference (will be used later for dispatching actions)
  const { ref } = config;
  // Return the Virtual DOM
  return (
    <Section title="Health status">
      <LabeledList>
        <LabeledList.Item label="Health">
          {data.health}
        </LabeledList.Item>
        <LabeledList.Item label="Color">
          {data.color}
        </LabeledList.Item>
      </LabeledList>
    </Section>
  );
};

This syntax can be very confusing at first, but it is very important to realize that this is just a natural extension of javascript. This syntax simply creates a Virtual DOM object, which you can treat as any other object in javascript. Here are some control flow examples:

Returning different elements based on a condition:

if (condition) {
  return <Foo />;
}
return <Bar />;

Conditionally rendering a element inside of another element:

<Box>
  {showProgress && (
    <ProgressBar value={progress} />
  )}
</Box>

Looping over the array to make element for each item:

<LabeledList>
  {items.map(item => (
    <LabeledList.Item key={item.id} label={item.label}>
      {item.content}
    </LabeledList.Item>
  ))}
</LabeledList>

Copypasta

We all do it, even the best of us. If you just want to make a tgui fast, here's what you need (note that you'll probably be forced to clean your shit up upon code review):

/obj/copypasta/ui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = 0, datum/tgui/master_ui = null, datum/ui_state/state = default_state) // Remember to use the appropriate state.
  ui = SStgui.try_update_ui(user, src, ui_key, ui, force_open)
  if(!ui)
    ui = new(user, src, ui_key, "copypasta", name, 300, 300, master_ui, state)
    ui.open()

/obj/copypasta/ui_data(mob/user)
  var/list/data = list()
  data["var"] = var

  return data

/obj/copypasta/ui_act(action, params)
  if(..())
    return
  switch(action)
    if("copypasta")
      var/newvar = params["var"]
      var = Clamp(newvar, min_val, max_val) // Just a demo of proper input sanitation.
      . = TRUE
  update_icon() // Not applicable to all objects.

And the template:

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

const SampleInterface = props => {
  // Extract state from props
  const { state } = props;
  // Extract config and data from the state
  const { config, data } = state;
  // Extract window reference (will be used later for dispatching actions)
  const { ref } = config;
  // Return the Virtual DOM
  return (
    <Section title="Section name">
      <LabeledList>
        <LabeledList.Item label="Variable">
          {data.var}
        </LabeledList.Item>
      </LabeledList>
    </Section>
  );
};