diff --git a/docs/toc.yml b/docs/toc.yml index c78eeb6b..6549523b 100644 --- a/docs/toc.yml +++ b/docs/toc.yml @@ -26,4 +26,8 @@ items: - name: List Adapters href: ui/list-adapters.md - name: Optimizing UI for Performance - href: ui/optimization.md \ No newline at end of file + href: ui/optimization.md + - name: Advanced UI features + items: + - name: Visual Styles + href: ui/advanced/visual-styles.md \ No newline at end of file diff --git a/docs/ui/advanced/visual-styles.md b/docs/ui/advanced/visual-styles.md new file mode 100644 index 00000000..9dd7eeb2 --- /dev/null +++ b/docs/ui/advanced/visual-styles.md @@ -0,0 +1,212 @@ +# 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` 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. + +```csharp +public sealed class ProgressBar : Widget, + IVisual +{ + 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(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` 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` using this implementation. + +You should delegate the implementation of the `Draw` method to a nested type within your visual style implementation, like this: + +```csharp +public sealed class MyVisualStyle : + IVisualStyle, + IVisualRenderer +{ + 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 + { + 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` 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. + +```csharp +public sealed class MyVisualStyle : + IVisualStyle, + IVisualRenderer, + IGetLayoutProperties +{ + 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, + IGetLayoutProperties + { + 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. + +```csharp +public sealed class ProgressBarStyle : + UserStyle, + IVisualRenderer, + IGetLayoutProperties +{ + 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`. + +```csharp +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` 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. diff --git a/src/AcidicGUI/Common/StateChange.cs b/src/AcidicGUI/Common/StateChange.cs new file mode 100644 index 00000000..a1429d94 --- /dev/null +++ b/src/AcidicGUI/Common/StateChange.cs @@ -0,0 +1,19 @@ +namespace AcidicGUI.Common; + +public sealed class StateChange +{ + public T CurrentValue { get; } + public T NewValue { get; } + public bool Canceled { get; private set; } + + public StateChange(T currentValue, T newValue) + { + this.CurrentValue = currentValue; + this.NewValue = newValue; + } + + public void Cancel() + { + Canceled = true; + } +} \ No newline at end of file diff --git a/src/AcidicGUI/GuiManager.cs b/src/AcidicGUI/GuiManager.cs index e135e925..a05d6005 100755 --- a/src/AcidicGUI/GuiManager.cs +++ b/src/AcidicGUI/GuiManager.cs @@ -23,17 +23,14 @@ public sealed class GuiManager : IFontFamilyProvider private ButtonState rightShift; private ButtonState leftAlt; private ButtonState rightAlt; - - - private IVisualStyle? visualStyleOverride; - private int screenWidth; - private int screenHeight; - private bool isRendering; - private Widget? hoveredWidget; - private Widget? widgetBeingDragged; - private Widget? keyboardFocus; - private MouseState? previousMouseState; - private bool reachedFirstUpdate; + private int screenWidth; + private int screenHeight; + private bool isRendering; + private Widget? hoveredWidget; + private Widget? widgetBeingDragged; + private Widget? keyboardFocus; + private MouseState? previousMouseState; + private bool reachedFirstUpdate; public bool IsRendering => isRendering; public IOrderedCollection TopLevels => topLevels; @@ -44,9 +41,22 @@ public sealed class GuiManager : IFontFamilyProvider topLevels = new Widget.TopLevelCollection(this); renderer = new GuiRenderer(context); - visualStyleOverride = globalStyle; + StyleManager.SetVisualStyle(globalStyle); } + /// + /// Invalidates all layout and geometry data of the entire widget tree, + /// forcing a full layout update and geometry repaint. Use only when changing + /// graphics modes, as this is extremely expensive. + /// + public void ScheduleFullReRender() + { + foreach (Widget widget in TopLevels) + { + widget.InvalidateLayout(); + } + } + /// /// Invalidates the geometry of all interface elements recursively. Use this when changing a global UI setting that affects how all widgets render. /// Note: This is expensive. That should be obvious. So do not call it every frame. @@ -198,6 +208,12 @@ public sealed class GuiManager : IFontFamilyProvider public void UpdateLayout() { reachedFirstUpdate = true; + + foreach (Widget widget in CollapseChildren()) + { + if (widget is IUpdateLayoutProperties updateLayoutProperties) + updateLayoutProperties.UpdateLayoutProperties(); + } var mustRebuildLayout = false; var tolerance = 0.001f; @@ -229,12 +245,13 @@ public sealed class GuiManager : IFontFamilyProvider } } + [Obsolete("Please use the new StyleManager API instead.")] public IVisualStyle GetVisualStyle() { if (fallbackVisualStyle.FallbackFont == null) fallbackVisualStyle.FallbackFont = context.GetFallbackFont(); - return visualStyleOverride ?? fallbackVisualStyle; + return StyleManager.ActiveStyle ?? fallbackVisualStyle; } public void Render() diff --git a/src/AcidicGUI/VisualStyles/FallbackVisualStyle.cs b/src/AcidicGUI/VisualStyles/FallbackVisualStyle.cs index bf590066..e950a6c6 100755 --- a/src/AcidicGUI/VisualStyles/FallbackVisualStyle.cs +++ b/src/AcidicGUI/VisualStyles/FallbackVisualStyle.cs @@ -6,9 +6,11 @@ using Microsoft.Xna.Framework; namespace AcidicGUI.VisualStyles; -internal sealed class FallbackVisualStyle : IVisualStyle +internal sealed class FallbackVisualStyle : + IVisualStyle, + IGetLayoutProperties, + IVisualRenderer { - public int ProgressBarHeight => 16; public int SliderThickness => 12; public Point ToggleSize => new Point(18, 18); public Point SwitchSize => ToggleSize; @@ -119,18 +121,6 @@ internal sealed class FallbackVisualStyle : IVisualStyle geometry.AddQuad(widget.ContentArea, Color.Gray * 0.25f); } - public void DrawProgressBar(ProgressBar widget, GeometryHelper geometry, float fillPercentage) - { - int width = widget.ContentArea.Width; - int fillWidth = (int) MathHelper.Clamp(MathHelper.Lerp(0, width, fillPercentage), 0, width); - - var background = widget.ContentArea; - var fill = new LayoutRect(background.Left, background.Top, fillWidth, background.Height); - - geometry.AddQuad(background, Color.Gray); - geometry.AddQuad(fill, Color.Red); - } - public void DrawSlider( Slider widget, GeometryHelper geometry, @@ -153,4 +143,27 @@ internal sealed class FallbackVisualStyle : IVisualStyle geometry.AddQuad(new LayoutRect((int)MathHelper.Lerp(widget.ContentArea.Left + 1, widget.ContentArea.Right - 1, value), widget.ContentArea.Top, 2, widget.ContentArea.Height), Color.White); } } + + public bool GetLayoutProperties(ref ProgressBar.ProgressBarLayoutProperties layoutProperties) + { + layoutProperties.ProgressBarHeight = 16; + return false; + } + + public void Draw( + Widget widget, + GeometryHelper geometry, + in LayoutRect contentRect, + ProgressBar.ProgressBarVisualProperties properties + ) + { + int width = contentRect.Width; + int fillWidth = (int) MathHelper.Clamp(MathHelper.Lerp(0, width, properties.FillPercentage), 0, width); + + var background = contentRect; + var fill = new LayoutRect(background.Left, background.Top, fillWidth, background.Height); + + geometry.AddQuad(background, Color.Gray); + geometry.AddQuad(fill, Color.Red); + } } \ No newline at end of file diff --git a/src/AcidicGUI/VisualStyles/IGetLayoutProperties.cs b/src/AcidicGUI/VisualStyles/IGetLayoutProperties.cs new file mode 100644 index 00000000..3a191c48 --- /dev/null +++ b/src/AcidicGUI/VisualStyles/IGetLayoutProperties.cs @@ -0,0 +1,34 @@ +using AcidicGUI.Widgets; + +namespace AcidicGUI.VisualStyles; + +/// +/// Allows retrieving of layout properties of the given type. +/// When implemented by a visual style, allows widgets to retrieve +/// layout properties from said visual style via . +/// +/// Any value type containing layout properties for a given type of widget. +public interface IGetLayoutProperties + where TLayoutProperties : struct +{ + /// + /// Retrieves layout properties and stores them into the given layout properties reference. + /// + /// The destination to which layout properties should be stored. + /// True if the layout properties have changed, false otherwise. If true, widgets needing these properties will invalidate their layout. + bool GetLayoutProperties(ref TLayoutProperties layoutProperties); +} + +/// +/// When implemented by a widget, allows the widget +/// to query layout properties from the visual style. +/// +public interface IUpdateLayoutProperties +{ + /// + /// Retrieves layout properties from the visual style. Called by the UI system on every + /// frame before layout updates occur. If layout properties change during this method call, + /// the widget will automatically invalidate its own layout. + /// + void UpdateLayoutProperties(); +} \ No newline at end of file diff --git a/src/AcidicGUI/VisualStyles/IVisual.cs b/src/AcidicGUI/VisualStyles/IVisual.cs new file mode 100644 index 00000000..167d172f --- /dev/null +++ b/src/AcidicGUI/VisualStyles/IVisual.cs @@ -0,0 +1,14 @@ +namespace AcidicGUI.VisualStyles; + +/// +/// Interface for a widget that can be drawn by visual styles given properties +/// controlled by the widget. +/// +/// Any value type containing any widget state needed for visual rendering. +public interface IVisual where TVisualProperties : struct +{ + /// + /// Gets a read-only reference to the visual properties associated with the widget. + /// + public ref readonly TVisualProperties VisualProperties { get; } +} \ No newline at end of file diff --git a/src/AcidicGUI/VisualStyles/IVisualRenderer.cs b/src/AcidicGUI/VisualStyles/IVisualRenderer.cs new file mode 100644 index 00000000..ae562bd6 --- /dev/null +++ b/src/AcidicGUI/VisualStyles/IVisualRenderer.cs @@ -0,0 +1,29 @@ +using AcidicGUI.Layout; +using AcidicGUI.Rendering; +using AcidicGUI.Widgets; + +namespace AcidicGUI.VisualStyles; + +/// +/// When implemented by a visual style, any implementing +/// to be rendered with the visual style +/// via where TWidget +/// is a implementing +/// +/// The type of visual properties needed to render widgets of the given type. +public interface IVisualRenderer where TVisualProperties : struct +{ + /// + /// Draws a widget using the specified visual properties. + /// + /// The widget to draw + /// The destination to which the widget should be drawn + /// The widget's calculated content bounds + /// The visual properties associated with the widget + void Draw( + Widget widget, + GeometryHelper geometry, + in LayoutRect contentRect, + TVisualProperties properties + ); +} \ No newline at end of file diff --git a/src/AcidicGUI/VisualStyles/IVisualStyle.cs b/src/AcidicGUI/VisualStyles/IVisualStyle.cs index cf9cde9a..a811ea07 100755 --- a/src/AcidicGUI/VisualStyles/IVisualStyle.cs +++ b/src/AcidicGUI/VisualStyles/IVisualStyle.cs @@ -6,9 +6,9 @@ using Microsoft.Xna.Framework; namespace AcidicGUI.VisualStyles; -public interface IVisualStyle : IFontFamilyProvider +public interface IVisualStyle : + IFontFamilyProvider { - int ProgressBarHeight { get; } int SliderThickness { get; } Point ToggleSize { get; } Point SwitchSize { get; } @@ -61,12 +61,6 @@ public interface IVisualStyle : IFontFamilyProvider bool pressed, bool selected ); - - void DrawProgressBar( - ProgressBar widget, - GeometryHelper geometry, - float fillPercentage - ); void DrawSlider( Slider widget, diff --git a/src/AcidicGUI/VisualStyles/StyleManager.cs b/src/AcidicGUI/VisualStyles/StyleManager.cs new file mode 100644 index 00000000..be23a936 --- /dev/null +++ b/src/AcidicGUI/VisualStyles/StyleManager.cs @@ -0,0 +1,73 @@ +using AcidicGUI.CustomProperties; +using AcidicGUI.Rendering; +using AcidicGUI.Widgets; + +namespace AcidicGUI.VisualStyles; + +/// +/// API for interacting with global styles throughout the UI. +/// +public static class StyleManager +{ + private static IVisualStyle? activeStyle; + + [Obsolete("This property is for compatibility with older widgets, and is destined to be removed.")] + internal static IVisualStyle? ActiveStyle => activeStyle; + + /// + /// Changes the active visual style, without notifying any widgets that this has happened. + /// + /// The new style to use. If null, styled widgets will use fallback rendering. + internal static void SetVisualStyle(IVisualStyle? newStyle) + { + activeStyle = newStyle; + } + + /// + /// Gets layout properties of the given type from the specified style override, + /// or the active global style if the style override is null. If the style doesn't + /// implement , then defaults + /// will be returned instead. + /// + /// + /// An optional style override. + /// A reference to a value where layouit properties will be written to. + /// Any value type. + /// True if the properties have changed in the visual style, false otherwise. + internal static bool GetLayoutProperties(IVisualStyle? styleOverride, ref TLayoutProperties properties) + where TLayoutProperties : struct + { + var style = styleOverride ?? activeStyle; + + properties = default(TLayoutProperties); + + if (style is IGetLayoutProperties retriever) + return retriever.GetLayoutProperties(ref properties); + + return false; + } + + /// + /// Uses the current visual style to draw the given widget. + /// If the visual style does not support drawing the given widget + /// (it doesn't implement ), then + /// no geometry will be drawn. + /// + /// The destination to which geometry should be drawn. + /// The widget to be drawn. Must implement . + /// Any type deriving implementing . + /// Any value type. + public static void DrawVisual(this GeometryHelper geometryHelper, TWidget widget) + where TWidget : Widget, IVisual + where TVisualProperties : struct + { + var style = widget.GetVisualStyleOverride() ?? activeStyle; + if (style is not IVisualRenderer renderer) + return; + + var contentArea = widget.ContentArea; + var styleProperties = widget.VisualProperties; + + renderer.Draw(widget, geometryHelper, in contentArea, styleProperties); + } +} \ No newline at end of file diff --git a/src/AcidicGUI/Widgets/Dropdown.cs b/src/AcidicGUI/Widgets/Dropdown.cs index 306e6071..25846745 100755 --- a/src/AcidicGUI/Widgets/Dropdown.cs +++ b/src/AcidicGUI/Widgets/Dropdown.cs @@ -55,7 +55,7 @@ public abstract class Dropdown : Widget, protected override Point GetContentSize(Point availableSize) { - Padding buttonPadding = GetVisualStyle().DropdownButtonPadding; + Padding buttonPadding = GetVisualStyleOverride().DropdownButtonPadding; Point buttonSize = buttonBox.GetCachedContentSize(availableSize); return new Point(buttonSize.X + buttonPadding.Horizontal, buttonSize.Y + buttonPadding.Vertical); @@ -63,7 +63,7 @@ public abstract class Dropdown : Widget, protected override void ArrangeChildren(IGuiContext context, LayoutRect availableSpace) { - Padding buttonPadding = GetVisualStyle().DropdownButtonPadding; + Padding buttonPadding = GetVisualStyleOverride().DropdownButtonPadding; buttonBox.UpdateLayout(context, new LayoutRect(availableSpace.Left + buttonPadding.Left, availableSpace.Top + buttonPadding.Top, availableSpace.Width - buttonPadding.Horizontal, availableSpace.Height - buttonPadding.Vertical)); Point screen = new Point(context.CanvasWidth, context.CanvasHeight); @@ -258,7 +258,7 @@ public abstract class Dropdown : Widget, protected override void RebuildGeometry(GeometryHelper geometry) { - GetVisualStyle().DrawDropdownItemBackground(this, geometry, hovered, pressed, active); + GetVisualStyleOverride().DrawDropdownItemBackground(this, geometry, hovered, pressed, active); } public void OnMouseClick(MouseButtonEvent e) @@ -404,7 +404,7 @@ public abstract class Dropdown : Widget, protected override void RebuildGeometry(GeometryHelper geometry) { - GetVisualStyle().DrawDropdownItemsBackground(geometry, DropdownArea); + GetVisualStyleOverride().DrawDropdownItemsBackground(geometry, DropdownArea); } } } \ No newline at end of file diff --git a/src/AcidicGUI/Widgets/Icon.cs b/src/AcidicGUI/Widgets/Icon.cs index 74a5cd36..49edde29 100755 --- a/src/AcidicGUI/Widgets/Icon.cs +++ b/src/AcidicGUI/Widgets/Icon.cs @@ -43,7 +43,7 @@ public sealed class Icon : Widget protected override Point GetContentSize(Point availableSize) { - Font? iconFont = GetVisualStyle().IconFont; + Font? iconFont = GetVisualStyleOverride().IconFont; if (iconFont == null || string.IsNullOrEmpty(iconString)) return Point.Zero; @@ -52,7 +52,7 @@ public sealed class Icon : Widget protected override void RebuildGeometry(GeometryHelper geometry) { - var font = GetVisualStyle().IconFont; + var font = GetVisualStyleOverride().IconFont; float x = ContentArea.Left + (ContentArea.Width - actualIconSize.X) / 2f; float y = ContentArea.Top + (ContentArea.Height - actualIconSize.Y) / 2f; diff --git a/src/AcidicGUI/Widgets/ProgressBar.cs b/src/AcidicGUI/Widgets/ProgressBar.cs index 7f4f899a..e618b956 100644 --- a/src/AcidicGUI/Widgets/ProgressBar.cs +++ b/src/AcidicGUI/Widgets/ProgressBar.cs @@ -1,29 +1,58 @@ +using AcidicGUI.Animation; using AcidicGUI.Rendering; +using AcidicGUI.VisualStyles; using Microsoft.Xna.Framework; namespace AcidicGUI.Widgets; -public sealed class ProgressBar : Widget +public sealed class ProgressBar : + Widget, + IVisual, + IUpdateLayoutProperties { - private float fillPercentage = 0; + private ProgressBarVisualProperties visualProperties; + private ProgressBarLayoutProperties layoutProperties; + /// + public ref readonly ProgressBarVisualProperties VisualProperties => ref visualProperties; + public float Value { - get => fillPercentage; + get => visualProperties.FillPercentage; set { - fillPercentage = MathHelper.Clamp(value, 0, 1); + var newPercentage = MathHelper.Clamp(value, 0, 1); + + if (Math.Abs(newPercentage - visualProperties.FillPercentage) < float.Epsilon) + return; + + visualProperties.FillPercentage = newPercentage; InvalidateGeometry(); } } protected override Point GetContentSize(Point availableSize) { - return new Point(availableSize.X, GetVisualStyle().ProgressBarHeight); + return new Point(availableSize.X, layoutProperties.ProgressBarHeight); } protected override void RebuildGeometry(GeometryHelper geometry) { - GetVisualStyle().DrawProgressBar(this, geometry, fillPercentage); + geometry.DrawVisual(this); + } + + public void UpdateLayoutProperties() + { + this.GetLayoutProperties(ref layoutProperties); + } + + public struct ProgressBarVisualProperties + { + public float FillPercentage; + } + + public struct ProgressBarLayoutProperties() + { + public int ProgressBarHeight = 16; } } \ No newline at end of file diff --git a/src/AcidicGUI/Widgets/ScrollView.cs b/src/AcidicGUI/Widgets/ScrollView.cs index dc2b4b43..e8d38da2 100755 --- a/src/AcidicGUI/Widgets/ScrollView.cs +++ b/src/AcidicGUI/Widgets/ScrollView.cs @@ -60,7 +60,7 @@ public sealed class ScrollView : availableSpace = new LayoutRect( availableSpace.Left, availableSpace.Top, - availableSpace.Width - GetVisualStyle().ScrollBarSize, + availableSpace.Width - GetVisualStyleOverride().ScrollBarSize, availableSpace.Height ); } @@ -104,10 +104,10 @@ public sealed class ScrollView : if (showScrollBar) { - GetVisualStyle().DrawScrollBar(this, geometry, new LayoutRect( - ContentArea.Right - GetVisualStyle().ScrollBarSize, + GetVisualStyleOverride().DrawScrollBar(this, geometry, new LayoutRect( + ContentArea.Right - GetVisualStyleOverride().ScrollBarSize, ContentArea.Top, - GetVisualStyle().ScrollBarSize, + GetVisualStyleOverride().ScrollBarSize, ContentArea.Height ), pageOffset, innerSize); } diff --git a/src/AcidicGUI/Widgets/Slider.cs b/src/AcidicGUI/Widgets/Slider.cs index b72fcdef..bcdd38ad 100755 --- a/src/AcidicGUI/Widgets/Slider.cs +++ b/src/AcidicGUI/Widgets/Slider.cs @@ -41,7 +41,7 @@ public sealed class Slider : Widget, protected override Point GetContentSize(Point availableSize) { - int thickness = GetVisualStyle().SliderThickness; + int thickness = GetVisualStyleOverride().SliderThickness; return direction == Direction.Horizontal ? new Point(100, thickness) @@ -50,7 +50,7 @@ public sealed class Slider : Widget, protected override void RebuildGeometry(GeometryHelper geometry) { - GetVisualStyle().DrawSlider(this, geometry, hovered, pressed, direction == Direction.Vertical, currentValue); + GetVisualStyleOverride().DrawSlider(this, geometry, hovered, pressed, direction == Direction.Vertical, currentValue); } private void SetValue(float newValue) diff --git a/src/AcidicGUI/Widgets/TextWidget.cs b/src/AcidicGUI/Widgets/TextWidget.cs index 22f2e704..957acd52 100755 --- a/src/AcidicGUI/Widgets/TextWidget.cs +++ b/src/AcidicGUI/Widgets/TextWidget.cs @@ -282,13 +282,13 @@ public class TextWidget : Widget, } var renderColor = element.MarkupData.IsSelected - ? GetVisualStyle().TextSelectionForeground - : (element.MarkupData.ColorOverride ?? TextColor) ?? GetVisualStyle().GetTextColor(this); + ? GetVisualStyleOverride().TextSelectionForeground + : (element.MarkupData.ColorOverride ?? TextColor) ?? GetVisualStyleOverride().GetTextColor(this); if (element.MeasuredSize.HasValue && element.MarkupData.Highlight.A > 0 || element.MarkupData.IsSelected) { var highlight = element.MarkupData.IsSelected - ? GetVisualStyle().TextSelectionBackground + ? GetVisualStyleOverride().TextSelectionBackground : element.MarkupData.Highlight; var highlightRect = new LayoutRect( diff --git a/src/AcidicGUI/Widgets/Toggle.cs b/src/AcidicGUI/Widgets/Toggle.cs index 6c9449ef..d0555d8b 100755 --- a/src/AcidicGUI/Widgets/Toggle.cs +++ b/src/AcidicGUI/Widgets/Toggle.cs @@ -1,3 +1,4 @@ +using AcidicGUI.Common; using AcidicGUI.Events; using AcidicGUI.Layout; using AcidicGUI.Rendering; @@ -20,6 +21,7 @@ public sealed class Toggle : Widget, private bool focused; private bool toggleValue; + public event Action>? OnBeforeValueChanged; public event Action? OnValueChanged; public bool UseSwitchVariant @@ -41,7 +43,7 @@ public sealed class Toggle : Widget, return; toggleValue = value; - NotifyChange(); + OnValueChanged?.Invoke(value); InvalidateGeometry(); } } @@ -51,22 +53,22 @@ public sealed class Toggle : Widget, protected override Point GetContentSize(Point availableSize) { return isSwitchVariant - ? GetVisualStyle().SwitchSize - : GetVisualStyle().ToggleSize; + ? GetVisualStyleOverride().SwitchSize + : GetVisualStyleOverride().ToggleSize; } protected override void RebuildGeometry(GeometryHelper geometry) { var size = isSwitchVariant - ? GetVisualStyle().SwitchSize - : GetVisualStyle().ToggleSize; + ? GetVisualStyleOverride().SwitchSize + : GetVisualStyleOverride().ToggleSize; var rect = new LayoutRect(ContentArea.Left + ((ContentArea.Width - size.X) / 2), ContentArea.Top + ((ContentArea.Height - size.Y) / 2), size.X, size.Y); if (isSwitchVariant) - GetVisualStyle().DrawToggleSwitch(this, geometry, rect, hovered, pressed, focused, toggleValue); + GetVisualStyleOverride().DrawToggleSwitch(this, geometry, rect, hovered, pressed, focused, toggleValue); else - GetVisualStyle().DrawToggle(this, geometry, rect, hovered, pressed, focused, toggleValue); + GetVisualStyleOverride().DrawToggle(this, geometry, rect, hovered, pressed, focused, toggleValue); } public void OnMouseEnter(MouseMoveEvent e) @@ -121,14 +123,22 @@ public sealed class Toggle : Widget, if (!hovered) return; - - toggleValue = !toggleValue; + InvalidateGeometry(); - NotifyChange(); + NotifyChange(!toggleValue); } - private void NotifyChange() + private void NotifyChange(bool newValue) { + var currentValue = toggleValue; + var change = new StateChange(currentValue, newValue); + + OnBeforeValueChanged?.Invoke(change); + + if (change.Canceled) + return; + + toggleValue = newValue; OnValueChanged?.Invoke(toggleValue); } } \ No newline at end of file diff --git a/src/AcidicGUI/Widgets/Widget.Layout.cs b/src/AcidicGUI/Widgets/Widget.Layout.cs index 5cc2b2f0..c8f1d711 100755 --- a/src/AcidicGUI/Widgets/Widget.Layout.cs +++ b/src/AcidicGUI/Widgets/Widget.Layout.cs @@ -1,23 +1,25 @@ using AcidicGUI.Layout; +using AcidicGUI.VisualStyles; using Microsoft.Xna.Framework; namespace AcidicGUI.Widgets; public partial class Widget { - private Widget? layoutRoot; - private bool layoutIsDirty = true; - private HorizontalAlignment horizontalAlignment; - private VerticalAlignment verticalAlignment; - private LayoutRect geometryRect; - private LayoutRect calculatedLayoutRect; - private Point? cachedContentSize; - private Padding padding; - private Padding margin; - private Point minimumSize; - private Point maximumSize; - private Point previousAvailableSize; - private Visibility visibility; + private readonly HashSet dirtyLayoutPropertyTypes = new(); + private Widget? layoutRoot; + private bool layoutIsDirty = true; + private HorizontalAlignment horizontalAlignment; + private VerticalAlignment verticalAlignment; + private LayoutRect geometryRect; + private LayoutRect calculatedLayoutRect; + private Point? cachedContentSize; + private Padding padding; + private Padding margin; + private Point minimumSize; + private Point maximumSize; + private Point previousAvailableSize; + private Visibility visibility; public bool IsVisible => Visibility == Visibility.Visible && (Parent == null || Parent.IsVisible); @@ -125,7 +127,20 @@ public partial class Widget layoutRoot = value; } } + + protected void GetLayoutProperties(ref TLayoutProperties properties) + where TLayoutProperties : struct + { + var wasDirty = StyleManager.GetLayoutProperties(GetVisualStyleOverride(), ref properties) + || dirtyLayoutPropertyTypes.Contains(typeof(TLayoutProperties)); + if (wasDirty) + { + dirtyLayoutPropertyTypes.Add(typeof(TLayoutProperties)); + InvalidateLayout(); + } + } + public void InvalidateOwnLayout() { InvalidateLayoutInternal(); @@ -145,6 +160,8 @@ public partial class Widget if (!layoutIsDirty) return; + dirtyLayoutPropertyTypes.Clear(); + if (visibility == Visibility.Collapsed) { calculatedLayoutRect = new LayoutRect(0, 0, 0, 0); diff --git a/src/AcidicGUI/Widgets/Widget.cs b/src/AcidicGUI/Widgets/Widget.cs index d9a5074e..b34a8a4c 100755 --- a/src/AcidicGUI/Widgets/Widget.cs +++ b/src/AcidicGUI/Widgets/Widget.cs @@ -22,7 +22,7 @@ public abstract partial class Widget : IFontFamilyProvider private ClippingMode clippingMode; private LayoutRect clipRect; private IWidgetEffect? effectOverride; - + public IWidgetEffect? RenderEffect { get => effectOverride; @@ -103,6 +103,9 @@ public abstract partial class Widget : IFontFamilyProvider get => visualStyleOverride; set { + if (visualStyleOverride != value) + return; + visualStyleOverride = value; InvalidateLayout(); } @@ -154,20 +157,27 @@ public abstract partial class Widget : IFontFamilyProvider } } - public IVisualStyle GetVisualStyle() + /// + /// Walks up the widget tree from this widget to the top-level, returning + /// the first visual style override that isn't null. If no widgets in the + /// path override the global visual style, then null will be returned. + /// + /// The visual style override for this widget or one of its parents, or null if there are no overriding widgets. + public IVisualStyle? GetVisualStyleOverride() { if (visualStyleOverride != null) return visualStyleOverride; if (parent != null) - return parent.GetVisualStyle(); + return parent.GetVisualStyleOverride(); - return GuiManager!.GetVisualStyle(); + // TODO: Return null and remove that property when we've ported all the widgets. + return StyleManager.ActiveStyle; } protected virtual void RebuildGeometry(GeometryHelper geometry) { - GetVisualStyle().DrawWidgetBackground(this, geometry); + GetVisualStyleOverride().DrawWidgetBackground(this, geometry); } private LayoutRect? GetClippingRectangle() diff --git a/src/SociallyDistant.Framework/Core/Config/ISettingsUiBuilder.cs b/src/SociallyDistant.Framework/Core/Config/ISettingsUiBuilder.cs index 817cab2c..13072bed 100755 --- a/src/SociallyDistant.Framework/Core/Config/ISettingsUiBuilder.cs +++ b/src/SociallyDistant.Framework/Core/Config/ISettingsUiBuilder.cs @@ -1,14 +1,66 @@ namespace SociallyDistant.Core.Core.Config { + /// + /// Interfaces for building settings pages, and adding + /// fields to said pages. + /// public interface ISettingsUiBuilder { + /// + /// Creates a new labeled section to categorize fields under. + /// + /// The name of the section as shown in the UI + /// A unique identifier representing the section + /// ISettingsUiBuilder AddSection(string sectionTitle, out int sectionId); ISettingsUiBuilder WithLabel(string labelText, int sectionId); - ISettingsUiBuilder WithToggle(string title, string? description, bool value, Action changeCallback, int sectionId); - ISettingsUiBuilder WithSlider(string title, string? description, float value, float minimum, float maximum, Action changeCallback, int sectionId); - ISettingsUiBuilder WithSlider(string title, string? description, int value, int minimum, int maximum, Action changeCallback, int sectionId); - ISettingsUiBuilder WithTextField(string title, string? description, string? currentValue, Action changeCallback, int sectionId); - ISettingsUiBuilder WithStringDropdown(string title, string? description, int currentIndex, string[] choices, Action changeCallback, int sectionId); + + ISettingsUiBuilder WithToggle( + string title, + string? description, + bool value, + Action changeCallback, + int sectionId, + bool requireConfirmation = false, + string? confirmationMessage = null + ); + + ISettingsUiBuilder WithSlider( + string title, + string? description, + float value, + float minimum, + float maximum, + Action changeCallback, + int sectionId + ); + + ISettingsUiBuilder WithSlider( + string title, + string? description, + int value, + int minimum, + int maximum, + Action changeCallback, + int sectionId + ); + + ISettingsUiBuilder WithTextField( + string title, + string? description, + string? currentValue, + Action changeCallback, + int sectionId + ); + + ISettingsUiBuilder WithStringDropdown( + string title, + string? description, + int currentIndex, + string[] choices, + Action changeCallback, + int sectionId + ); } } \ No newline at end of file diff --git a/src/SociallyDistant.Framework/Core/Config/SettingsCategory.cs b/src/SociallyDistant.Framework/Core/Config/SettingsCategory.cs index 262a8e82..55087ce3 100755 --- a/src/SociallyDistant.Framework/Core/Config/SettingsCategory.cs +++ b/src/SociallyDistant.Framework/Core/Config/SettingsCategory.cs @@ -11,6 +11,7 @@ public string Name => reflectionAttribute.DisplayName; public string SectionName => reflectionAttribute.SectionName; public bool Hidden => reflectionAttribute.Hidden; + public int Priority => reflectionAttribute.Priority; protected SettingsCategory(ISettingsManager settingsManager) { diff --git a/src/SociallyDistant.Framework/Core/Config/SettingsCategoryAttribute.cs b/src/SociallyDistant.Framework/Core/Config/SettingsCategoryAttribute.cs index 96b926be..a3f3d968 100755 --- a/src/SociallyDistant.Framework/Core/Config/SettingsCategoryAttribute.cs +++ b/src/SociallyDistant.Framework/Core/Config/SettingsCategoryAttribute.cs @@ -3,17 +3,19 @@ [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] public class SettingsCategoryAttribute : Attribute { - public SettingsCategoryAttribute(string key, string displayName, string sectionName, bool hidden = false) + public SettingsCategoryAttribute(string key, string displayName, string sectionName, bool hidden = false, int priority = 0) { Key = key; DisplayName = displayName; Hidden = hidden; SectionName = sectionName; + this.Priority = priority; } public string SectionName { get; } public string Key { get; } public string DisplayName { get; } public bool Hidden { get; } + public int Priority { get; } } } \ No newline at end of file diff --git a/src/SociallyDistant.Framework/Core/Config/SystemConfigCategories/GraphicsSettings.cs b/src/SociallyDistant.Framework/Core/Config/SystemConfigCategories/GraphicsSettings.cs index 87951f25..e920fd63 100755 --- a/src/SociallyDistant.Framework/Core/Config/SystemConfigCategories/GraphicsSettings.cs +++ b/src/SociallyDistant.Framework/Core/Config/SystemConfigCategories/GraphicsSettings.cs @@ -5,7 +5,7 @@ using SociallyDistant.Core.Core.Config; namespace SociallyDistant.Core.Config.SystemConfigCategories { - [SettingsCategory("graphics", "Graphics", CommonSettingsCategorySections.Hardware)] + [SettingsCategory("graphics", "Graphics", CommonSettingsCategorySections.Hardware, priority: 1)] public class GraphicsSettings : SettingsCategory { public string? DisplayResolution @@ -62,7 +62,13 @@ namespace SociallyDistant.Core.Config.SystemConfigCategories private string[] GetAvailableResolutions() { - return GraphicsAdapter.DefaultAdapter.SupportedDisplayModes.OrderByDescending(x => x.Width * x.Height).Where(x => x.Height > 720).Select(x => $"{x.Width}x{x.Height}").Distinct().ToArray(); + return GraphicsAdapter.DefaultAdapter.SupportedDisplayModes. + OrderByDescending(x => x.Width) + .ThenByDescending(x => x.Height) + .Where(x => x.Height >= 720) + .Select(x => $"{x.Width}x{x.Height}") + .Distinct() + .ToArray(); } /// @@ -79,58 +85,56 @@ namespace SociallyDistant.Core.Config.SystemConfigCategories if (currentIndex == -1) currentIndex = 0; #endif + uiBuilder.AddSection("Display", out int display).WithStringDropdown( + "Resolution", + null, + currentIndex, + resolutions, + x => DisplayResolution = resolutions[x], + display + ).WithToggle( + "Fullscreen", + null, + Fullscreen, + x => Fullscreen = x, + display + ).WithToggle( + "Enable V-sync", + null, + VSync, + x => VSync = x, + display + ); - uiBuilder.AddSection("Display", out int display) - .WithStringDropdown( - "Resolution", - null, - currentIndex, - resolutions, - x => DisplayResolution = resolutions[x], - display - ).WithToggle( - "Fullscreen", - null, - Fullscreen, - x => Fullscreen = x, - display - ).WithToggle( - "Enable V-sync", - null, - VSync, - x => VSync = x, - display - ); - - uiBuilder.AddSection("Desktop Effects", out int effects) - .WithToggle("Background blurs", - "Adds translucent, blurred backgrounds to various interface elements like the terminal", - BlurEffect, - x => BlurEffect = x, - effects) - .WithToggle("Bloom", - "Adds subtle glow to brighter UI elements and text", - BloomEffect, - x => BloomEffect = x, - effects) - .WithToggle( - "Glitch bands", - "Enable or disable the flashing bands of color that appear during malware infections and high system load.", - EnableXorgGlitches, - x => EnableXorgGlitches = x, - effects - ); + uiBuilder.AddSection("Desktop Effects", out int effects).WithToggle( + "Background blurs", + "Adds translucent, blurred backgrounds to various interface elements like the terminal", + BlurEffect, + x => BlurEffect = x, + effects + ).WithToggle( + "Bloom", + "Adds subtle glow to brighter UI elements and text", + BloomEffect, + x => BloomEffect = x, + effects + ).WithToggle( + "Glitch bands", + "Enable or disable the flashing bands of color that appear during malware infections and high system load.", + EnableXorgGlitches, + x => EnableXorgGlitches = x, + effects + ); if (OperatingSystem.IsLinux()) { - uiBuilder.AddSection("Linux", out int linux) - .WithToggle( - "Wayland Support", - "Change whether Socially Distant uses Wayland or X11 to communicate with your desktop environment. Disabling Wayland support may work around bugs on certain desktops. Changing this setting requires a game restart.", - UseWaylandBackend, - x => UseWaylandBackend = x, - linux - ); + uiBuilder.AddSection("Linux", out int linux).WithToggle( + "Wayland Support", + "Change whether Socially Distant uses Wayland or X11 to communicate with your desktop environment. Disabling Wayland support may work around bugs on certain desktops. Changing this setting requires a game restart.", + UseWaylandBackend, + x => UseWaylandBackend = x, + linux + ); } } diff --git a/src/SociallyDistant.Framework/Core/SociallyDistantUtility.cs b/src/SociallyDistant.Framework/Core/SociallyDistantUtility.cs index 72791848..db680f7c 100755 --- a/src/SociallyDistant.Framework/Core/SociallyDistantUtility.cs +++ b/src/SociallyDistant.Framework/Core/SociallyDistantUtility.cs @@ -3,6 +3,7 @@ using System.Security; using System.Text; using SociallyDistant.Core.Modules; using SociallyDistant.Core.OS.Network; +using SociallyDistant.Core.Shell.Windowing; namespace SociallyDistant.Core.Core { @@ -34,6 +35,21 @@ namespace SociallyDistant.Core.Core public static readonly string PlayerHomeId = "player"; + public static Task GetResultAsync(this IMessageDialog messageDialog) + { + var completionSource = new TaskCompletionSource(); + + messageDialog.DismissCallback += OnDialogDismiss; + + return completionSource.Task; + + void OnDialogDismiss(MessageDialogResult result) + { + messageDialog.DismissCallback -= OnDialogDismiss; + completionSource.SetResult(result); + } + } + public static IEnumerable UnravelAggregateExceptions(AggregateException? aggregate) { if (aggregate == null) diff --git a/src/SociallyDistant.Framework/Modules/Application.cs b/src/SociallyDistant.Framework/Modules/Application.cs index ccff1910..338411c8 100755 --- a/src/SociallyDistant.Framework/Modules/Application.cs +++ b/src/SociallyDistant.Framework/Modules/Application.cs @@ -1,11 +1,12 @@ using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; using AcidicGUI.Widgets; using Microsoft.Win32; using Serilog; namespace SociallyDistant.Core.Modules; -public abstract class Application +public abstract class Application : IDisposable { private static Application? _current; private bool started; @@ -37,6 +38,14 @@ public abstract class Application } } + public void Restart() + { + if (!started) + return; + + OnRestart(); + } + public void Start() { if (started) @@ -55,11 +64,12 @@ public abstract class Application } protected abstract void Run(); + protected abstract void OnRestart(); private void DetermineHardwareData() { Log.Information("Determining system info and whether we can run..."); - OperatingSystem = Environment.OSVersion.ToString(); + OperatingSystem = $"{RuntimeInformation.OSDescription} {RuntimeInformation.OSArchitecture} ({RuntimeInformation.FrameworkDescription})"; Log.Information($"Operating system is {OperatingSystem}"); switch (Environment.OSVersion.Platform) @@ -120,4 +130,9 @@ public abstract class Application // Pick the first. CpuName = cpuModels[0]; } + + public virtual void Dispose() + { + _current = null; + } } \ No newline at end of file diff --git a/src/SociallyDistant.Framework/UI/Effects/BackgroundBlurWidgetEffect.cs b/src/SociallyDistant.Framework/UI/Effects/BackgroundBlurWidgetEffect.cs index 4fc02a31..1f1fbbee 100755 --- a/src/SociallyDistant.Framework/UI/Effects/BackgroundBlurWidgetEffect.cs +++ b/src/SociallyDistant.Framework/UI/Effects/BackgroundBlurWidgetEffect.cs @@ -40,19 +40,41 @@ public sealed class BackgroundBlurWidgetEffect : IWidgetEffect, ColorSourceBlend = Blend.SourceAlpha, ColorDestinationBlend = Blend.InverseSourceAlpha, AlphaSourceBlend = Blend.One, AlphaDestinationBlend = Blend.InverseSourceAlpha, }; - private BackgroundBlurWidgetEffect(MonoGameEffect defaultUiShader, Effect bluReFfect) + private BackgroundBlurWidgetEffect(MonoGameEffect defaultUiShader, Effect blurEffect) { this.defaultUiShader = defaultUiShader; - this.gaussianShader = bluReFfect; + this.gaussianShader = blurEffect; - blurrinessParameter = bluReFfect.Parameters["Blurriness"]; - texelSizeParameter = bluReFfect.Parameters["TexelSize"]; - curveParameter = bluReFfect.Parameters["Curve"]; + blurrinessParameter = blurEffect.Parameters["Blurriness"]; + texelSizeParameter = blurEffect.Parameters["TexelSize"]; + curveParameter = blurEffect.Parameters["Curve"]; - vertexBuffer = new VertexBuffer(bluReFfect.GraphicsDevice, typeof(VertexPositionColorTexture), 4, BufferUsage.None); - indexBuffer = new IndexBuffer(bluReFfect.GraphicsDevice, typeof(int), 6, BufferUsage.None); - - vertexBuffer.SetData(new VertexPositionColorTexture[] { new VertexPositionColorTexture(new Vector3(-1, -1, 0), Color.White, new Vector2(0, 0)), new VertexPositionColorTexture(new Vector3(1, -1, 0), Color.White, new Vector2(1, 0)), new VertexPositionColorTexture(new Vector3(-1, 1, 0), Color.White, new Vector2(0, 1)), new VertexPositionColorTexture(new Vector3(1, 1, 0), Color.White, new Vector2(1, 1)) }); + vertexBuffer = new VertexBuffer(blurEffect.GraphicsDevice, typeof(VertexPositionColorTexture), 4, BufferUsage.None); + indexBuffer = new IndexBuffer(blurEffect.GraphicsDevice, typeof(int), 6, BufferUsage.None); + + vertexBuffer.SetData(new VertexPositionColorTexture[] + { + new VertexPositionColorTexture( + new Vector3(-1, -1, 0), + Color.White, + new Vector2(0, 0) + ), + new VertexPositionColorTexture( + new Vector3(1, -1, 0), + Color.White, + new Vector2(1, 0) + ), + new VertexPositionColorTexture( + new Vector3(-1, 1, 0), + Color.White, + new Vector2(0, 1) + ), + new VertexPositionColorTexture( + new Vector3(1, 1, 0), + Color.White, + new Vector2(1, 1) + ) + }); indexBuffer.SetData(new int[] { 0, 1, 2, 2, 1, 3 }); settingsObserver = Application.Instance.Context.SettingsManager.ObserveChanges(OnGameSettingsChanged); @@ -87,15 +109,19 @@ public sealed class BackgroundBlurWidgetEffect : IWidgetEffect, } var blurSettings = widget.GetCustomProperties(); - + var cachedGeometry = widget.GetCustomProperties(); + blurriness = blurSettings.Blurriness; curve = blurSettings.ComputedCurve; Viewport viewport = renderer.GraphicsDevice.Viewport; - ResizeIfNeeded(ref blurTarget1, viewport.Width, viewport.Height); - ResizeIfNeeded(ref blurTarget2, viewport.Width, viewport.Height); + var needsToResize = ResizeIfNeeded(ref blurTarget1, viewport.Width, viewport.Height); + needsToResize |= ResizeIfNeeded(ref blurTarget2, viewport.Width, viewport.Height); + if (needsToResize) + cachedGeometry.Geometry = null; + if (blurTarget1 == null) return; @@ -146,13 +172,17 @@ public sealed class BackgroundBlurWidgetEffect : IWidgetEffect, { } - private void ResizeIfNeeded(ref RenderTarget2D? target, int width, int height) + private bool ResizeIfNeeded(ref RenderTarget2D? target, int width, int height) { if (target == null || target.Width != width || target.Height != height) { target?.Dispose(); target = new RenderTarget2D(gaussianShader.GraphicsDevice, width, height, false, SurfaceFormat.Color, DepthFormat.Depth24Stencil8, 4, RenderTargetUsage.PreserveContents); + + return true; } + + return false; } private void DoBlur(RenderTarget2D source, RenderTarget2D destination, int pass) diff --git a/src/SociallyDistant.Framework/UI/GuiService.cs b/src/SociallyDistant.Framework/UI/GuiService.cs index ce34b198..643c43b9 100755 --- a/src/SociallyDistant.Framework/UI/GuiService.cs +++ b/src/SociallyDistant.Framework/UI/GuiService.cs @@ -50,6 +50,11 @@ public sealed class GuiService : refreshGeometryListener = EventBus.Listen(OnGeometryRefreshRequested); } + public void ScheduleFullReRender() + { + acidicGui.ScheduleFullReRender(); + } + public override void Initialize() { base.Initialize(); diff --git a/src/SociallyDistant.Framework/UI/Recycling/SettingsWidgets/WidgetListSettingsUIBuilder.cs b/src/SociallyDistant.Framework/UI/Recycling/SettingsWidgets/WidgetListSettingsUIBuilder.cs index fe61d826..e10d49d3 100755 --- a/src/SociallyDistant.Framework/UI/Recycling/SettingsWidgets/WidgetListSettingsUIBuilder.cs +++ b/src/SociallyDistant.Framework/UI/Recycling/SettingsWidgets/WidgetListSettingsUIBuilder.cs @@ -40,10 +40,25 @@ public class WidgetListSettingsUiBuilder : ISettingsUiBuilder string? description, bool value, Action changeCallback, - int sectionId + int sectionId, + bool requireConfirmation, + string? confirmationMessage = null ) { - widgets.AddWidget(new SettingsFieldWidget { Title = title, Description = description, Slot = new SwitchWidget { IsActive = value, Callback = changeCallback }, UseReverseLayout = false }, sectionMap[sectionId]); + widgets.AddWidget(new SettingsFieldWidget + { + Title = title, + Description = description, + Slot = new SwitchWidget + { + IsActive = value, + Callback = changeCallback, + RequireConfirmation = requireConfirmation, + ConfirmationTitle = title, + ConfirmationMessage = confirmationMessage + }, + UseReverseLayout = false + }, sectionMap[sectionId]); return this; } diff --git a/src/SociallyDistant.Framework/UI/Recycling/SwitchWidget.cs b/src/SociallyDistant.Framework/UI/Recycling/SwitchWidget.cs index 69660e0e..3cff8243 100755 --- a/src/SociallyDistant.Framework/UI/Recycling/SwitchWidget.cs +++ b/src/SociallyDistant.Framework/UI/Recycling/SwitchWidget.cs @@ -6,9 +6,20 @@ public sealed class SwitchWidget : IWidget { public bool IsActive { get; set; } public Action? Callback { get; set; } + public bool RequireConfirmation { get; set; } + public string? ConfirmationTitle { get; set; } + public string? ConfirmationMessage { get; set; } + public RecyclableWidgetController Build() { - return new SwitchWidgetController() { IsActive = IsActive, Callback = Callback }; + return new SwitchWidgetController() + { + IsActive = IsActive, + Callback = Callback, + RequireConfirmation = this.RequireConfirmation, + ConfirmationTitle = this.ConfirmationTitle, + ConfirmationMessage = this.ConfirmationMessage + }; } } \ No newline at end of file diff --git a/src/SociallyDistant.Framework/UI/Recycling/SwitchWidgetController.cs b/src/SociallyDistant.Framework/UI/Recycling/SwitchWidgetController.cs index 9ae73c0e..e3e096f6 100755 --- a/src/SociallyDistant.Framework/UI/Recycling/SwitchWidgetController.cs +++ b/src/SociallyDistant.Framework/UI/Recycling/SwitchWidgetController.cs @@ -1,5 +1,9 @@ +using AcidicGUI.Common; using AcidicGUI.ListAdapters; using AcidicGUI.Widgets; +using SociallyDistant.Core.Core; +using SociallyDistant.Core.Modules; +using SociallyDistant.Core.Shell.Windowing; namespace SociallyDistant.Core.UI.Recycling; @@ -9,7 +13,9 @@ public sealed class SwitchWidgetController : RecyclableWidgetController public bool IsActive { get; set; } public Action? Callback { get; set; } - + public bool RequireConfirmation { get; set; } + public string? ConfirmationTitle { get; set; } + public string? ConfirmationMessage { get; set; } public override void Build(ContentWidget destination) { @@ -17,22 +23,53 @@ public sealed class SwitchWidgetController : RecyclableWidgetController toggle.UseSwitchVariant = true; toggle.ToggleValue = IsActive; - toggle.OnValueChanged += HandleValueChanged; + toggle.OnBeforeValueChanged += HandleValueChanged; destination.Content = toggle; } - private void HandleValueChanged(bool value) + private void HandleValueChanged(StateChange change) { - Callback?.Invoke(value); + if (!RequireConfirmation) + { + Callback?.Invoke(change.NewValue); + return; + } + + change.Cancel(); + + ConfirmChange(change.NewValue); } public override void Recycle() { if (toggle != null) - toggle.OnValueChanged -= HandleValueChanged; + toggle.OnBeforeValueChanged -= HandleValueChanged; Recyclewidget(toggle); toggle = null; } + + private async void ConfirmChange(bool newValue) + { + var sociallyDistant = Application.Instance.Context; + + var messageDialog = sociallyDistant.Shell.CreateMessageDialog(ConfirmationTitle ?? "Confirm setting change"); + messageDialog.Message = ConfirmationMessage ?? "Are you sure you want to change this setting?"; + messageDialog.MessageType = MessageBoxType.Warning; + + messageDialog.Buttons.Add(new MessageBoxButtonData("Yes", MessageDialogResult.Yes)); + messageDialog.Buttons.Add(new MessageBoxButtonData("No", MessageDialogResult.No)); + + var result = await messageDialog.GetResultAsync(); + + if (result != MessageDialogResult.Yes) + return; + + if (toggle == null) + return; + + toggle.ToggleValue = newValue; + Callback?.Invoke(newValue); + } } \ No newline at end of file diff --git a/src/SociallyDistant.Framework/UI/VisualStyles/SociallyDistantVisualStyle.cs b/src/SociallyDistant.Framework/UI/VisualStyles/SociallyDistantVisualStyle.cs index 90dcb47e..5b652e9f 100755 --- a/src/SociallyDistant.Framework/UI/VisualStyles/SociallyDistantVisualStyle.cs +++ b/src/SociallyDistant.Framework/UI/VisualStyles/SociallyDistantVisualStyle.cs @@ -13,47 +13,66 @@ using SociallyDistant.Core.UI.Common; namespace SociallyDistant.Core.UI.VisualStyles; -public class SociallyDistantVisualStyle : IVisualStyle +public abstract class UserStyle +{ + private bool isDirty = true; + + protected bool GetDirtyState() + { + var wasDirty = isDirty; + isDirty = false; + return wasDirty; + } + + protected void SetDirty() + { + isDirty = true; + } +} + +public class SociallyDistantVisualStyle : IVisualStyle, + IGetLayoutProperties, + IVisualRenderer { private readonly IGameContext game; - private readonly Color mainBackground = new Color(0x11, 0x13, 0x15); - private readonly Color statusBarColor = new Color(0x01, 0x22, 0x37, 0xff); - private readonly Color accentPrimary = new Color(0x13, 0x85, 0xC3, 0xff); - private readonly Color accentEvil = new(0xDE, 0x17, 0x17); - private readonly Color accentWarning = new Color(0xE0, 0x86, 0x17); - private readonly Color accentSuccess = new Color(0x17, 0x82, 0x0E); - private readonly Color accentCyberspace = new Color(0x34, 0xB1, 0xFD); - private readonly Color fieldBackground = new Color(0x25, 0x28, 0x2B); - private readonly Color fieldStroke = new Color(0x60, 0x64, 0x67); - private readonly Color tabInactiveBackgroundDefault = new Color(0x44, 0x44, 0x44, 191); - private readonly Color tabInactiveBorderDefault = new Color(0x44, 0x44, 0x44); - private readonly Color tabActiveBackgroundDefault = new Color(0x16, 0x93, 0xD6, 190); - private readonly Color tabActiveBorderDefault = new Color(0x16, 0x93, 0xD6); - private readonly Color sectionTextColor = new(0x85, 0x85, 0x85); - private readonly Color inputInactiveBorderColor = new Color(0x54, 0x57, 0x5A); - private readonly Color inputInactiveHoveredBorderColor = new Color(0x6F, 0x74, 0x77); - private readonly Color inputActiveBorderColor = new Color(0x19, 0xA1, 0xEA); - private readonly Color inputActiveHoveredBorderColor = new Color(0x80, 0xC3, 0xFD); - private readonly Color inputInactiveBackground = new Color(0x19, 0x1C, 0x1D); - private readonly Color inputInactiveHoveredBackground = new Color(0x2C, 0x2F, 0x32); - private readonly Color inputInactivePressedBackground = new Color(0x1F, 0x22, 0x25); - private readonly Color inputActiveBackground = new Color(0x13, 0x85, 0xC3); - private readonly Color inputActiveHoveredBackground = new Color(0x19, 0xA1, 0xEA); - private readonly Color inputActivePressedBackground = new Color(0x13, 0x85, 0xC3); - private readonly Color buttonBackground = new Color(0x44, 0x44, 0x44); - private readonly Color buttonBorder = new Color(0x16, 0x93, 0xD6); - private readonly Color buttonHoveredBackground = new Color(0x0F, 0x73, 0xA9); - private readonly Color buttonPressedBackground = new Color(0x08, 0x53, 0x7B); - private readonly Color selectionColor = new(0x08, 0x53, 0x7B); - private readonly Color playerBubbleBackground = new Color(0x08, 0x53, 0x7B); + private readonly ProgressBarStyle progressBarSTyle = new(); + private readonly Color mainBackground = new Color(0x11, 0x13, 0x15); + private readonly Color statusBarColor = new Color(0x01, 0x22, 0x37, 0xff); + private readonly Color accentPrimary = new Color(0x13, 0x85, 0xC3, 0xff); + private readonly Color accentEvil = new(0xDE, 0x17, 0x17); + private readonly Color accentWarning = new Color(0xE0, 0x86, 0x17); + private readonly Color accentSuccess = new Color(0x17, 0x82, 0x0E); + private readonly Color accentCyberspace = new Color(0x34, 0xB1, 0xFD); + private readonly Color fieldBackground = new Color(0x25, 0x28, 0x2B); + private readonly Color fieldStroke = new Color(0x60, 0x64, 0x67); + private readonly Color tabInactiveBackgroundDefault = new Color(0x44, 0x44, 0x44, 191); + private readonly Color tabInactiveBorderDefault = new Color(0x44, 0x44, 0x44); + private readonly Color tabActiveBackgroundDefault = new Color(0x16, 0x93, 0xD6, 190); + private readonly Color tabActiveBorderDefault = new Color(0x16, 0x93, 0xD6); + private readonly Color sectionTextColor = new(0x85, 0x85, 0x85); + private readonly Color inputInactiveBorderColor = new Color(0x54, 0x57, 0x5A); + private readonly Color inputInactiveHoveredBorderColor = new Color(0x6F, 0x74, 0x77); + private readonly Color inputActiveBorderColor = new Color(0x19, 0xA1, 0xEA); + private readonly Color inputActiveHoveredBorderColor = new Color(0x80, 0xC3, 0xFD); + private readonly Color inputInactiveBackground = new Color(0x19, 0x1C, 0x1D); + private readonly Color inputInactiveHoveredBackground = new Color(0x2C, 0x2F, 0x32); + private readonly Color inputInactivePressedBackground = new Color(0x1F, 0x22, 0x25); + private readonly Color inputActiveBackground = new Color(0x13, 0x85, 0xC3); + private readonly Color inputActiveHoveredBackground = new Color(0x19, 0xA1, 0xEA); + private readonly Color inputActivePressedBackground = new Color(0x13, 0x85, 0xC3); + private readonly Color buttonBackground = new Color(0x44, 0x44, 0x44); + private readonly Color buttonBorder = new Color(0x16, 0x93, 0xD6); + private readonly Color buttonHoveredBackground = new Color(0x0F, 0x73, 0xA9); + private readonly Color buttonPressedBackground = new Color(0x08, 0x53, 0x7B); + private readonly Color selectionColor = new(0x08, 0x53, 0x7B); + private readonly Color playerBubbleBackground = new Color(0x08, 0x53, 0x7B); private Font iconFont = null!; private IFontFamily defaultFont = null!; private IFontFamily monospace = null!; private Texture2D? checkboxEmblem; - - public int ProgressBarHeight => 16; + public int SliderThickness => 18; public Point ToggleSize => new Point(20, 20); public Point SwitchSize => new Point(40, 22); @@ -540,18 +559,6 @@ public class SociallyDistantVisualStyle : IVisualStyle } } - public void DrawProgressBar(ProgressBar widget, GeometryHelper geometry, float fillPercentage) - { - int width = widget.ContentArea.Width; - int fillWidth = (int) MathHelper.Clamp(MathHelper.Lerp(0, width, fillPercentage), 0, width); - - var background = widget.ContentArea; - var fill = new LayoutRect(background.Left, background.Top, fillWidth, background.Height); - - geometry.AddQuad(background, Color.Gray); - geometry.AddQuad(fill, Color.Red); - } - public void DrawSlider( Slider widget, GeometryHelper geometry, @@ -605,6 +612,56 @@ public class SociallyDistantVisualStyle : IVisualStyle geometry.AddRoundedRectangleOutline(new LayoutRect(nubOffsetX, nubOffsetY, SliderThickness, SliderThickness), thickness, halfThickness, border); } + + public sealed class ProgressBarStyle : UserStyle, + IGetLayoutProperties, + IVisualRenderer + { + private readonly int progressBarHeight = 16; + + public bool GetLayoutProperties(ref ProgressBar.ProgressBarLayoutProperties layoutProperties) + { + layoutProperties.ProgressBarHeight = progressBarHeight; + return GetDirtyState(); + } + + public void Draw( + Widget widget, + GeometryHelper geometry, + in LayoutRect contentRect, + ProgressBar.ProgressBarVisualProperties properties + ) + { + int width = contentRect.Width; + int fillWidth = (int) MathHelper.Clamp(MathHelper.Lerp(0, width, properties.FillPercentage), 0, width); + + var background = contentRect; + var fill = new LayoutRect(background.Left, background.Top, fillWidth, background.Height); + + geometry.AddQuad(background, Color.Gray); + geometry.AddQuad(fill, Color.Red); + } + } + + public bool GetLayoutProperties(ref ProgressBar.ProgressBarLayoutProperties layoutProperties) + { + return progressBarSTyle.GetLayoutProperties(ref layoutProperties); + } + + public void Draw( + Widget widget, + GeometryHelper geometry, + in LayoutRect contentRect, + ProgressBar.ProgressBarVisualProperties properties + ) + { + progressBarSTyle.Draw( + widget, + geometry, + in contentRect, + properties + ); + } } public enum InputFieldStyle diff --git a/src/SociallyDistant/Core/Config/ModdingSettings.cs b/src/SociallyDistant/Core/Config/ModdingSettings.cs index 8d670d25..97ead679 100755 --- a/src/SociallyDistant/Core/Config/ModdingSettings.cs +++ b/src/SociallyDistant/Core/Config/ModdingSettings.cs @@ -1,4 +1,5 @@ using SociallyDistant.Core.Core.Config; +using SociallyDistant.Core.Modules; namespace SociallyDistant.Core.Config { @@ -41,8 +42,14 @@ namespace SociallyDistant.Core.Config "Mod debug mode", "Enable or disable Mod Debug Mode. Mod Debug mode sets up a safe, temporary environment for testing in-development mods with, and doesn't allow you to play regular save files. Changing this setting requires restarting the game.", ModDebugMode, - x => ModDebugMode = x, - development + x => + { + ModDebugMode = x; + Application.Instance.Restart(); + }, + development, + true, + "Changing this setting requires a game restart. Any unsaved progress will be lost." ); } } diff --git a/src/SociallyDistant/GameApplication.cs b/src/SociallyDistant/GameApplication.cs index 1156b9f7..37501f67 100755 --- a/src/SociallyDistant/GameApplication.cs +++ b/src/SociallyDistant/GameApplication.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using Serilog; using SociallyDistant.Core.Config; using SociallyDistant.Core.Config.SystemConfigCategories; @@ -7,14 +8,14 @@ using SociallyDistant.Core.Shell; namespace SociallyDistant; -internal sealed class GameApplication : Application, - IDisposable +internal sealed class GameApplication : Application { private readonly SociallyDistantGame game; private readonly SettingsManager settingsManager = new(); public override IGameContext Context => game; + public bool RestartWasRequested { get; private set; } public ISettingsManager SettingsManager => settingsManager; public GameApplication() @@ -52,8 +53,32 @@ internal sealed class GameApplication : Application, } } - public void Dispose() + protected override void OnRestart() + { + game.Exit(); + + var processPath = Environment.ProcessPath; + var args = Environment.GetCommandLineArgs(); + + if (!string.IsNullOrWhiteSpace(processPath)) + return; + + var startInfo = new ProcessStartInfo(); + startInfo.FileName = processPath; + + foreach (string arg in args) + startInfo.ArgumentList.Add(arg); + + startInfo.UseShellExecute = false; + startInfo.WorkingDirectory = Environment.CurrentDirectory; + + Process.Start(startInfo); + } + + /// + public override void Dispose() { game.Dispose(); + base.Dispose(); } } \ No newline at end of file diff --git a/src/SociallyDistant/SociallyDistantGame.cs b/src/SociallyDistant/SociallyDistantGame.cs index dec62728..30ae48ce 100755 --- a/src/SociallyDistant/SociallyDistantGame.cs +++ b/src/SociallyDistant/SociallyDistantGame.cs @@ -2,6 +2,7 @@ using System.Reactive.Subjects; using System.Text; using AcidicGUI; +using JetBrains.Annotations; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Serilog; @@ -26,6 +27,7 @@ using SociallyDistant.Core.Shell; using SociallyDistant.Core.Shell.Windowing; using SociallyDistant.Core.Social; using SociallyDistant.Core.UI; +using SociallyDistant.Core.UI.Effects; using SociallyDistant.DevTools; using SociallyDistant.GamePlatform; using SociallyDistant.GamePlatform.ContentManagement; @@ -601,7 +603,7 @@ internal sealed class SociallyDistantGame : Log.Logger = new LoggerConfiguration() .WriteTo.Console() .CreateLogger(); - + AppDomain.CurrentDomain.UnhandledException += Fuck; void Fuck(object sender, UnhandledExceptionEventArgs e) @@ -610,15 +612,24 @@ internal sealed class SociallyDistantGame : Log.Fatal(e.ExceptionObject.ToString() ?? "Unknown exception details."); } + restart: + + var restartRequested = false; + try { using var game = new GameApplication(); game.Start(); + + restartRequested = game.RestartWasRequested; } finally { Log.CloseAndFlush(); } + + if (restartRequested) + goto restart; } public static void ScheduleAction(Action action) @@ -692,6 +703,10 @@ internal sealed class SociallyDistantGame : if (explicitApply) { + graphicsManager.PreferredBackBufferWidth = parameters.BackBufferWidth; + graphicsManager.PreferredBackBufferHeight = parameters.BackBufferHeight; + graphicsManager.IsFullScreen = settings.Fullscreen; + graphicsManager.ApplyChanges(); ApplyVirtualDisplayMode(settings); } diff --git a/src/SociallyDistant/UI/Settings/SystemSettingsController.cs b/src/SociallyDistant/UI/Settings/SystemSettingsController.cs index 299445c4..f8f03162 100755 --- a/src/SociallyDistant/UI/Settings/SystemSettingsController.cs +++ b/src/SociallyDistant/UI/Settings/SystemSettingsController.cs @@ -83,7 +83,9 @@ public class SystemSettingsController : { var models = new List(); - foreach (SettingsCategory category in game.SettingsManager.GetCategoriesInSection(sectionTitle)) + foreach (SettingsCategory category in game.SettingsManager.GetCategoriesInSection(sectionTitle) + .OrderByDescending(x=>x.Priority) + .ThenBy(x=> x.Name)) { var model = new SettingsCategoryModel { diff --git a/src/SociallyDistant/UI/Shell/DockIconView.cs b/src/SociallyDistant/UI/Shell/DockIconView.cs index 7c64b8cb..9fab3435 100755 --- a/src/SociallyDistant/UI/Shell/DockIconView.cs +++ b/src/SociallyDistant/UI/Shell/DockIconView.cs @@ -28,7 +28,7 @@ public sealed class DockIconView : Widget protected override void RebuildGeometry(GeometryHelper geometry) { - var color = GetVisualStyle().SelectionColor; + var color = GetVisualStyleOverride().SelectionColor; if (isActive) { diff --git a/src/sociallydistant.sln.DotSettings b/src/sociallydistant.sln.DotSettings index 56959aa4..eb3f095f 100755 --- a/src/sociallydistant.sln.DotSettings +++ b/src/sociallydistant.sln.DotSettings @@ -5,8 +5,13 @@ False False 3 - 10000 + 3 + 3 False + True + CHOP_IF_LONG + True + False CHOP_ALWAYS False @@ -15,7 +20,7 @@ True False True - CHOP_ALWAYS + CHOP_ALWAYS CHOP_ALWAYS True