S&box Wiki

Guide to Widgets

A beginners guide to Widgets

Editor UIs are built entirely out of widgets. Widgets are different from ASP.NET Razor, which is used for the in-game UI. Widgets are available in the Editor namespace, thus you should only make them from a tool or editor project.

Widgets can be various elements or components, such as buttons, text boxes, trees, or images.

Children widgets are the smaller widgets that are contained within a larger widget, known as the parent widget. Imagine a parent widget as a container, like a box, and the children widgets are like smaller objects placed inside that box.

If a widget does not have a parent, it is a root widget. This widget will act as a window on the user's operating system.

Creating and opening a window

There are four primary ways to do so:

  1. Using the Dock attribute on a class that inherits Widget at some level.
  2. Implementing the IAssetEditor interface and opening an asset with the extension specified on the class' EditorForAssetType attribute.
  3. Manually creating the widget with the Parent field set to null, typically in a static method with the Menu attribute.
  4. Using the Tool attribute on a class that inherits Widget at some level.

Option 1 - Widget with Dock attribute

This will create widgets that behave like the Asset Browser, Projects list, Console feed, etc.

using Editor; namespace MyToolProject; //never forget your namespace [Dock("Editor", "Example Editor Dock", "local_fire_department")] public class DockAttrExample : Widget { public DockAttrExample(Widget parent) : base(parent, false) { //the constructor's parameters must have a widget and nothing else //otherwise the attribute will fail to find a constructor to use when creating the widget //create your child widgets } }

The Dock attribute has three parameters

  1. target
    This is the DockWindow registered with the attribute that the widget should show up in.
    Editor and Hammer are the only known options.
  2. name
    What should be displayed when listing available docks.
  3. icon (optional)
    Identifier of the icon to use.

The dock can then be opened from the associated window's list.

For Editor:

sbox-dev_gYojMiKVH2.png

For Hammer:

sbox-dev_glPOyG8aAl.png

Option 2 - Asset Editor

using Editor; namespace MyToolProject; //never forget your namespace [EditorForAssetType("8letters")] //cannot be more than 8 characters, will need to be a registered asset public class AssetEditorExample : Widget, IAssetEditor { //return false if you want to have a widget created for each asset //return true if you want only one widget to be made public bool CanOpenMultipleAssets => true; public AssetEditorExample(Widget parent, bool isDarkWindow = false) : base(parent, isDarkWindow) { //create your child widgets } public void AssetOpen(Asset asset) { //called every time an asset is opened in the asset browser (only for the type specified in EditorForAssetType) //do something with the asset, like call LoadResource //save your asset by calling SaveToDisk on the asset with the resource //if CanOpenMultipleAssets returns true, //you should refocus this widget Focus(); } }

The EditorForAssetType attribute should have the extension of your custom asset type.

When the an asset is double clicked in the Asset Browser (or the edit button on the inspector) the widget will be created and AssetOpen will be called with the asset.

You may need to load

Option 3 - Manually

Not recommended for most use cases, but still an option.

using Editor; namespace MyToolProject; //never forget your namespace public class WidgetExample : Widget { public WidgetExample(Widget parent, bool isDarkWindow = false) : base(parent, isDarkWindow) { //create children widgets //if you don't call this, you essentiall create a memory leak Show(); } //methods [Menu("Editor", "Example Code/Widget Example")] public static void OpenExample() { WidgetExample widget = new(null); } }

If the constructor doesn't call Show, then you will need to call it somewhere after constructing.
This only needs to be done for widgets that don't have a parent.

The static method with the Menu attribute creates an entry in the Editor's tool bar.

sbox-dev_drdnpJjEiz.png

Option 4 - Widget with Tool attribute

Does not have to be a DockWindow, just has to inherit Widget. This is typically used for larger tools with many features and widgets.

using Editor; namespace MyToolProject; //never forget your namespace [Tool("Full Scale Tool", "local_fire_department", "Example of a large tool with lots of widgets")] public class ToolAttrExample : DockWindow { public ToolAttrExample() : base() { //you must have an anonymous constructor //otherwise the attribute will fail to find a constructor to use when creating the widget //create your child widgets } }

Example Window

Let's create a widget that tests our user's Half-Life: 2 knowledge.

using Editor; using System; using System.Collections.Generic; namespace MyToolProject; //never forget your namespace, choose a more appropriate one than this though public class HalfLife2Exam : Widget { public HalfLife2Exam(Widget parent, bool isDarkWindow = false) : base(parent, isDarkWindow) { //set to false if you want the widget to be hidden instead //only do this if you will re-open the widget instead of creating a new one DeleteOnClose = true; //these sizes will get scaled by the operating system's window scaling MinimumSize = new(256, 256); Size = new(384, 512); //Name is used for the widget debugger, and DockWindow widgets //it is optional Name = "HL2Exam"; //also optional, but highly recommended if you are making your widget as a window WindowTitle = "Exam - Half-Life: 2"; //also optional, but looks much better than the operating system's default SetWindowIcon("edit_note"); //a Layout controls the positioning and sizing of widgets added to it //it's not necessary, but makes things a LOT easier Layout = Layout.Column(); //we will write more here //leave this at the bottom Show(); } [Menu("Editor", "Exams/Half-Life: 2")] public static void OpenExam() { HalfLife2Exam _ = new(null); } }

Using the Editor's tool bar we can open the window

sbox-dev_xWOFyc4Q9y.png

Let's start adding a few widgets to ask questions and take answers. First question we will ask, is for the name of the character you play as.

Layout.Add(new Label("1. What is the name of the character you play as?", this)); //single line text input GordonsName = Layout.Add(new LineEdit(this) { PlaceholderText = "Firstname Lastname" });

