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);

Call Set after BuildLayout, not before. BuildLayout wipes the values you set earlier.

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 looks like web CSS but isn't. A few things that differ:

  • Place a box with horizontal-align and vertical-align, then nudge it with margins.
  • Colours can end in two extra digits for how see-through they are: #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);

While the cursor is free, the player can't move, aim or use abilities. DestroyLayout gives it back too.

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 have to reach players first. See Uploading Content.

Change a style later​

Use SetStyle with the name of the thing to change. The panel isn't rebuilt.

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 does the animating, so you only send this 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 hiccups, their panel comes back as it was when you last called BuildLayout, without the rows you added. Put 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 update can be dropped, but the next one replaces it anyway.

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 has to 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, and 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);