UI Overview
Socially Distant uses a custom widget-based UI system that prioritizes automatic layout over manual layout. The UI system is built specifically to meet the game's advanced needs. Nonetheless, it's fairly simple to work with. This page is an overview of the most common widget types, and how everything fits together in the game.
Visual Styles
In Socially Distant, and in Ritchie's Toolbox in general, widgets do not have a defined look by default. It is up to the game to provide a global visual style for all widgets. Socially Distant provides SociallyDistantVisualStyle
as the UI's global visual style.
The game must define a global visual style. However, the game can also define widget-specific visual styles. This is useful for creating a visual style for an in-game website that gives the website its own distinct look and feel from the rest of the in-game OS's programs. When setting a custom visual style on a widget, all children of that widget inherit the custom visual style.
*️⃣ Note
Because widgets don't have a defined look without a Visual Style to dictate it, widgets also don't have a default font for text rendering. It is up to the Visual Style to provide all fonts used in the UI, with the exception of custom fonts set on specific widgets.
The Visual Style system should never be worked around. If you're designing a custom widget, you should integrate it with the game's Visual Style as much as you can. This allows your custom widget to be reusable, themeable, and to reman consistent within the game's art style. Consider modifying SociallyDistantVisualStyle
to do the rendering for your custom widget if you are contributing to the base game.
Widgets
Socially Distant's user interface is made of widgets. Widgets are assembled like building blocks to make more complex widgets, which can be further assembled together into more complex widrgets. Most widgets in the game are just groups of smaller widgets arranged in a certain way to accomplish a reusable layout.
Custom Properties
Every widget has a set of properties shared across all widgets. Each specific widget also has a set of its own specific properties. For example, all widgets have a HorizontalAlignment
property while only TextWidget
s have a TextAlign
property. You can get far with this. However, there are cases where that's not enough. Sometimes, a parent widget needs to know something about each of its child widgets that isn't common across all widget types. Sometimes, the Visual Style may support drawing widgets in a certain way but there's no way for it to know whether you want it to do that. This is where Custom Properties come in. All widgets support them.
Custom Properties is a system that lets you assign custom data to a widget. This data can then be picked up by some other part of the game. For example, to make a widget draw with a red background:
var widget = new Box();
widget.SetCustomProperty(WidgetBackgrounds.Common);
widget.SetCustomProperty(CommonColor.Red);
The above code tells SociallyDistantVisualStyle to use a common color for the widget's background, and that the color should be the game's red accent color.
There are many more uses of custom properties, as you discover more of the UI system.
Input Events
Unlike Windows Forms and WPF, and most other UI systems, not all widgets are aware of input events such as mouse movement or key presses. Widgets only listen to the input events they need to, and only fire events that they want to. For example, an input field does not fire a KeyChar
event - rather it tells you when the input field's value has been changed or submitted.
If you need a widget to respond to an input event, you must wrap it in a parent widget that listens for the given input event. This means either using an existing widget, like Button
, that handles the inputs you need. Otherwise, you need to create a custom widget.
When creating a custom widget, you inform the UI system of the input events you care about by implementing them as interfaces. The following interfaces can be implemented by a Widget
class to receive input events.
Mouse
IMouseEnterHandler
: Receive an event when the mouse cursor enters the widget's content area.IMouseLeaveHandler
: Receive an event when the mouse cursor leaves the widget's content area.IMouseMoveHandler
: Receive an event every time the mouse cursor moves within the widgets' content area.IMouseDownHandler
: Receive an event when a mouse button is pressed on the widget.IMouseUpHandler
: Receive an event when a mouse button is released on the widget.IMouseClickHandler
: Receive an event when a mouse button is pressed and then released on the widget.IMouseScrollHandler
: Receive an event when the mouse wheel is scrolled on the widget.
Drag and Drop
IDragStartHandler
: Receive an event when the widget starts being dragged by the mouse.IDragHandler
: Receive an event while the mouse continues to drag the widget.IDragEndHandler
: Receive an event when the mouse stops dragging the widget.
Keyboard
IKeyDownHandler
: Receive an event when a key is pressed or held, if the widget is in focus.IKeyUpHandler
: Receive an event when a key is released, if the widget is in focus.IKeyCharHandler
: Receive an event when text is typed, if the widget is in focus.
Preview Keyboard
IPreviewKeyDownHandler
: Receive an event when a key is pressed or held, even if the widget isn't in focus.IPreviewKeyUpHandler
: Receive an event when a key is released, even if the widget isn't in focus.IPreviewKeyCharHandler
: Receive an event when text is typed, even if the widget isn't in focus.
Keyboard Focus
IGainFocusHandler
: Receive an event when the widget, or one of its children, gains keyboard focus.ILoseFocusHandler
: Receive an event when the widget, or one of its children, loses keyboard focus.
Special
IUpdateHandler
: Receive an event every frame.
When a widget receives any of the above events, the event is bubbled to the first parent who listens to the same event type. This bubbling repeats until the UI system reaches a widget with no parent. A widget can prevent the event from propagating to its parent by calling e.Handle()
on the event.
A widget can also request keyboard focus in response to an event by calling e.RequestFocus()
. For example, the InputField
widget requests focus when you click on it. Requesting focus also prevents the event from propagating to a parent.
Types of widgets
Base widgets
Base widgets are widgets you can't directly use as UI elements. Rather, they serve as building blocks for actual UI elements, providing their common base functionality. You can write your own, but there are three types of base widgets that serve most needs.
Widget
This is the base class of all widgets, including other base widgets. It provides the default layout and rendering behaviour for all widgets, and several properties that all widgets must have.
MinimumSize
: The minimum size, in pixels, that a widget must take within its container.MaximumSize
: The maximum size, in pixels, that a widget is allowed to take within its container.Parent?
: A reference to the widget's parent widget. Can be null.RenderEffect?
: A reference to an object implementingIWidgetEffect
that can be used to apply visual effects to a widget's geometry. Can be null.VisualStyle?
: A reference to an object implementingIVisualStyle
that can be set to override the game's global visual style. Can be null and should be rarely used.HorizontalAlignment
: Defines the widget's horizontal alignment (left, center, right, or stretch) within the space given to it by its parent widget. Default isStretch
.VerticalAlignment
: Defines the widget's vertical alignment (top, middle, right, stretch) within the space given to it by its parent widget. Default isStretch
.Padding
: An amount, in pixels, for each cardinal direction, of space between a given edge of the widget and the corresponding edge of the space given to the widget by its parent.Margin
: An amount, in pixels, for each cardinal direction, of space between a given edge of the widget and the corresponding edge of the widget's content/children.RenderOpacity
: The visual translucency, between0
and1
, of the widget and its children. Default is1
.ContentArea
: The calculated position and size of the widget, expressed as a layout rectangle, as of the last layout update.Enabled
: A value indicating whether the widget is enabled or disabled. If disabled, the widget appears visually grayed out and doesn't receive any input events. Default is enabled.Visibility
: A value defining the widget's visibility (visible, hiddem, or collapsed). If visible, the widget contributes to layout and renders. If hidden, the widget contributes to layout but doesn't render. If collapsed, the widget doesn't render or contribute to layout. Default is visible.
ContentWidget
A ContentWidget
is just a Widget
that can contain a single child. The most widely-used examples are Box
, Button
, and InfoBox
. Although users of a ContentWidget
can only assign one child to the widget, the widget itself may add multiple children. This is how InfoBox
is able to have a decorative colored strip as well as a title.
The only special property of ContentWidget
is Content
, which is a reference to a widget. Setting Content
to another widget will assign that widget as the child of the ContentWidget
.
ContainerWidget
A ContainerWidget
is a Widget
that can contain multiple child widgets. The most common examples are FlexPanel
, StackPanel
, ScrollView
, WrapPanel
and OverlayPanel
. Like a ContentWidget
, a ContainerWidget
may contain more children that cannot be directly accessed (such as the "new tab" button on window tab lists).
The only special property of a ContainerWidget
is its ChildWidgets
collection. Use this to add, remove, and otherwise access the widget's children.
UI Elements
These widgets are all building blocks for more complex user interfaces in the game.
TextWidget
TextWidget
is an extremely-common, and extremely versatile text renderer for the entire UI system. It ain't your grandma's text label, as it supports rich text markup and even images.
The most important properties of a TextWidget
are:
Text
: The text displayed in the widget.TextAlign
: The horizontal alignment of the text (left, center, right) within the widget's content area. Defaults to left.WordWrapped
: Whether the text is word-wrapped within the widget's content area. Defaults tofalse
for performance reasons.UseMarkup
: Whether the widget uses rich text markup. Defaults tofalse
for performance reasons.ShowMarkup
: WhenUseMarkup
is turned on, determines whether the raw markup is displayed. Defaults tofalse
, and should generally only be used when writing input fields.Color?
: The color (or, when in markup mode, default color) of text. Can be null. If set to null, the color comes from the widget's visual style. Defaults to null.FontSize?
: The font size (or, when in markup mode, default font size), in pixels, of the text. Can be null. If null, the value comes from the visual style. Defaulrs to null.FontWeight?
: Determines the weight (boldness) of the text. If in markup mode, the<b>
tag can override this toFontWeight.Bold
. Defaults toFontWeight.Normal
.Font?
: The font style of the text. Can be null. If null, the font style comes from the visual style. Defaults to null.
InputField
This is a customizable text entry field using TextWidget
for its display. You can control most of the same properties you can with TextWidget
itself. You can also control how the Enter Key and mouse behave in the input field. These are the most important properties of the input field:
Value
: The current value of the input field.Placeholder
: Text to display in the input field when it's empty.Multiline
: Whether the text can occupy multiple lines. Defaults to false.WordWrapped
: Whether the input field wraps its display horizontally or scrolls it. Defaults to false
Toggle
Toggles are basic on/off switches. They can be displayed as checkboxes or left/right toggle switches (this is purely cosmetic, they function identically).
UseSwitchVariant
: Whether the toggle is a checkbox or a switch.ToggleValue
: The current toggle value, on or off.
Button
A clickable surface. The button doesn't visually alter itself or its children in any way, unless the visual style decides to render a button background (Socially Distant does not). The Button is a ContentWidget
, and is used to make any other widget clickable. This is useful for creating custom UI elements where you need a clickable area (such as the close button on a tab), but want to deal with visual interactions yourself.
StringDropdown
A simple selection dropdown that presents a list of strings to the user and allows them to select one. The code can be used as a reference implementation for other Dropdown
-based widgets, if you need a more-advanced item display than just text. However, in most cases, StringDropdown
is all you need.
Icon
A widget that can be used to display a Unicode text icon at any size, using the visual style's icon font. In Socially Distant, this is used to display Material Design icons.
Image
A widget used to display a picture or other texture.
Slider
A widget used to allow the user to input a ranged number value. The slider can be vertical or horizontal, and the mouse is used to drag the slider between its minimum and maximum values.
Layout Widgets
Layout Widgets are ContainerWidget
widgets that apply a specific layout algorithm to their children. When putting complex UIs together, layout widgets are the most useful tool you have.
StackPanel
StackPanel arranges its children in a stack, either vertically or horizontally. You can define the direction (vertical by default), and the spacing between each child (in pixels, 0 by default). StackPanel is the most common layout widget used in the game.
WrapPanel
WrapPanel behaves similarly to StackPanel. The main difference is its children wrap to a new line when they can no longer fit on the current line. For this reason, you can set both the horizontal and vertical spacing between children.
If the wrap panel's direction is vertical, children are arranged top-to-bottom and wrap at the bottom edge. When in horizontal mode, children are arranged left-to-right and wrap at the right edge.
Wrap Panels are used for window tab lists and file grids.
FlexPanel
FlexPanel is a basic implementation of the flexbox algorithm. Children can either be auto-sized or sized proportionally within the flex panel. Other than that, it has the same properties as StackPanel
.
To change the sizing behaviour of a FlexPanel's child, you use the FlexPanelProperties
custom property. For example, to make a child fill 100% of the flex panel's remaining space:
var flex = new FlexPanel();
var widget = new Box();
flex.ChildWidgets.Add(widget);
var flexSettings = widget.GetCustomProperties<FlexPanelProperties>();
flexSettings.Mode = SizeMode.Proportional;
flexSettings.Percentage = 1f;
FlexPanels are used by the game's Status Bar, Dock, window title areas, and to arrange almost all of the game's top-level layout.
ScrollView
ScrollView behaves identically to a vertical StackPanel, but scrolls its children vertically. It can also render a scrollbar if its children don't fit within the ScrollView's content area. The ScrollView shrinks to fit its children, and expands until its children can't fit within the ScrollView's parent.
⚠️ Warning
Adding a ScrollView as a child of a FlexPanel can cause some NASTY layout bugs if you don't do it correctly. You must either set the ScrollView's flex mode to proportional, or make sure there's a mimimum width and maximum height set on the Scrollview or one of its parents.
OverlayPanel
OverlayPanel just allows you to overlay multiple children on top of each other using the default layout algorithm of all Widget
s. This is used by the game's modal dialogs, as well as by the cards in Info Panel to display a close button in their top-right corner.
Shell Widgets
Shell Widgets are custom widgets provided by Socially Distant (and not Ritchie's Toolbox) that implement game-specific needs.
SimpleTerminal
A port of the suckless simple terminal emulator (st
). It's used by the game's Terminal, and is by no means simple to use.
TextButton
A simple clickable button with centered text on it.
CompositeIconWidget
Used to draw a CompositeIcon
value, which can be either a texture or a Material icon. This is used for window icons, Dock icons, and file icons.
ToolbarIcon
A clickable CompositeIconWidget
. Used for the navigation buttons in File Manager and Web Browser's toolbar.
InfoBox
A box with a colored decorative strip, optional title, and content. Used by modal question/info dialogs, email messages, and Markdown blockquotes.
ListItem
A clickable and selectable widget. Used for various item lists in the game, like the player's email inbox, DM list, and the Category list in System Settings.
ListItemWithHeader
A ListItem
with a dark gray text label above it. Used in various sectioned lists like the Categories list in System Settings.
Writing a custom Widget
If you find yourself creating the same set of widgets over and over again, and laying them out the same way, then it's time for you to create a custom widget. So here's how. Let's create a simple widget that displays an icon and a text label next to it, like a FileGrid does.
First, we define a custom class inheriting Widget
.
public class FileIcon : Widget
{
}
Next, declare and instantiate all of the child widgets you'll need to create this file icon layout. We'll need an icon, a label, and a way to stack them next to each other.
public class MyWidget : Widget
{
private readonly StackPanel root = new();
private readonly CompositeIconWidget icon = new();
private readonly TextWidget label = new();
}
We must add the root of our custom widget as a child to the custom widget itself. If we were just instantiating MyWidget
, we wouldn't be able to add children to it because Widget.Children
is protected
to prevent you from adding children to widgets where that doesn't make sense. So, we must add our custom widget's children within the MyWidget
constructor.
public MyWidget()
{
Children.Add(root);
}
*️⃣ Code review note
It is a good practice to name the root widget of your custom widget
root
, and to make sure it is the first widget instantiated and added. This makes it clear that you are building a custom widget that just uses other widgets as building blocks. This also means you can change the type of layout widget used later.
Next, add the icon and label as child widgets to the root
widget.
root.ChildWidgets.Add(icon);
root.ChildWidgets.Add(label);
You can now instantiate and use MyWidget
inside other widgets! It's up to you to expose all of the properties, events, and public methods you need to control the custom widget. You can also set any visual and layout properties you need to during the MyWidget
constructor, for example:
public MyWidget()
{
root.Direction = Direction.Horizontal;
root.Spacing = 3;
root.Padding = 3;
label.VerticalAlignment = VerticalAlignment.Middle;
icon.VerticalAlignment = VerticalAlignment.Middle;
icon.IconSize = 16;
label.WordWrapped = true;
Children.Add(root);
root.ChildWidgets.Add(icon);
root.ChildWidgets.Add(label);
}
Further Reading
This should cover the basics of using the UI system, and should give plenty of common widgets to work with. However, you might want to learn more things you can do.
- Learn how to add programs and websites to Socially Distant
- Learn how to optimize UI for performance
- Learn how to create list views with ListAdapter
- Learn how to create cystom dropdowns