If we re-open our exam, we can now see the label and text input.

sbox-dev_YHrZshWzTI.png

It's nice to see things working, but the spacing is ugly. We can fix this by adding a stretch cell to occupy the extra space in one spot.

Layout.AddStretchCell();

This will bunch the label and text input together

sbox-dev_VIOWf8ouPo.png

Changing when the stretch cell is added to the layout, you can spread the widgets out differently. This is what happens when you add the stretch cell after the label, but before the text input

sbox-dev_jzu3bGhmc6.png

For now, let's make the stretch cell as the last member of the layout.

Next, let's ask what weapons the player can use in Half-Life 2 using a list of check boxes

//code ... public class HalfLife2Exam : Widget { private LineEdit GordonsName; private List<CheckBox> CorrectCheckBoxes = new(); private List<CheckBox> WrongCheckBoxes = new(); public HalfLife2Exam(Widget parent, bool isDarkWindow = false) : base(parent, isDarkWindow) { //code ... Layout.Add(new Label("2. Which of these weapons can players use in Half-Life: 2?", this)); //create the check boxes and put them into a list of correct and incorrect //we will evaluate these later CorrectCheckBoxes.Add(Layout.Add(new CheckBox("SMG", this))); CorrectCheckBoxes.Add(Layout.Add(new CheckBox("Crowbar", this))); WrongCheckBoxes.Add(Layout.Add(new CheckBox("AK-47", this))); WrongCheckBoxes.Add(Layout.Add(new CheckBox("M4A1", this))); CorrectCheckBoxes.Add(Layout.Add(new CheckBox("AR2", this))); //fill up space after all our widgets Layout.AddStretchCell(); //leave this at the bottom Show(); } //code ... }

Notice we store the check boxes into a list. We will need to access to the check boxes later when grading the test.

The check boxes work, but there should be spacing between question 1 and 2. We can solve this using the layout

Layout.AddSpacingCell(16);

Add this between the text input and the question 2 label, and a 16 pixel (scaled by operating system) gap will now appear between the two questions

sbox-dev_NazSyZz9hL.png

This test is already quite big with 2 whole questions, so let's wrap it up by adding a submit button

Layout.AddSpacingCell(16); Layout.Add(new Button("Submit", this) { Clicked = ExamSubmitted });

and a method to grade the exam when it's clicked

public void ExamSubmitted() { /* grade exam however you wamt */ float score = 75f; PopupWindow popup = new("Exam results", $"You scored {score}%"); popup.Show(); }

The final code should look something like

using Editor; using System; using System.Collections.Generic; namespace MyToolProject; //never forget your namespace, choose a more appropriate one than this though public class HalfLife2Exam : Widget { private LineEdit GordonsName; private List<CheckBox> CorrectCheckBoxes = new(); private List<CheckBox> WrongCheckBoxes = new(); public HalfLife2Exam(Widget parent, bool isDarkWindow = false) : base(parent, isDarkWindow) { //set to false if you want the widget to be hidden instead //only do this if you will re-open the widget instead of creating a new one DeleteOnClose = true; //these sizes will get scaled by the operating system's window scaling MinimumSize = new(256, 256); Size = new(384, 512); //Name is used for the widget debugger, and DockWindow widgets //it is optional Name = "HL2Exam"; //also optional, but highly recommended if you are making your widget as a window WindowTitle = "Exam - Half-Life: 2"; //also optional, but looks much better than the operating system's default SetWindowIcon("edit_note"); //a Layout controls the positioning and sizing of widgets added to it //it's not necessary, but makes things a LOT easier Layout = Layout.Column(); Layout.Add(new Label("1. What is the name of the character you play as?", this)); //single line text input GordonsName = Layout.Add(new LineEdit(this) { PlaceholderText = "Firstname Lastname" }); Layout.AddSpacingCell(16); Layout.Add(new Label("2. Which of these weapons can players use in Half-Life: 2?", this)); //create the check boxes and put them into a list of correct and incorrect //we will evaluate these later CorrectCheckBoxes.Add(Layout.Add(new CheckBox("SMG", this))); CorrectCheckBoxes.Add(Layout.Add(new CheckBox("Crowbar", this))); WrongCheckBoxes.Add(Layout.Add(new CheckBox("AK-47", this))); WrongCheckBoxes.Add(Layout.Add(new CheckBox("M4A1", this))); CorrectCheckBoxes.Add(Layout.Add(new CheckBox("AR2", this))); Layout.AddSpacingCell(16); Layout.Add(new Button("Submit", this) { Clicked = ExamSubmitted }); //fill up space after all our widgets Layout.AddStretchCell(); //leave this at the bottom Show(); } public void ExamSubmitted() { int points = 0; int maximumPoints = 1; //remove capitalization, and trim whitespace if (GordonsName.Value.ToLower().Trim() == "gordon freeman") points++; foreach (CheckBox checkbox in CorrectCheckBoxes) { maximumPoints++; if (checkbox.Value) points++; } foreach (CheckBox checkbox in WrongCheckBoxes) { maximumPoints++; if (!checkbox.Value) points++; } float score = MathF.Round(points / (float) maximumPoints * 100f); PopupWindow popup = new("Exam results", $"You scored {score}%"); popup.Show(); } [Menu("Editor", "Exams/Half-Life: 2")] public static void OpenExam() { HalfLife2Exam _ = new(null); } }

Fill out the exam

sbox-dev_DHmeBe6p4W.png

and click submit

sbox-dev_pbrvvCMbFM.png

Dock Windows

Read Widget Docking for guidance