Skip to main content

UI panels

Namespace: DeadworksManaged.Api.UI

A panel is a box on a player's screen that your plugin controls. It can hold text, images and buttons the player can click.

Requires the launcher

Players only see panels if they joined through the Deadworks launcher. Without it they see nothing.

Show a panel to a player​

Describe what is in the panel, then send it with BuildLayout. Give the panel a name, and use the same name every time you talk to it.

using DeadworksManaged.Api;
using DeadworksManaged.Api.UI;

public class RoundHud : DeadworksPluginBase
{
public override string Name => "Round HUD";

static UINode Layout() => UI.Vertical().Add(
UI.Label("timer", "0:00"),
UI.Label("score", "0 - 0"));

public override void OnClientFullConnect(ClientFullConnectEvent args)
{
if (args.Controller is { } controller)
UI.Panel("round").BuildLayout(controller.Recipients, Layout());
}
}

UI.Vertical() stacks what is inside it top to bottom. UI.Horizontal() lays it out left to right.

controller.Recipients sends to that one player. Use RecipientFilter.All to send to everyone.

Change the text in a panel​

Each label has a name. Call Set with that name and the new text.

UI.Panel("round").Set(controller.Recipients, "timer", "1:24");

To change several at once:

UI.Panel("round").Build()
.Set("timer", "1:24")
.Set("score", "3 - 2")
.SendTo(RecipientFilter.All);
warning

BuildLayout wipes values set before it. Call Set after BuildLayout, not before.

Remove a panel​

UI.Panel("round").DestroyLayout(controller.Recipients);

Style a panel​

Add styles with WithStyle. They are Panorama styles, the same ones the game's own UI uses.

static UINode Layout() => UI.Vertical()
.WithStyle("horizontal-align", "center")
.WithStyle("vertical-align", "top")
.WithStyle("margin-top", "24px")
.WithStyle("padding", "8px 14px")
.WithStyle("background-color", "#0a0c08e6")
.Add(
UI.Label("timer", "0:00")
.WithStyle("font-size", "20px")
.WithStyle("color", "#FFEFD7"),
UI.Label("score", "0 - 0")
.WithStyle("font-size", "14px")
.WithStyle("color", "#5FE69E"));

Panorama styles look like web CSS but aren't. The differences:

  • Place a box with horizontal-align and vertical-align, then nudge it with margins.
  • A color can end in two extra digits that set its opacity: #0a0c08e6.
  • To fill the space that is left, use width: fill-parent-flow(1.0).

Add a button​

Give the button an event name with OnClick, then say what happens with On.

static UINode Layout() => UI.Vertical().Add(
UI.Label("status", "Ready?"),
UI.Button("readyButton", "I'm ready").OnClick("ready"));

public override void OnLoad(bool isReload)
{
UI.Panel("lobby").On("ready", e =>
{
UI.Panel("lobby").Set(e.Caller.Recipients, "status", "Waiting for others...");
});
}

e.Caller is the player who clicked.

To tell buttons apart, send a value with the click and read it with e.ArgAt:

UI.Button("cell3", "").OnClick("move", "3");

UI.Panel("board").On("move", e =>
{
int cell = int.Parse(e.ArgAt(0));
});

Let a player use their mouse​

Players can't click a panel while they are aiming. Free their cursor with RequestCursor, and give it back with ReleaseCursor.

UI.Panel("lobby").RequestCursor(controller.Recipients);

UI.Panel("lobby").ReleaseCursor(controller.Recipients);
note

While the cursor is free, the player can't move, aim or use abilities. Call ReleaseCursor or DestroyLayout to give it back.

Style a button​

UI.Button("readyButton", "I'm ready")
.WithStyle("padding", "6px 12px")
.WithStyle("background-color", "#1a241f")
.WithHoverStyle("background-color", "#2a3a32") // while the mouse is over it
.WithTextStyle("color", "#FFEFD7") // the text inside it
.OnClick("ready");

Show an image​

