Visual Styles
Visual Styles control how all widgets in the UI system look. They're responsible for determining both visual and layout attributes for each widget. While most game UI systems tend to allow direct control over how widgets look on an individual basis, Socially Distant (and AcidicGUI) does not.. This forces the user interface to maintain a consistent visual appearance across the entire game, to improve accessibility. All buttons should look like a button.
The visual style system is designed to allow custom widgets to take advantage of it. Therefore, the visual style system does take some learning.
The IVisualStyle
Interface
The IVisualStyle
interface defines methods that must be implemented by all visual styles in order for core widgets to function. AcidicGUI comes with a built-in FallbackVisualStyle
that gets used when no other option is available. SociallyDistant.Framework
provides SociallyDistantVisualStyle
, which implements the visual appearance of the in-game operating system.
The IVisual<TVisualProperties>
interface
This interface should be implemented by a widget that wants to be rendered using the visual style system. You use this interface to communicate important state about the widget being rendered. For example, when styling a progress bar, you will need to know its fill percentage. This is how the IVisual
interface is used to implement that.
public sealed class ProgressBar : Widget,
IVisual<ProgressBar.ProgressBarVisualProperties>
{
private ProgressBarVisualProperties visualProperties;
public ref readonly ProgressBarCisualProperties VisualProperties => ref visualProperties;
public float Value
{
get => visualProperties.FillPercentage;
set
{
visualProperties.FillPercentage = value;
InvalidateGeometry();
}
}
protected override void RebuildGeometry(GeometryHelper geometry)
{
geometry.DrawVisual<ProgressBar, ProgressBarVisualProperties>(this);
}
public struct ProgressBarVisualProperties
{
public float FillPercentage;
}
}
Note that ProgressBar
itself does not implement any actual rendering code. It just provides a means of retrieving its fill percentage (and any other visual state) through the VisualProperyies
property. Instead, we call the DrawVisual()
extension method of GeometryHelper
, which instructs the visual style system to paint the widget.
The IVisualRenderer<TVisualProperties>
Interface
This interface allows you to define how a widget should be rendered, given its visual properties. If implemented by a class implementing IVisualStyle
, the visual style system will be able to render any widgets implementing IVisual<TVisualProperties>
using this implementation.
You should delegate the implementation of the Draw
method to a nested type within your visual style implementation, like this:
public sealed class MyVisualStyle :
IVisualStyle,
IVisualRenderer<ProgressBar.ProgressBarVisualProperties>
{
private readonly ProgressBarStyle progressBarStyle = new();
`public void Draw(
Widget widget,
GeometryHelper geometry,
in LayoutRect contentRect,
ProgressBarVisualProperties properties
)
{
progressBarStyle.Draw(widget, geometry, in contentRect, properties);
}
public sealed class ProgressBarStyle :
IVisualRenderer<ProgressBar.ProgressBarVisualProperties>
{
public void Draw(
Widget widget,
GeometryHelper geometry,
in LayoutRect contentRect,
ProgressBarVisualProperties properties
)
{
var fillPercentage = properties.FillPercentage;
// Draw stuff!
}
}
}
Because C# does not have a way to name the Draw
method based on what type of visual properties being used, delegating the rendering implementation like this allows you to more easily navigate the visual style's code. It also allows you to more easily communicate that certain style attributes only affect progress bars, and therefore only need to cause progress bars to repaint when changed.
The IGetLayoutProperties<TLayoutProperties>
Interface
Sometimes, widgets need to use properties from a visual style to determine their layout. For example, a progress bar's height is controlled by the visual style system. That's the role of this interface. When implemented by a visual style, the visual style system can query layout properties from your own visual style dynamically. If you don't implement this interface for a given widget's layout properties, the widget itself will define default values that it will not allow you to change directly.
Here's how you can change the height of all progress bars.
public sealed class MyVisualStyle :
IVisualStyle,
IVisualRenderer<ProgressBar.ProgressBarVisualProperties>,
IGetLayoutProperties<ProgressBar.ProgressBarLayoutProperties>
{
private readonly ProgressBarStyle progressBarStyle = new();
public bool GetLayoutProperties(ref ProgressBar.ProgressBarLayoutProperties layoutProperties)
{
return progressBarStyle.GetLayoutProperties(ref layoutProperties);
}
`public void Draw(
Widget widget,
GeometryHelper geometry,
in LayoutRect contentRect,
ProgressBarVisualProperties properties
)
{
progressBarStyle.Draw(widget, geometry, in contentRect, properties);
}
public sealed class ProgressBarStyle :
IVisualRenderer<ProgressBar.ProgressBarVisualProperties>,
IGetLayoutProperties<ProgressBar.ProgressBarLayoutProperties>
{
public bool GetLayoutProperties(ref ProgressBar.ProgressBarLayoutProperties layoutProperties)
{
layoutProperties.ProgressBarHeight = 4;
return false;
}
public void Draw(
Widget widget,
GeometryHelper geometry,
in LayoutRect contentRect,
ProgressBarVisualProperties properties
)
{
var fillPercentage = properties.FillPercentage;
// Draw stuff!
}
}
}
Just like IVisualRenderer
, you should delegate your implementation of the GetLayoutProperties()
method.
Note that the GetLayoutProperties
method returns bool
. When a widget asks for layout properties, the value you return determines whether that widget should invalidate its layout. You should return true
if the layout properties are dirty, and false
otherwise. If they're static, always return false
as all widgets get dirtied automatically when a visual style is loaded.
The UserStyle
class in SociallyDistant.Framework
This is an extremely simple base class for widget styles in Socially Distant. It handles the dirtiness of layout properties for you, and provides access to accessibility and UI settings set by the player.
You can adjust the above code example with ProgressBarStyle
to make progress bar height dynamic.
public sealed class ProgressBarStyle :
UserStyle,
IVisualRenderer<ProgressBar.ProgressBarVisualProperties>,
IGetLayoutProperties<ProgressBar.ProgressBarLayoutProperties>
{
public int ProgressBarHeight
{
get => progressBarHeight;
set
{
if (progressBarHeight == value)
return;
progressBarHeight = value;
SetDirty();
}
}
public bool GetLayoutProperties(ref ProgressBar.ProgressBarLayoutProperties layoutProperties)
{
layoutProperties.ProgressBarHeight = progressBarHeight;
return GetDirtyState();
}
public void Draw(
Widget widget,
GeometryHelper geometry,
in LayoutRect contentRect,
ProgressBarVisualProperties properties
)
{
var fillPercentage = properties.FillPercentage;
// Draw stuff!
}
}
Accessing layout properties in a Widget
Any widget can access the layout properties of any type of widget, so long as the struct is public.
Here's how you can access the layout properties of a ProgressBar
.
public sealed class NotAProgressBar : Widget,
IUpdateLayoutProperties
{
private ProgressBar.ProgressBarLayoutProperties progressBarLayout;
public void UpdateLayoutProperties()
{
this.GetLayoutProperties(ref progressBarLayout);
}
}
The widget implements IUpdateLayoutProperties
, which is called every frame for every widget that implements it, before any actual layout updates occur. Calling this.GetLayoutProperties()
fills the given field with layout properties from the current visual style. If the visual style doesn't implement IGetLayoutProperties<TLayoutProperties>
for the given struct type, then it will be filled with default values instead.
By using GetLayoutProperties()
within the context of UpdateLayoutProperties()
, any changes reported by the visual style system will automatically invalidate the widget allowing you to immediately apply the layout properties.
Visual Style Overrides
Sometimes, you want a section of the UI to use a different visual style altogether. For example, in Socially Distant, a website may want to have a different look and feel than the in-game OS. This is where Visual Style Overrides come in.
Each widget has a VisualStyleOverride
property you can set to an instance of a class implementing IVisualStyle
. If VisualStyleOverride
is not null, then this visual style will be used by the widget instead of the current global style of the game. This property is also inherited by child widgets, meaning overriding the style of a widget will affect every widget below it in the hierarchy.