using System.Linq; using Content.Client.Stylesheets; using Robust.Client.AutoGenerated; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; using Robust.Shared.Collections; using Robust.Shared.Configuration; namespace Content.Client.Options.UI; /// /// Control used on all tabs of the in-game options menu, /// contains the "save" and "reset" buttons and controls the entire logic. /// /// /// /// Basic operation is simple: options tabs put this control at the bottom of the tab, /// they bind UI controls to it with calls such as , /// then they call . The rest is all handled by the control. /// /// /// Individual options are implementations of . See the type for details. /// Common implementations for building on top of CVars are already exist, /// but tabs can define their own if they need to. /// /// /// Generally, options are added via helper methods such as , /// however it is totally possible to directly instantiate the backing types /// and add them via . /// /// /// The options system is general purpose enough that does not, itself, /// know what a CVar is. It does automatically save CVars to config when save is pressed, but otherwise CVar interaction /// is handled by implementations. /// /// /// Behaviorally, the row has 3 control buttons: save, reset changed, and reset to default. /// "Save" writes the configuration changes and saves the configuration. /// "Reset changed" discards changes made in the menu and re-loads the saved settings. /// "Reset to default" resets the settings on the menu to be the default, out-of-the-box values. /// Note that "Reset to default" does not save immediately, the user must still press save manually. /// /// /// The disabled state of the 3 buttons is updated dynamically based on the values of the options. /// /// [GenerateTypedNameReferences] public sealed partial class OptionsTabControlRow : Control { [Dependency] private readonly ILocalizationManager _loc = default!; [Dependency] private readonly IConfigurationManager _cfg = default!; private ValueList _options; public OptionsTabControlRow() { RobustXamlLoader.Load(this); IoCManager.InjectDependencies(this); ResetButton.StyleClasses.Add(StyleClass.ButtonOpenRight); ApplyButton.OnPressed += ApplyButtonPressed; ResetButton.OnPressed += ResetButtonPressed; DefaultButton.OnPressed += DefaultButtonPressed; } /// /// Add a new option to be tracked by the control. /// /// The option object that manages this object's logic /// /// The type of option being passed in. Necessary to allow the return type to match the parameter type /// for easy chaining. /// /// The same as passed in, for easy chaining. public T AddOption(T option) where T : BaseOption { _options.Add(option); return option; } /// /// Add a checkbox option backed by a simple boolean CVar. /// /// The CVar represented by the checkbox. /// The UI control for the option. /// /// If true, the checkbox is inverted relative to the CVar: if the CVar is true, the checkbox will be unchecked. /// /// The option instance backing the added option. /// public OptionCheckboxCVar AddOptionCheckBox(CVarDef cVar, CheckBox checkBox, bool invert = false) { return AddOption(new OptionCheckboxCVar(this, _cfg, cVar, checkBox, invert)); } /// /// Add a slider option, displayed in percent, backed by a simple float CVar. /// /// The CVar represented by the slider. /// The UI control for the option. /// The minimum value the slider should allow. The default value represents "0%" /// The maximum value the slider should allow. The default value represents "100%" /// /// Scale with which to multiply slider values when mapped to the backing CVar. /// For example, if a scale of 2 is set, a slider at 75% writes a value of 1.5 to the CVar. /// /// The option instance backing the added option. /// /// /// Note that percentage values are represented as ratios in code, i.e. a value of 100% is "1". /// /// public OptionSliderFloatCVar AddOptionPercentSlider( CVarDef cVar, OptionSlider slider, float min = 0, float max = 1, float scale = 1) { return AddOption(new OptionSliderFloatCVar(this, _cfg, cVar, slider, min, max, scale, FormatPercent)); } /// /// Add a color slider option, backed by a simple string CVar. /// /// The CVar represented by the slider. /// The UI control for the option. /// The option instance backing the added option. public OptionColorSliderCVar AddOptionColorSlider( CVarDef cVar, OptionColorSlider slider) { return AddOption(new OptionColorSliderCVar(this, _cfg, cVar, slider)); } /// /// Add a slider option, backed by a simple integer CVar. /// /// The CVar represented by the slider. /// The UI control for the option. /// The minimum value the slider should allow. /// The maximum value the slider should allow. /// /// An optional delegate used to format the textual value display of the slider. /// If not provided, the default behavior is to directly format the integer value as text. /// /// The option instance backing the added option. public OptionSliderIntCVar AddOptionSlider( CVarDef cVar, OptionSlider slider, int min, int max, Func? format = null) { return AddOption(new OptionSliderIntCVar(this, _cfg, cVar, slider, min, max, format ?? FormatInt)); } /// /// Add a drop-down option, backed by a CVar. /// /// The CVar represented by the drop-down. /// The UI control for the option. /// /// The set of options that will be shown in the drop-down. Items are ordered as provided. /// /// The type of the CVar being controlled. /// The option instance backing the added option. public OptionDropDownCVar AddOptionDropDown( CVarDef cVar, OptionDropDown dropDown, IReadOnlyCollection.ValueOption> options) where T : notnull { return AddOption(new OptionDropDownCVar(this, _cfg, cVar, dropDown, options)); } /// /// Initializes the control row. This should be called after all options have been added. /// public void Initialize() { foreach (var option in _options) { option.LoadValue(); } UpdateButtonState(); } /// /// Re-loads options in the settings from backing values. /// Should be called when the options window is opened to make sure all values are up-to-date. /// public void ReloadValues() { Initialize(); } /// /// Called by to signal that an option's value changed through user interaction. /// /// /// implementations should not call this function directly, /// instead they should call . /// public void ValueChanged() { UpdateButtonState(); } private void UpdateButtonState() { var anyModified = _options.Any(option => option.IsModified()); var anyModifiedFromDefault = _options.Any(option => option.IsModifiedFromDefault()); DefaultButton.Disabled = !anyModifiedFromDefault; ApplyButton.Disabled = !anyModified; ResetButton.Disabled = !anyModified; } private void ApplyButtonPressed(BaseButton.ButtonEventArgs obj) { foreach (var option in _options) { if (option.IsModified()) option.SaveValue(); } _cfg.SaveToFile(); UpdateButtonState(); } private void ResetButtonPressed(BaseButton.ButtonEventArgs obj) { foreach (var option in _options) { option.LoadValue(); } UpdateButtonState(); } private void DefaultButtonPressed(BaseButton.ButtonEventArgs obj) { foreach (var option in _options) { option.ResetToDefault(); } UpdateButtonState(); } private string FormatPercent(OptionSliderFloatCVar slider, float value) { return _loc.GetString("ui-options-value-percent", ("value", value)); } private static string FormatInt(OptionSliderIntCVar slider, int value) { return value.ToString(); } } /// /// Base class of a single "option" for . /// /// /// /// Implementations of this class handle loading values from backing storage or defaults, /// handling UI controls, and saving. The main does not know what a CVar is. /// /// /// is a derived class that makes it easier to work with options /// backed by a single CVar. /// /// /// The control row that owns this option. /// public abstract class BaseOption(OptionsTabControlRow controller) { /// /// Should be called by derived implementations to indicate that their value changed, due to user interaction. /// protected virtual void ValueChanged() { controller.ValueChanged(); } /// /// Loads the value represented by this option from its backing store, into the UI state. /// public abstract void LoadValue(); /// /// Saves the value in the UI state to the backing store. /// public abstract void SaveValue(); /// /// Resets the UI state to that of the factory-default value. This should not write to the backing store. /// public abstract void ResetToDefault(); /// /// Called to check if this option's UI value is different from the backing store value. /// /// If true, the UI value is different and was modified by the user. public abstract bool IsModified(); /// /// Called to check if this option's UI value is different from the backing store's default value. /// /// If true, the UI value is different. public abstract bool IsModifiedFromDefault(); } /// /// Derived class of intended for making mappings to simple CVars easier. /// /// The type of the CVar. /// public abstract class BaseOptionCVar : BaseOption where TValue : notnull { /// /// Raised immediately when the UI value of this option is changed by the user, even before saving. /// /// /// /// This can be used to update parts of the options UI based on the state of a checkbox. /// /// public event Action? ImmediateValueChanged; private readonly IConfigurationManager _cfg; private readonly CVarDef _cVar; /// /// Sets and gets the actual CVar value to/from the frontend UI state or control. /// /// /// /// In the simplest case, this function should set a UI control's state to represent the CVar, /// and inversely conver the UI control's state to the CVar value. For simple controls like a checkbox or slider, /// this just means passing through their value property. /// /// protected abstract TValue Value { get; set; } protected BaseOptionCVar( OptionsTabControlRow controller, IConfigurationManager cfg, CVarDef cVar) : base(controller) { _cfg = cfg; _cVar = cVar; } public override void LoadValue() { Value = _cfg.GetCVar(_cVar); } public override void SaveValue() { _cfg.SetCVar(_cVar, Value); } public override void ResetToDefault() { Value = _cVar.DefaultValue; } public override bool IsModified() { return !IsValueEqual(Value, _cfg.GetCVar(_cVar)); } public override bool IsModifiedFromDefault() { return !IsValueEqual(Value, _cVar.DefaultValue); } protected virtual bool IsValueEqual(TValue a, TValue b) { // Use different logic for floats so there's some error margin. // This check is handled cleanly at compile-time by the JIT. if (typeof(TValue) == typeof(float)) return MathHelper.CloseToPercent((float) (object) a, (float) (object) b); return EqualityComparer.Default.Equals(a, b); } protected override void ValueChanged() { base.ValueChanged(); ImmediateValueChanged?.Invoke(Value); } } /// /// Implementation of a CVar option that simply corresponds with a . /// /// /// /// Generally, you should just call AddOption methods on /// instead of instantiating this type directly. /// /// /// public sealed class OptionCheckboxCVar : BaseOptionCVar { private readonly CheckBox _checkBox; private readonly bool _invert; protected override bool Value { get => _checkBox.Pressed ^ _invert; set => _checkBox.Pressed = value ^ _invert; } /// /// Creates a new instance of this type. /// /// The control row that owns this option. /// The configuration manager to get and set values from. /// The CVar that is being controlled by this option. /// The UI control for the option. /// /// If true, the checkbox is inverted relative to the CVar: if the CVar is true, the checkbox will be unchecked. /// /// /// /// It is generally more convenient to call overloads on /// such as instead of instantiating this type directly. /// /// public OptionCheckboxCVar( OptionsTabControlRow controller, IConfigurationManager cfg, CVarDef cVar, CheckBox checkBox, bool invert) : base(controller, cfg, cVar) { _checkBox = checkBox; _invert = invert; checkBox.OnToggled += _ => { ValueChanged(); }; } } /// /// Implementation of a CVar option that simply corresponds with a floating-point . /// /// public sealed class OptionSliderFloatCVar : BaseOptionCVar { /// /// Scale with which to multiply slider values when mapped to the backing CVar. /// /// /// For example, if a scale of 2 is set, a slider at 75% writes a value of 1.5 to the CVar. /// public float Scale { get; } private readonly OptionSlider _slider; private readonly Func _format; protected override float Value { get => _slider.Slider.Value * Scale; set { _slider.Slider.Value = value / Scale; UpdateLabelValue(); } } /// /// Creates a new instance of this type. /// /// /// /// It is generally more convenient to call overloads on /// such as instead of instantiating this type directly. /// /// /// The control row that owns this option. /// The configuration manager to get and set values from. /// The CVar that is being controlled by this option. /// The UI control for the option. /// The minimum value the slider should allow. /// The maximum value the slider should allow. /// /// Scale with which to multiply slider values when mapped to the backing CVar. See . /// /// Function that will be called to format the value display next to the slider. public OptionSliderFloatCVar( OptionsTabControlRow controller, IConfigurationManager cfg, CVarDef cVar, OptionSlider slider, float minValue, float maxValue, float scale, Func format) : base(controller, cfg, cVar) { Scale = scale; _slider = slider; _format = format; slider.Slider.MinValue = minValue; slider.Slider.MaxValue = maxValue; slider.Slider.OnValueChanged += _ => { ValueChanged(); UpdateLabelValue(); }; } private void UpdateLabelValue() { _slider.ValueLabel.Text = _format(this, _slider.Slider.Value); } } /// /// Implementation of a CVar option that simply corresponds with a string . /// /// public sealed class OptionColorSliderCVar : BaseOptionCVar { private readonly OptionColorSlider _slider; protected override string Value { get => _slider.Slider.Color.ToHex(); set { _slider.Slider.Color = Color.FromHex(value); UpdateLabelColor(); } } /// /// Creates a new instance of this type. /// /// /// /// It is generally more convenient to call overloads on /// such as instead of instantiating this type directly. /// /// /// The control row that owns this option. /// The configuration manager to get and set values from. /// The CVar that is being controlled by this option. /// The UI control for the option. public OptionColorSliderCVar( OptionsTabControlRow controller, IConfigurationManager cfg, CVarDef cVar, OptionColorSlider slider) : base(controller, cfg, cVar) { _slider = slider; slider.Slider.OnColorChanged += _ => { ValueChanged(); UpdateLabelColor(); }; } private void UpdateLabelColor() { _slider.ExampleLabel.FontColorOverride = Color.FromHex(Value); } } /// /// Implementation of a CVar option that simply corresponds with an integer . /// /// public sealed class OptionSliderIntCVar : BaseOptionCVar { private readonly OptionSlider _slider; private readonly Func _format; protected override int Value { get => (int) _slider.Slider.Value; set { _slider.Slider.Value = value; UpdateLabelValue(); } } /// /// Creates a new instance of this type. /// /// /// /// It is generally more convenient to call overloads on /// such as instead of instantiating this type directly. /// /// /// The control row that owns this option. /// The configuration manager to get and set values from. /// The CVar that is being controlled by this option. /// The UI control for the option. /// The minimum value the slider should allow. /// The maximum value the slider should allow. /// Function that will be called to format the value display next to the slider. public OptionSliderIntCVar( OptionsTabControlRow controller, IConfigurationManager cfg, CVarDef cVar, OptionSlider slider, int minValue, int maxValue, Func format) : base(controller, cfg, cVar) { _slider = slider; _format = format; slider.Slider.MinValue = minValue; slider.Slider.MaxValue = maxValue; slider.Slider.Rounded = true; slider.Slider.OnValueChanged += _ => { ValueChanged(); UpdateLabelValue(); }; } private void UpdateLabelValue() { _slider.ValueLabel.Text = _format(this, (int) _slider.Slider.Value); } } /// /// Implementation of a CVar option via a drop-down. /// /// public sealed class OptionDropDownCVar : BaseOptionCVar where T : notnull { private readonly OptionDropDown _dropDown; private readonly ItemEntry[] _entries; protected override T Value { get => (T) _dropDown.Button.SelectedMetadata!; set => _dropDown.Button.SelectId(FindValueId(value)); } /// /// Creates a new instance of this type. /// /// /// /// It is generally more convenient to call overloads on /// such as instead of instantiating this type directly. /// /// /// The control row that owns this option. /// The configuration manager to get and set values from. /// The CVar that is being controlled by this option. /// The UI control for the option. /// The list of options shown to the user. public OptionDropDownCVar( OptionsTabControlRow controller, IConfigurationManager cfg, CVarDef cVar, OptionDropDown dropDown, IReadOnlyCollection options) : base(controller, cfg, cVar) { if (options.Count == 0) throw new ArgumentException("Need at least one option!"); _dropDown = dropDown; _entries = new ItemEntry[options.Count]; var button = dropDown.Button; var i = 0; foreach (var option in options) { _entries[i] = new ItemEntry { Key = option.Key, }; button.AddItem(option.Label, i); button.SetItemMetadata(button.GetIdx(i), option.Key); i += 1; } dropDown.Button.OnItemSelected += args => { dropDown.Button.SelectId(args.Id); ValueChanged(); }; } private int FindValueId(T value) { for (var i = 0; i < _entries.Length; i++) { if (IsValueEqual(_entries[i].Key, value)) return i; } // This will just default select the first entry or whatever. return 0; } /// /// A single option for a drop-down. /// /// The value that this option has. This is what will be written to the CVar if selected. /// The visual text shown to the user for the option. /// /// public sealed class ValueOption(T key, string label) { /// /// The value that this option has. This is what will be written to the CVar if selected. /// public readonly T Key = key; /// /// The visual text shown to the user for the option. /// public readonly string Label = label; } private struct ItemEntry { public T Key; } }