UI.Image("logo", "file://{images}/myaddon/logo.vtex");

To show a hero's portrait, get the image from the hero:

var portrait = Heroes.Atlas.GetHeroData()?.IconImageSmall ?? "";

UI.Image("portrait", portrait);

Your own images must reach players first. See Uploading content.

Change a style later​

Use SetStyle with the name of the thing to change. SetStyle doesn't rebuild the panel.

UI.Panel("round").SetStyle(controller.Recipients, "score", "color", "#fb6c34");

Animate something​

Animate changes a style gradually. This shrinks a bar to nothing over 30 seconds:

UI.Panel("round").Animate(controller.Recipients, "bar", "width", "0%", "30s");

The player's game runs the animation, so send it once.

Add or remove a row​

Use AppendChild and RemoveChild to change part of a panel without rebuilding it.

// Add a label inside the thing named "list"
UI.Panel("scores").AppendChild(controller.Recipients, "list",
UI.Label("row5", "Abrams: 12"));

// Take it out again
UI.Panel("scores").RemoveChild(controller.Recipients, "row5");

If a player's connection drops briefly, their panel returns to its state at your last BuildLayout call. Rows you added are lost. Add them back in UI.ClientResync:

public override void OnLoad(bool isReload)
{
UI.ClientResync += slot =>
{
var controller = Players.FromSlot(slot);
if (controller != null)
RebuildScores(controller);
};
}

Update a panel many times a second​

Use SetUnreliable for values that change constantly. An unreliable update can be dropped. The next update replaces it.

public override void OnGameFrame(bool simulating, bool firstTick, bool lastTick)
{
UI.Panel("round").Build()
.SetUnreliable("timer", FormatTime())
.SetUnreliable("score", FormatScore())
.SendTo(RecipientFilter.All);
}

Send every value each time, even the ones that haven't changed.

Use your own layout file​

For more control, write the panel as a Panorama layout file and ship it to players in an addon.

myaddon/
panorama/
layout/killfeed.xml
scripts/killfeed.js
<root>
<scripts>
<include src="s2r://panorama/scripts/dw_addon.js" />
<include src="file://{resources}/scripts/killfeed.js" />
</scripts>

<Panel style="horizontal-align: right; vertical-align: top; flow-children: down;">
<Label id="headline" text="" />
<Label id="tally" text="" />
</Panel>
</root>

Load it with LoadXml, not BuildLayout:

UI.Panel("killfeed").LoadXml(controller.Recipients, "file://{resources}/layout/killfeed.xml");

After that it works like any other panel. Set changes the label with that name:

UI.Panel("killfeed").Set(RecipientFilter.All, "headline", "Abrams killed Haze");

The addon must reach players before they join. See Uploading content.

Run your own script in a panel​

Add a script to your addon to draw values yourself and to send clicks back.

DW.registerPanel({
init: function (panel) {
panel.onClick("clearButton", function () {
panel.send("clear");
});
},

render: function (panel) {
panel.text("tally", panel.int("total") + " kills this round");
}
});

render runs whenever your plugin calls Set. panel.int("total") reads the value you set. panel.send("clear") reaches the On("clear", ...) in your plugin.

UI.Panel("killfeed").Set(RecipientFilter.All, "total", 7);

UI.Panel("killfeed").On("clear", e =>
{
UI.Panel("killfeed").Clear(RecipientFilter.All);
});

Check whether a player has your addon​

A player without your addon can't load the layout. Listen for that, and show them a plain panel instead.

public override void OnLoad(bool isReload)
{
UI.AddonStatusChanged += (slot, panelId, state) =>
{
if (state != AddonState.Failed) return;

var controller = Players.FromSlot(slot);
if (controller != null)
UI.Panel(panelId).BuildLayout(controller.Recipients, Layout());
};
}

See your changes without restarting​

While you work on an addon's files, Reload sends each player their panel again.

UI.Panel("killfeed").Reload(RecipientFilter.All);

See also​