NanoUI: Difference between revisions

From VORE Station Wiki
Jump to navigation Jump to search
(Import NanoUI Readme.md from Virgo's Github - edits and fixes to follow.)
 
m (Fix up final)
 
(3 intermediate revisions by the same user not shown)
Line 1: Line 1:
<!-- TOC depth:6 withLinks:1 updateOnSave:0 orderedList:0 -->
==Introduction==
NanoUI is one of the three primary user interface libraries currently in use on Polaris (browse(), /datum/browser, NanoUI). It is the most complex of the three, but offers quite a few advantages, most notably in default features.


- [NanoUI](#nanoui)
NanoUI adds a <code>ui_interact()</code> proc to all atoms, which, ideally, should be called via <code>interact()</code>; However, the current standardized layout is <code>ui_interact()</code> being directly called from anywhere in the atom, generally <code>attack_hand()</code> or <code>attack_self()</code>. The <code>ui_interact()</code> proc should not contain anything but NanoUI data and code.
- [Introduction](#introduction)
- [Components](#components)
- [`ui_interact()`](#ui_interact)
- [`Topic()`](#topic)
- [Template (doT)](#template-dot)
- [Helpers](#helpers)
- [Link](#link)
- [displayBar](#displayBar)
- [doT](#dot)
- [Styling](#styling)
- [Contributing](#contributing)


<!-- /TOC -->
Here is a simple example from [https://github.com/ParadiseSS13/Paradise/blob/master/code/game/machinery/poolcontroller.dm poolcontroller.dm @ ParadiseSS13/Paradise.]
# NanoUI
    <code>/obj/machinery/poolcontroller/attack_hand(mob/user)
        ui_interact(user)
    /obj/machinery/poolcontroller/ui_interact(mob/user, ui_key = "main", var/datum/nanoui/ui = null, var/force_open = 1)
        var/data[0]
        data["currentTemp"] = temperature
        data["emagged"] = emagged
        data["TempColor"] = temperaturecolor
        ui = SSnano.try_update_ui(user, src, ui_key, ui, data, force_open)
        if(!ui)
            ui = new(user, src, ui_key, "poolcontroller.tmpl", "Pool Controller Interface", 520, 410)
            ui.set_initial_data(data)
            ui.open()</code>


## Introduction
==Components==


### Credit goes to Neersighted of /tg/station for the styling and large chunks of content of this README.
===<code>ui_interact()</code>===
The <code>ui_interact()</code> proc is used to open a NanoUI (or update it if already open). As NanoUI will call this proc to update your UI, you should include the data list within it. On /tg/station, this is handled via <code>get_ui_data()</code>, however, as it would take quite a long time to convert every single one of the 100~ UI's to using such a method, it is instead just directly created within <code>ui_interact()</code>.


NanoUI is one of the three primary user interface libraries currently in use
The parameters for <code>try_update_ui</code> and <code>/datum/nanoui/New()</code> are documented in the code [https://github.com/PolarisSS13/Polaris/tree/master/code/modules/nano here].
on Polaris (browse(), /datum/browser, NanoUI). It is the most complex of the three,
but offers quite a few advantages, most notably in default features.


NanoUI adds a `ui_interact()` proc to all atoms, which, ideally, should be called
For: <code>/datum/nanoui/New(nuser, nsrc_object, nui_key, ntemplate_filename, ntitle = 0, nwidth = 0, nheight = 0, var/atom/nref = null, var/datum/nanoui/master_ui = null, var/datum/topic_state/state = default_state)</code> Most of the parameters are fairly self explanatory.
via `interact()`; However, the current standardized layout is `ui_interact()` being
directly called from anywhere in the atom, generally `attack_hand()` or `attack_self()`.
The `ui_interact()` proc should not contain anything but NanoUI data and code.


Here is a simple example from
*<code>nuser</code> is the person who gets to see the UI window
[poolcontroller.dm @ ParadiseSS13/Paradise](https://github.com/ParadiseSS13/Paradise/blob/master/code/game/machinery/poolcontroller.dm).
*<code>nsrc_obj</code> is the thing you want to call Topic() on
*<code>nui_key</code> should almost always be <code>main</code>
*<code>ntemplate_filename</code> is the filename with <code>.tmpl</code> extension in /nano/templates/
*<code>ntitle</code> is what you want to show at the top of the UI window
*<code>nwidth</code> is the width of the new window
*<code>nheight</code> is the height of the new window
*<code>nref</code> is used for onclose()
*<code>master_ui</code> is used for UIs that have multiple children, see code for examples
*And finally, <code>state</code>.


```
The most interesting parameter here is <code>state</code>, which allows the object to choose the checks that allow the UI to be interacted with.
    /obj/machinery/poolcontroller/attack_hand(mob/user)
        ui_interact(user)


    /obj/machinery/poolcontroller/ui_interact(mob/user, ui_key = "main", var/datum/nanoui/ui = null, var/force_open = 1)
The default state (<code>default_state</code>) checks that the user is alive, conscious, and within a few tiles. It allows universal access to silicons. Other states exist, and may be more appropriate for different interfaces. For example, <code>physical_state</code> requires the user to be nearby, even if they are a silicon. <code>inventory_state</code> checks that the user has the object in their first-level (not container) inventory, this is suitable for devices such as radios; <code>admin_state</code> checks that the user is an admin (good for admin tools).
        var/data[0]
  <code>/obj/item/the/thing/ui_interact(mob/user, ui_key = "main", var/datum/nanoui/ui = null, force_open = 0)
        var/data[0]
        ui = SSnano.try_update_ui(user, src, ui_key, ui, data, force_open = force_open)
        if(!ui)
            ui = new(user, src, ui_key, "template_name_here.tmpl", title, width, height)
            ui.set_initial_data(data)
            ui.open()</code>


        data["currentTemp"] = temperature
===<code>Topic()</code>===
        data["emagged"] = emagged
<code>Topic()</code> handles input from the UI. Typically you will recieve some data from a button press, or pop up a input dialog to take a numerical value from the user. Sanity checking is useful here, as <code>Topic()</code> is trivial to spoof with arbitrary data.
        data["TempColor"] = temperaturecolor


        ui = SSnano.try_update_ui(user, src, ui_key, ui, data, force_open)
The <code>Topic()</code> interface is just the same as with more conventional, stringbuilder-based UIs, and this needs little explanation.
        if(!ui)
      <code>/obj/item/weapon/tank/Topic(href, href_list)
            ui = new(user, src, ui_key, "poolcontroller.tmpl", "Pool Controller Interface", 520, 410)
        if(..())
            ui.set_initial_data(data)
            return 1
            ui.open()
```
        if(href_list["dist_p"])
            if(href_list["dist_p"] == "custom")
                var/custom = input(usr, "What rate do you set the regulator to? The dial reads from 0 to [TANK_MAX_RELEASE_PRESSURE].") as null|num
                if(isnum(custom))
                    href_list["dist_p"] = custom
                    .()
            else if(href_list["dist_p"] == "reset")
                distribute_pressure = TANK_DEFAULT_RELEASE_PRESSURE
            else if(href_list["dist_p"] == "min")
                distribute_pressure = TANK_MIN_RELEASE_PRESSURE
            else if(href_list["dist_p"] == "max")
                distribute_pressure = TANK_MAX_RELEASE_PRESSURE
            else
                distribute_pressure = text2num(href_list["dist_p"])
            distribute_pressure = min(max(round(distribute_pressure), TANK_MIN_RELEASE_PRESSURE), TANK_MAX_RELEASE_PRESSURE)
        if(href_list["stat"])
            if(istype(loc,/mob/living/carbon))
                var/mob/living/carbon/location = loc
                if(location.internal == src)
                    location.internal = null
                    location.internals.icon_state = "internal0"
                    usr << "<nowiki><span class='notice'>You close the tank release valve.</span></nowiki>"
                    if(location.internals)
                        location.internals.icon_state = "internal0"
                else
                    if(location.wear_mask && (location.wear_mask.flags & MASKINTERNALS))
                        location.internal = src
                        usr << "<nowiki><span class='notice'>You open \the [src] valve.</span></nowiki>"
                        if(location.internals)
                            location.internals.icon_state = "internal1"
                    else
                        usr << "<nowiki><span class='warning'>You need something to connect to \the [src]!</span></nowiki>"</code>


===Template (doT)===
NanoUI templates are written in a customized version of [https://olado.github.io/doT/index.html doT], a Javascript template engine. Data is accessed from the <code>data</code> object, configuration (not used in pratice) from the <code>config</code> object, and template helpers are accessed from the <code>helper</code> object.


It is worth explaining that Polaris's version of doT uses custom syntax for the templates. The <code>?</code> operator is split into <code>if</code>, <code>else if parameter</code>, and <code>else</code>, instead of <code>?</code>, <code>?? paramater</code>, <code>??</code>, and the <code>=</code> operator is replaced with <code>:</code>. Refer to the chart below for a full comparison.


## Components
====Helpers====


### `ui_interact()`
===== link: =====
<code><nowiki>{{:helpers.link(text, icon, {'parameter': true}, status, class, id)}}</nowiki></code>


The `ui_interact()` proc is used to open a NanoUI (or update it if already open).
Used to create a link (button), which will pass its parameters to <code>Topic()</code>.
As NanoUI will call this proc to update your UI, you should include the data list
within it. On /tg/station, this is handled via `get_ui_data()`, however, as it would
take quite a long time to convert every single one of the 100~ UI's to using such a method,
it is instead just directly created within `ui_interact()`.


The parameters for `try_update_ui` and `/datum/nanoui/New()` are documented in
`<nowiki>{{:helpers.link(text, icon, {'parameter': true}, status, class, id)}}</nowiki>`
the code [here](https://github.com/PolarisSS13/Polaris/tree/master/code/modules/nano).
 
For:
`/datum/nanoui/New(nuser, nsrc_object, nui_key, ntemplate_filename, ntitle = 0, nwidth = 0, nheight = 0, var/atom/nref = null, var/datum/nanoui/master_ui = null, var/datum/topic_state/state = default_state)`
Most of the parameters are fairly self explanatory.
- `nuser` is the person who gets to see the UI window
- `nsrc_obj` is the thing you want to call Topic() on
- `nui_key` should almost always be `main`
- `ntemplate_filename` is the filename with `.tmpl` extension in /nano/templates/
- `ntitle` is what you want to show at the top of the UI window
- `nwidth` is the width of the new window
- `nheight` is the height of the new window
- `nref` is used for onclose()
- `master_ui` is used for UIs that have multiple children, see code for examples
- And finally, `state`.
 
The most interesting parameter here is `state`, which allows the object to choose the
checks that allow the UI to be interacted with.
 
The default state (`default_state`) checks that the user is alive, conscious,
and within a few tiles. It allows universal access to silicons. Other states
exist, and may be more appropriate for different interfaces. For example,
`physical_state` requires the user to be nearby, even if they are a silicon.
`inventory_state` checks that the user has the object in their first-level
(not container) inventory, this is suitable for devices such as radios;
`admin_state` checks that the user is an admin (good for admin tools).
 
```
    /obj/item/the/thing/ui_interact(mob/user, ui_key = "main", var/datum/nanoui/ui = null, force_open = 0)
        var/data[0]
 
        ui = SSnano.try_update_ui(user, src, ui_key, ui, data, force_open = force_open)
        if(!ui)
            ui = new(user, src, ui_key, "template_name_here.tmpl", title, width, height)
            ui.set_initial_data(data)
            ui.open()
```
 
### `Topic()`
 
`Topic()` handles input from the UI. Typically you will recieve some data from
a button press, or pop up a input dialog to take a numerical value from the
user. Sanity checking is useful here, as `Topic()` is trivial to spoof with
arbitrary data.
 
The `Topic()` interface is just the same as with more conventional,
stringbuilder-based UIs, and this needs little explanation.
 
```
    /obj/item/weapon/tank/Topic(href, href_list)
        if(..())
            return 1
 
        if(href_list["dist_p"])
            if(href_list["dist_p"] == "custom")
                var/custom = input(usr, "What rate do you set the regulator to? The dial reads from 0 to [TANK_MAX_RELEASE_PRESSURE].") as null|num
                if(isnum(custom))
                    href_list["dist_p"] = custom
                    .()
            else if(href_list["dist_p"] == "reset")
                distribute_pressure = TANK_DEFAULT_RELEASE_PRESSURE
            else if(href_list["dist_p"] == "min")
                distribute_pressure = TANK_MIN_RELEASE_PRESSURE
            else if(href_list["dist_p"] == "max")
                distribute_pressure = TANK_MAX_RELEASE_PRESSURE
            else
                distribute_pressure = text2num(href_list["dist_p"])
            distribute_pressure = min(max(round(distribute_pressure), TANK_MIN_RELEASE_PRESSURE), TANK_MAX_RELEASE_PRESSURE)
        if(href_list["stat"])
            if(istype(loc,/mob/living/carbon))
                var/mob/living/carbon/location = loc
                if(location.internal == src)
                    location.internal = null
                    location.internals.icon_state = "internal0"
                    usr << "<span class='notice'>You close the tank release valve.</span>"
                    if(location.internals)
                        location.internals.icon_state = "internal0"
                else
                    if(location.wear_mask && (location.wear_mask.flags & MASKINTERNALS))
                        location.internal = src
                        usr << "<span class='notice'>You open \the [src] valve.</span>"
                        if(location.internals)
                            location.internals.icon_state = "internal1"
                    else
                        usr << "<span class='warning'>You need something to connect to \the [src]!</span>"
```
 
### Template (doT)
 
NanoUI templates are written in a customized version of
[doT](https://olado.github.io/doT/index.html),
a Javascript template engine. Data is accessed from the `data` object,
configuration (not used in pratice) from the `config` object, and template
helpers are accessed from the `helper` object.
 
It is worth explaining that Polaris's version of doT uses custom syntax
for the templates. The `?` operator is split into `if`, `else if parameter`, and `else`,
instead of `?`, `?? paramater`, `??`, and the `=` operator is replaced with `:`. Refer
to the chart below for a full comparison.
 
#### Helpers
 
##### Link
 
`{{:helpers.link(text, icon, {'parameter': true}, status, class, id)}}`


Used to create a link (button), which will pass its parameters to `Topic()`.
Used to create a link (button), which will pass its parameters to `Topic()`.


* Text: The text content of the link/button
*Text: The text content of the link/button
* Icon: The icon shown to the left of the link (http://fontawesome.io/)
*Icon: The icon shown to the left of the link (http://fontawesome.io/)
* Parameters: The values to be passed to `Topic()`'s `href_list`.
*Parameters: The values to be passed to `Topic()`'s `href_list`.
* Status: `null` for clickable, a class for selected/unclickable.
*Status: `null` for clickable, a class for selected/unclickable.
* Class: Styling to apply to the link.
*Class: Styling to apply to the link.
* ID: Sets the element ID.
*ID: Sets the element ID.


Status and Class have almost the same effect. However, changing a link's status
Status and Class have almost the same effect. However, changing a link's status from <code>null</code> to something else makes it unclickable, while setting a custom Class does not.
from `null` to something else makes it unclickable, while setting a custom Class
does not.


Ternary operators are often used to avoid writing many `if` statements.
Ternary operators are often used to avoid writing many <code>if</code> statements. For example, depending on if a value in <code>data</code> is true or false we can set a button to clickable or selected:
For example, depending on if a value in `data` is true or false we can set a
button to clickable or selected:


`{{:helper.link('Close', 'lock', {'stat': 1}, data.valveOpen ? null : 'selected')}}`
<code><nowiki>{{:helper.link('Close', 'lock', {'stat': 1}, data.valveOpen ? null : 'selected')}}</nowiki></code>


Available classes/statuses are:
Available classes/statuses are:


* null (normal)
*null (normal)
* selected
*selected
* disabled
*disabled
* yellowButton
*yellowButton
* redButton
*redButton
* linkDanger
*linkDanger


##### displayBar
=====DisplayBar=====
<code><nowiki>{{:helpers.displayBar(value, min, max, class, text)}}</nowiki></code>


`{{:helpers.displayBar(value, min, max, class, text)}}`
Used to create a bar, to display a numerical value visually. Min and Max default to 0 and 100, but you can change them to avoid doing your own percent calculations.


Used to create a bar, to display a numerical value visually. Min and Max default
*Value: Defaults to a percentage but can be a straight number if Min/Max are set
to 0 and 100, but you can change them to avoid doing your own percent calculations.
*Min: The minimum value (left hand side) of the bar
 
*Max: The maximum value (right hand side) of the bar
* Value: Defaults to a percentage but can be a straight number if Min/Max are set
*Class: The color of the bar (null/normal, good, average, bad)
* Min: The minimum value (left hand side) of the bar
*Text: The text label for the data contained in the bar (often just number form)
* Max: The maximum value (right hand side) of the bar
* Class: The color of the bar (null/normal, good, average, bad)
* Text: The text label for the data contained in the bar (often just number form)


As with buttons, ternary operators are quite useful:
As with buttons, ternary operators are quite useful:


`{{:helper.bar(data.tankPressure, 0, 1013, (data.tankPressure > 200) ? 'good' : ((data.tankPressure > 100) ? 'average' : 'bad'))}}`
<code><nowiki>{{:helper.bar(data.tankPressure, 0, 1013, (data.tankPressure > 200) ? 'good' : ((data.tankPressure > 100) ? 'average' : 'bad'))}}</nowiki></code>


===doT===
doT is a simple template language, with control statements mixed in with regular HTML and interpolation expressions.


#### doT
However, Polaris uses a custom version with a different syntax. Refer to the chart below for the differences.
 
{| class="wikitable"
doT is a simple template language, with control statements mixed in with
|+
regular HTML and interpolation expressions.
!Operator
 
!doT
However, Polaris uses a custom version with a different syntax. Refer
!equiv
to the chart below for the differences.
|-
 
|Conditional
Operator   |  doT       |    equiv         |
|?
|-----------|------------|-------------------|
|if
|Conditional| ?         | if               |
|-
|           | ??         | else             |
|
|           | ?? (param) | else if(param)   |
|??
|Interpolate| =         | :                 |
|else
|^ + Encode | !         | >                 |
|-
|Evaluation | #         | #                 |
|
|Defines   | ## #       | ## #             |
|?? (param)
|Iteration | ~ (param) | for (param)       |
|else if(param)
 
|-
|interpolate
|=
|<blockquote>:</blockquote>
|-
|^ + Encode
|!
|>
|-
|Evaluation
|#
|#
|-
|Defines
|###
|###
|-
|Iteration
|~ (param)
|for (param)
|}
Here is a simple example from tanks, checking if a variable is true:
Here is a simple example from tanks, checking if a variable is true:
[[File:NANOUIEXAMPLE.png|none|frame|The doT tutorial is [https://olado.github.io/doT/tutorial.html#intro here.]]]


```
==== '''Print Tag''' ====
    {{if data.maskConnected}}
        <span>The regulator is connected to a mask.</span>
    {{else if}}
        <span>The regulator is not connected to a mask.</span>
    {{/if}}
```


The doT tutorial is [here](https://olado.github.io/doT/tutorial.html).
* The print tag outputs the given expression as text to the UI.


__Print Tag__
<code><nowiki>{{:data.variable}}</nowiki></code> <code><nowiki>{{:functioncall()}}</nowiki></code>
- The print tag outputs the given expression as text to the UI.
 
`{{:data.variable}}`
`{{:functioncall()}}`


(with escape):
(with escape):


`{{>expression }}`
<code><nowiki>{{>expression }}</nowiki></code>
 


__If Tag__
==== '''If Tag''' ====
- The if tag displays content conditionally based on the provided expression being true.
- When combined with the else tag the if tag can also show content if the provided expression is false.
- The else tag can optionally have an expression provided (e.g. "`{{else expression2}}`"), giving it "elseif" functionality.


`{{if expression}} <expression true content> {{/if}}`
* The if tag displays content conditionally based on the provided expression being true.
`{{if expression}} <expression true content> {{else}} <expression false content> {{/if}}`
* When combined with the else tag the if tag can also show content if the provided expression is false.
`{{if expression1}} <expression1 true content> {{else expression2}} <expression2 true content> {{/if}}`
* The else tag can optionally have an expression provided (e.g. "<code><nowiki>{{else expression2}}</nowiki></code>"), giving it "elseif" functionality.


__For Tag__
<code><nowiki>{{if expression}}</nowiki> <expression true content> <nowiki>{{/if}}</nowiki></code> <code><nowiki>{{if expression}}</nowiki> <expression true content> <nowiki>{{else}}</nowiki> <expression false content> <nowiki>{{/if}}</nowiki></code> <code><nowiki>{{if expression1}}</nowiki> <expression1 true content> <nowiki>{{else expression2}}</nowiki> <expression2 true content> <nowiki>{{/if}}</nowiki></code>
- Loop through entries in an array; it can be associative (with keys) or numerical indexed, but you have to use some special syntax for assocative lists.
- Each time the `for` tag iterates though the array it sets a variable (default "value") to the data of the current entry (another variable, default "index", contains the index). An example of this is using the print tag to print the contents (e.g. `{{:value.key1}}` and `{{:value.key2}}`).
- If combined with an `empty` tag the for tag can display content when the array is empty.


Indexed:
==== '''For Tag''' ====
`{{for array}} <list entry content> {{/for}}`
`{{for array}} <list entry content> {{empty}} <empty list content> {{/for}}`


Associative:
* Loop through entries in an array; it can be associative (with keys) or numerical indexed, but you have to use some special syntax for assocative lists.
`{{for object:key:index}} <key, value> {{/for}}`
* Each time the <code>for</code> tag iterates though the array it sets a variable (default "value") to the data of the current entry (another variable, default "index", contains the index). An example of this is using the print tag to print the contents (e.g. <code><nowiki>{{:value.key1}}</nowiki></code> and <code><nowiki>{{:value.key2}}</nowiki></code>).
* If combined with an <code>empty</code> tag the for tag can display content when the array is empty.


Indexed: <code><nowiki>{{for array}}</nowiki> <list entry content> <nowiki>{{/for}}</nowiki></code> <code><nowiki>{{for array}}</nowiki> <list entry content> <nowiki>{{empty}}</nowiki> <empty list content> <nowiki>{{/for}}</nowiki></code>


__Inclusion Tag__
Associative: <code><nowiki>{{for object:key:index}}</nowiki> <key, value> <nowiki>{{/for}}</nowiki></code>
- Include the contents of another template which has been added to the ui.
`{{#def.atmosphericScan}}`


- You first must have added a template to the ui server side in your DM code:
==== '''Inclusion Tag''' ====
`ui.add_template("atmosphericScan", "atmospheric_scan.tmpl")`


- Then you can reference it in the main template.  The tag will be replaced by the contents of the named template.  All tags in the named template are evaluated as normal.
* Include the contents of another template which has been added to the ui. <code><nowiki>{{#def.atmosphericScan}}</nowiki></code>
* You first must have added a template to the ui server side in your DM code: <code>ui.add_template("atmosphericScan", "atmospheric_scan.tmpl")</code>
* Then you can reference it in the main template.  The tag will be replaced by the contents of the named template.  All tags in the named template are evaluated as normal.


#### Styling
=== Styling ===
/tg/station has standardized styling, with specific article tags, and headers, and sections.
/tg/station has standardized styling, with specific article tags, and headers, and sections. However, as the templates are already horrifying unstandardized, Polaris does not have any particular styling standards.
However, as the templates are already horrifying unstandardized, Polaris does not have any
particular styling standards.


The only real requirement is that it, A. Looks alrightish, and B. Functions properly. Try
The only real requirement is that it, A. Looks alrightish, and B. Functions properly. Try to avoid snowflaking anything into the main CSS file, please.
to avoid snowflaking anything into the main CSS file, please.

Latest revision as of 15:40, 16 May 2020

Introduction

NanoUI is one of the three primary user interface libraries currently in use on Polaris (browse(), /datum/browser, NanoUI). It is the most complex of the three, but offers quite a few advantages, most notably in default features.

NanoUI adds a ui_interact() proc to all atoms, which, ideally, should be called via interact(); However, the current standardized layout is ui_interact() being directly called from anywhere in the atom, generally attack_hand() or attack_self(). The ui_interact() proc should not contain anything but NanoUI data and code.

Here is a simple example from poolcontroller.dm @ ParadiseSS13/Paradise.

    /obj/machinery/poolcontroller/attack_hand(mob/user)
        ui_interact(user)

    /obj/machinery/poolcontroller/ui_interact(mob/user, ui_key = "main", var/datum/nanoui/ui = null, var/force_open = 1)
        var/data[0]

        data["currentTemp"] = temperature
        data["emagged"] = emagged
        data["TempColor"] = temperaturecolor

        ui = SSnano.try_update_ui(user, src, ui_key, ui, data, force_open)
        if(!ui)
            ui = new(user, src, ui_key, "poolcontroller.tmpl", "Pool Controller Interface", 520, 410)
            ui.set_initial_data(data)
            ui.open()

Components

ui_interact()

The ui_interact() proc is used to open a NanoUI (or update it if already open). As NanoUI will call this proc to update your UI, you should include the data list within it. On /tg/station, this is handled via get_ui_data(), however, as it would take quite a long time to convert every single one of the 100~ UI's to using such a method, it is instead just directly created within ui_interact().

The parameters for try_update_ui and /datum/nanoui/New() are documented in the code here.

For: /datum/nanoui/New(nuser, nsrc_object, nui_key, ntemplate_filename, ntitle = 0, nwidth = 0, nheight = 0, var/atom/nref = null, var/datum/nanoui/master_ui = null, var/datum/topic_state/state = default_state) Most of the parameters are fairly self explanatory.

  • nuser is the person who gets to see the UI window
  • nsrc_obj is the thing you want to call Topic() on
  • nui_key should almost always be main
  • ntemplate_filename is the filename with .tmpl extension in /nano/templates/
  • ntitle is what you want to show at the top of the UI window
  • nwidth is the width of the new window
  • nheight is the height of the new window
  • nref is used for onclose()
  • master_ui is used for UIs that have multiple children, see code for examples
  • And finally, state.

The most interesting parameter here is state, which allows the object to choose the checks that allow the UI to be interacted with.

The default state (default_state) checks that the user is alive, conscious, and within a few tiles. It allows universal access to silicons. Other states exist, and may be more appropriate for different interfaces. For example, physical_state requires the user to be nearby, even if they are a silicon. inventory_state checks that the user has the object in their first-level (not container) inventory, this is suitable for devices such as radios; admin_state checks that the user is an admin (good for admin tools).

  /obj/item/the/thing/ui_interact(mob/user, ui_key = "main", var/datum/nanoui/ui = null, force_open = 0)
        var/data[0]

        ui = SSnano.try_update_ui(user, src, ui_key, ui, data, force_open = force_open)
        if(!ui)
            ui = new(user, src, ui_key, "template_name_here.tmpl", title, width, height)
            ui.set_initial_data(data)
            ui.open()

Topic()

Topic() handles input from the UI. Typically you will recieve some data from a button press, or pop up a input dialog to take a numerical value from the user. Sanity checking is useful here, as Topic() is trivial to spoof with arbitrary data.

The Topic() interface is just the same as with more conventional, stringbuilder-based UIs, and this needs little explanation.

     /obj/item/weapon/tank/Topic(href, href_list)
        if(..())
            return 1

        if(href_list["dist_p"])
            if(href_list["dist_p"] == "custom")
                var/custom = input(usr, "What rate do you set the regulator to? The dial reads from 0 to [TANK_MAX_RELEASE_PRESSURE].") as null|num
                if(isnum(custom))
                    href_list["dist_p"] = custom
                    .()
            else if(href_list["dist_p"] == "reset")
                distribute_pressure = TANK_DEFAULT_RELEASE_PRESSURE
            else if(href_list["dist_p"] == "min")
                distribute_pressure = TANK_MIN_RELEASE_PRESSURE
            else if(href_list["dist_p"] == "max")
                distribute_pressure = TANK_MAX_RELEASE_PRESSURE
            else
                distribute_pressure = text2num(href_list["dist_p"])
            distribute_pressure = min(max(round(distribute_pressure), TANK_MIN_RELEASE_PRESSURE), TANK_MAX_RELEASE_PRESSURE)
        if(href_list["stat"])
            if(istype(loc,/mob/living/carbon))
                var/mob/living/carbon/location = loc
                if(location.internal == src)
                    location.internal = null
                    location.internals.icon_state = "internal0"
                    usr << "<span class='notice'>You close the tank release valve.</span>"
                    if(location.internals)
                        location.internals.icon_state = "internal0"
                else
                    if(location.wear_mask && (location.wear_mask.flags & MASKINTERNALS))
                        location.internal = src
                        usr << "<span class='notice'>You open \the [src] valve.</span>"
                        if(location.internals)
                            location.internals.icon_state = "internal1"
                    else
                        usr << "<span class='warning'>You need something to connect to \the [src]!</span>"

Template (doT)

NanoUI templates are written in a customized version of doT, a Javascript template engine. Data is accessed from the data object, configuration (not used in pratice) from the config object, and template helpers are accessed from the helper object.

It is worth explaining that Polaris's version of doT uses custom syntax for the templates. The ? operator is split into if, else if parameter, and else, instead of ?, ?? paramater, ??, and the = operator is replaced with :. Refer to the chart below for a full comparison.

Helpers

link:

{{:helpers.link(text, icon, {'parameter': true}, status, class, id)}}

Used to create a link (button), which will pass its parameters to Topic().

`{{:helpers.link(text, icon, {'parameter': true}, status, class, id)}}`

Used to create a link (button), which will pass its parameters to `Topic()`.

  • Text: The text content of the link/button
  • Icon: The icon shown to the left of the link (http://fontawesome.io/)
  • Parameters: The values to be passed to `Topic()`'s `href_list`.
  • Status: `null` for clickable, a class for selected/unclickable.
  • Class: Styling to apply to the link.
  • ID: Sets the element ID.

Status and Class have almost the same effect. However, changing a link's status from null to something else makes it unclickable, while setting a custom Class does not.

Ternary operators are often used to avoid writing many if statements. For example, depending on if a value in data is true or false we can set a button to clickable or selected:

{{:helper.link('Close', 'lock', {'stat': 1}, data.valveOpen ? null : 'selected')}}

Available classes/statuses are:

  • null (normal)
  • selected
  • disabled
  • yellowButton
  • redButton
  • linkDanger
DisplayBar

{{:helpers.displayBar(value, min, max, class, text)}}

Used to create a bar, to display a numerical value visually. Min and Max default to 0 and 100, but you can change them to avoid doing your own percent calculations.

  • Value: Defaults to a percentage but can be a straight number if Min/Max are set
  • Min: The minimum value (left hand side) of the bar
  • Max: The maximum value (right hand side) of the bar
  • Class: The color of the bar (null/normal, good, average, bad)
  • Text: The text label for the data contained in the bar (often just number form)

As with buttons, ternary operators are quite useful:

{{:helper.bar(data.tankPressure, 0, 1013, (data.tankPressure > 200) ? 'good' : ((data.tankPressure > 100) ? 'average' : 'bad'))}}

doT

doT is a simple template language, with control statements mixed in with regular HTML and interpolation expressions.

However, Polaris uses a custom version with a different syntax. Refer to the chart below for the differences.

Operator doT equiv
Conditional ? if
?? else
?? (param) else if(param)
interpolate =

:

^ + Encode ! >
Evaluation # #
Defines ### ###
Iteration ~ (param) for (param)

Here is a simple example from tanks, checking if a variable is true:

The doT tutorial is here.

Print Tag

  • The print tag outputs the given expression as text to the UI.

{{:data.variable}} {{:functioncall()}}

(with escape):

{{>expression }}

If Tag

  • The if tag displays content conditionally based on the provided expression being true.
  • When combined with the else tag the if tag can also show content if the provided expression is false.
  • The else tag can optionally have an expression provided (e.g. "{{else expression2}}"), giving it "elseif" functionality.

{{if expression}} <expression true content> {{/if}} {{if expression}} <expression true content> {{else}} <expression false content> {{/if}} {{if expression1}} <expression1 true content> {{else expression2}} <expression2 true content> {{/if}}

For Tag

  • Loop through entries in an array; it can be associative (with keys) or numerical indexed, but you have to use some special syntax for assocative lists.
  • Each time the for tag iterates though the array it sets a variable (default "value") to the data of the current entry (another variable, default "index", contains the index). An example of this is using the print tag to print the contents (e.g. {{:value.key1}} and {{:value.key2}}).
  • If combined with an empty tag the for tag can display content when the array is empty.

Indexed: {{for array}} <list entry content> {{/for}} {{for array}} <list entry content> {{empty}} <empty list content> {{/for}}

Associative: {{for object:key:index}} <key, value> {{/for}}

Inclusion Tag

  • Include the contents of another template which has been added to the ui. {{#def.atmosphericScan}}
  • You first must have added a template to the ui server side in your DM code: ui.add_template("atmosphericScan", "atmospheric_scan.tmpl")
  • Then you can reference it in the main template. The tag will be replaced by the contents of the named template. All tags in the named template are evaluated as normal.

Styling

/tg/station has standardized styling, with specific article tags, and headers, and sections. However, as the templates are already horrifying unstandardized, Polaris does not have any particular styling standards.

The only real requirement is that it, A. Looks alrightish, and B. Functions properly. Try to avoid snowflaking anything into the main CSS file, please.