S&box Wiki

Revision Difference

Guide_to_Widgets#551935

<cat>Code.Editor</cat>⤶ <title>Guide to Widgets</title>⤶ ⤶ # A beginners guide to Widgets⤶ ⤶ Editor UIs are built entirely out of widgets. Widgets are different from [ASP.NET Razor](ui-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](Creating_Editor_Tools).⤶ ⤶ 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 three primary ways to do so:⤶ 1. Using the [Dock attribute](https://asset.party/api/Editor.DockAttribute) on a class that inherits [Widget](https://asset.party/api/Editor.Widget) at some level.⤶ 2. Implementing the [IAssetEditor interface](https://asset.party/api/Editor.IAssetEditor) and opening an asset with the extension specified on the class' [EditorForAssetType attribute](https://asset.party/api/Editor.EditorForAssetTypeAttribute).⤶ 3. Manually creating the widget with the Parent field set to `null`, typically in a static method with the [Menu attribute](https://asset.party/api/Editor.MenuAttribute).⤶ ⤶ ## Option 1 - Widget with Dock attribute⤶ ⤶ This will create widgets that behave like the Asset Browser, Projects list, Console feed, etc.⤶ ⤶ ```cs⤶ using Editor;⤶ ⤶ namespace MyToolProject; //never forget your namespace⤶ ⤶ [Dock("Editor", "Example Editor Dock", "local_fire_department")]⤶ public partial 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](https://asset.party/api/Editor.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`:⤶ <upload src="8f39a/8dbc7a4c911f556.png" size="15179" name="sbox-dev_gYojMiKVH2.png" />⤶ ⤶ For `Hammer`:⤶ <upload src="8f39a/8dbc7a4f928920d.png" size="34001" name="sbox-dev_glPOyG8aAl.png" />⤶ ⤶ ## Option 2 - Asset Editor⤶ ⤶ ```cs⤶ 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](Editor.EditorForAssetTypeAttribute) should have the extension of your [custom asset type](Custom_Asset_Types).⤶ ⤶ 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.⤶ ⤶ ```cs⤶ 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](https://asset.party/api/Editor.MenuAttribute) creates an entry in the Editor's tool bar.⤶ <upload src="8f39a/8dbc7aa50929051.png" size="13224" name="sbox-dev_drdnpJjEiz.png" />⤶ ⤶ # Example Window⤶ ⤶ Let's create a widget that tests our user's *Half-Life: 2* knowledge.⤶ ⤶ ```cs⤶ 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⤶ <upload src="8f39a/8dbc7ac1f241e31.png" size="3288" name="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.⤶ ⤶ ```cs⤶ 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.⤶ <upload src="8f39a/8dbc7acf0f7988f.png" size="8425" name="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.⤶ ⤶ ```cs⤶ Layout.AddStretchCell();⤶ ```⤶ ⤶ This will bunch the label and text input together⤶ <upload src="8f39a/8dbc7ad512e8ad5.png" size="8459" name="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⤶ <upload src="8f39a/8dbc7ad893bad5d.png" size="8458" name="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⤶ ```cs⤶ //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⤶ ```cs⤶ 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⤶ <upload src="8f39a/8dbc7b078b8e6fb.png" size="5886" name="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 ⤶ ⤶ ```cs⤶ Layout.AddSpacingCell(16);⤶ Layout.Add(new Button("Submit", this) { Clicked = ExamSubmitted });⤶ ```⤶ ⤶ and a method to grade the exam when it's clicked⤶ ```cs⤶ 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⤶ ```cs⤶ 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⤶ <upload src="8f39a/8dbc7b4cdbb6a51.png" size="14180" name="sbox-dev_DHmeBe6p4W.png" />⤶ ⤶ and click submit⤶ <upload src="8f39a/8dbc7b4e04bff89.png" size="4927" name="sbox-dev_pbrvvCMbFM.png" />⤶ ⤶ # Dock Windows⤶ ⤶ TODO: <page>Creating Dock Windows</page>