Viewport improvements (#3765)

This commit is contained in:
Pieter-Jan Briers
2021-04-19 09:52:40 +02:00
committed by GitHub
parent 8e2fc49357
commit 147a54c642
40 changed files with 1173 additions and 418 deletions

View File

@@ -29,11 +29,14 @@ namespace Content.Client.UserInterface
public readonly Button CloseButton;
public readonly Button SaveButton;
public CharacterSetupGui(IEntityManager entityManager,
public CharacterSetupGui(
IEntityManager entityManager,
IResourceCache resourceCache,
IClientPreferencesManager preferencesManager,
IPrototypeManager prototypeManager)
{
AddChild(new ParallaxControl());
_entityManager = entityManager;
_preferencesManager = preferencesManager;
var margin = new Control

View File

@@ -0,0 +1,53 @@
<Control xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cui="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
xmlns:cuic="clr-namespace:Content.Client.UserInterface">
<cuic:ParallaxControl />
<Control HorizontalAlignment="Center" VerticalAlignment="Center">
<PanelContainer StyleClasses="AngleRect" />
<VBoxContainer MinSize="300 200">
<HBoxContainer>
<Label Margin="8 0 0 0" Text="{Loc 'connecting-title'}"
StyleClasses="LabelHeading" VAlign="Center" />
<Button Name="ExitButton" Text="{Loc 'connecting-exit'}"
HorizontalAlignment="Right" HorizontalExpand="True" />
</HBoxContainer>
<cui:HighDivider />
<VBoxContainer VerticalExpand="True" Margin="4 4 4 0">
<Control VerticalExpand="True" Margin="0 0 0 8">
<VBoxContainer Name="ConnectingStatus">
<Label Text="{Loc 'connecting-in-progress'}" Align="Center" />
<!-- Who the fuck named these cont- oh wait I did -->
<Label Name="ConnectStatus" StyleClasses="LabelSubText" Align="Center" />
</VBoxContainer>
<VBoxContainer Name="ConnectFail" Visible="False">
<Label Name="ConnectFailReason" Align="Center" />
<Button Name="RetryButton" Text="{Loc 'connecting-retry'}"
HorizontalAlignment="Center"
VerticalExpand="True" VerticalAlignment="Bottom" />
</VBoxContainer>
<VBoxContainer Name="Disconnected">
<Label Text="{Loc 'connecting-disconnected'}" Align="Center" />
<Label Name="DisconnectReason" Align="Center" />
<Button Name="ReconnectButton" Text="{Loc 'connecting-reconnect'}"
HorizontalAlignment="Center"
VerticalExpand="True" VerticalAlignment="Bottom" />
</VBoxContainer>
</Control>
<Label Name="ConnectingAddress" StyleClasses="LabelSubText" HorizontalAlignment="Center" />
</VBoxContainer>
<PanelContainer>
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#444" ContentMarginTopOverride="2" />
</PanelContainer.PanelOverride>
</PanelContainer>
<HBoxContainer Margin="12 0 4 0" VerticalAlignment="Bottom">
<Label Text="{Loc 'connecting-tip'}" StyleClasses="LabelSubText" />
<Label Text="{Loc 'connecting-version'}" StyleClasses="LabelSubText"
HorizontalAlignment="Right" HorizontalExpand="True" />
</HBoxContainer>
</VBoxContainer>
</Control>
</Control>

View File

@@ -0,0 +1,62 @@
using Content.Client.State;
using Content.Client.UserInterface.Stylesheets;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Network;
namespace Content.Client.UserInterface
{
[GenerateTypedNameReferences]
public sealed partial class LauncherConnectingGui : Control
{
private readonly LauncherConnecting _state;
public LauncherConnectingGui(LauncherConnecting state)
{
_state = state;
RobustXamlLoader.Load(this);
LayoutContainer.SetAnchorPreset(this, LayoutContainer.LayoutPreset.Wide);
Stylesheet = IoCManager.Resolve<IStylesheetManager>().SheetSpace;
ReconnectButton.OnPressed += _ => _state.RetryConnect();
RetryButton.OnPressed += _ => _state.RetryConnect();
ExitButton.OnPressed += _ => _state.Exit();
var addr = state.Address;
if (addr != null)
ConnectingAddress.Text = addr;
state.PageChanged += OnPageChanged;
state.ConnectFailReasonChanged += ConnectFailReasonChanged;
state.ConnectionStateChanged += ConnectionStateChanged;
ConnectionStateChanged(state.ConnectionState);
}
private void ConnectFailReasonChanged(string? reason)
{
ConnectFailReason.Text = reason == null
? null
: Loc.GetString("connecting-fail-reason", ("reason", reason));
}
private void OnPageChanged(LauncherConnecting.Page page)
{
ConnectingStatus.Visible = page == LauncherConnecting.Page.Connecting;
ConnectFail.Visible = page == LauncherConnecting.Page.ConnectFailed;
Disconnected.Visible = page == LauncherConnecting.Page.Disconnected;
}
private void ConnectionStateChanged(ClientConnectionState state)
{
ConnectStatus.Text = Loc.GetString($"connecting-state-{state}");
}
}
}

View File

@@ -7,82 +7,88 @@
xmlns:maths="clr-namespace:Robust.Shared.Maths;assembly=Robust.Shared.Maths"
xmlns:voting="clr-namespace:Content.Client.Voting">
<!-- One day I'll code a Margin property for controls. -->
<MarginContainer MarginBottomOverride="20" MarginLeftOverride="20" MarginRightOverride="20"
MarginTopOverride="20">
<PanelContainer StyleClasses="AngleRect" />
<VBoxContainer>
<!-- Top row -->
<HBoxContainer MinSize="0 40">
<MarginContainer MarginLeftOverride="8">
<Label StyleClasses="LabelHeadingBigger" VAlign="Center" Text="{Loc 'Lobby'}" />
</MarginContainer>
<Label Name="CServerName" StyleClasses="LabelHeadingBigger" VAlign="Center" />
<voting:VoteCallMenuButton Name="CCallVoteButton" StyleClasses="ButtonBig" />
<Button Name="COptionsButton" StyleClasses="ButtonBig" Text="{Loc 'Options'}" />
<Button Name="CLeaveButton" StyleClasses="ButtonBig" Text="{Loc 'Leave'}" />
</HBoxContainer>
<!-- Gold line -->
<PanelContainer>
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="{x:Static style:StyleNano.NanoGold}"
ContentMarginTopOverride="2" />
</PanelContainer.PanelOverride>
</PanelContainer>
<!-- Middle section with the two vertical panels -->
<HBoxContainer VerticalExpand="True">
<!-- Left panel -->
<VBoxContainer Name="CLeftPanelContainer" HorizontalExpand="True">
<cui:StripeBack>
<MarginContainer MarginLeftOverride="3" MarginRightOverride="3" MarginBottomOverride="3"
MarginTopOverride="3">
<HBoxContainer SeparationOverride="6">
<Button Name="CObserveButton" Text="{Loc 'Observe'}" StyleClasses="ButtonBig" />
<Label Name="CStartTime" Align="Right"
FontColorOverride="{x:Static maths:Color.DarkGray}"
StyleClasses="LabelBig" HorizontalExpand="True" />
<Button Name="CReadyButton" ToggleMode="True" Text="{Loc 'Ready Up'}"
StyleClasses="ButtonBig" />
</HBoxContainer>
</MarginContainer>
</cui:StripeBack>
<MarginContainer VerticalExpand="True" MarginLeftOverride="3" MarginRightOverride="3"
MarginBottomOverride="3"
MarginTopOverride="3">
<chat:ChatBox Name="CChat" />
<Control>
<!-- Parallax background -->
<cui:ParallaxControl />
<!-- One day I'll code a Margin property for controls. -->
<MarginContainer MarginBottomOverride="20" MarginLeftOverride="20" MarginRightOverride="20"
MarginTopOverride="20">
<PanelContainer StyleClasses="AngleRect" />
<VBoxContainer>
<!-- Top row -->
<HBoxContainer MinSize="0 40">
<MarginContainer MarginLeftOverride="8">
<Label StyleClasses="LabelHeadingBigger" VAlign="Center" Text="{Loc 'Lobby'}" />
</MarginContainer>
</VBoxContainer>
<Label Name="CServerName" StyleClasses="LabelHeadingBigger" VAlign="Center" />
<voting:VoteCallMenuButton Name="CCallVoteButton" StyleClasses="ButtonBig" />
<Button Name="COptionsButton" StyleClasses="ButtonBig" Text="{Loc 'Options'}" />
<Button Name="CLeaveButton" StyleClasses="ButtonBig" Text="{Loc 'Leave'}" />
</HBoxContainer>
<!-- Gold line -->
<PanelContainer MinSize="2 0">
<PanelContainer>
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="{x:Static style:StyleNano.NanoGold}" />
<gfx:StyleBoxFlat BackgroundColor="{x:Static style:StyleNano.NanoGold}"
ContentMarginTopOverride="2" />
</PanelContainer.PanelOverride>
</PanelContainer>
<!-- Right panel -->
<Control HorizontalExpand="True">
<VBoxContainer>
<!-- Player list -->
<cui:NanoHeading Text="{Loc 'Online Players'}" />
<MarginContainer VerticalExpand="True"
MarginRightOverride="3" MarginLeftOverride="3"
MarginBottomOverride="3" MarginTopOverride="3">
<cui:LobbyPlayerList Name="COnlinePlayerList"
HorizontalExpand="True"
VerticalExpand="True" />
</MarginContainer>
<!-- Server info -->
<cui:NanoHeading Text="{Loc 'Server Info'}" />
<MarginContainer VerticalExpand="True"
MarginRightOverride="3" MarginLeftOverride="3"
MarginBottomOverride="2" MarginTopOverride="3">
<cui:ServerInfo Name="CServerInfo" />
<!-- Middle section with the two vertical panels -->
<HBoxContainer VerticalExpand="True">
<!-- Left panel -->
<VBoxContainer Name="CLeftPanelContainer" HorizontalExpand="True">
<cui:StripeBack>
<MarginContainer MarginLeftOverride="3" MarginRightOverride="3" MarginBottomOverride="3"
MarginTopOverride="3">
<HBoxContainer SeparationOverride="6">
<Button Name="CObserveButton" Text="{Loc 'Observe'}" StyleClasses="ButtonBig" />
<Label Name="CStartTime" Align="Right"
FontColorOverride="{x:Static maths:Color.DarkGray}"
StyleClasses="LabelBig" HorizontalExpand="True" />
<Button Name="CReadyButton" ToggleMode="True" Text="{Loc 'Ready Up'}"
StyleClasses="ButtonBig" />
</HBoxContainer>
</MarginContainer>
</cui:StripeBack>
<MarginContainer VerticalExpand="True" MarginLeftOverride="3" MarginRightOverride="3"
MarginBottomOverride="3"
MarginTopOverride="3">
<chat:ChatBox Name="CChat" />
</MarginContainer>
</VBoxContainer>
<MarginContainer SizeFlagsHorizontal="ShrinkEnd" MarginTopOverride="8" MarginRightOverride="8">
<VBoxContainer Name="CVoteContainer" />
</MarginContainer>
</Control>
</HBoxContainer>
</VBoxContainer>
</MarginContainer>
<!-- Gold line -->
<PanelContainer MinSize="2 0">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="{x:Static style:StyleNano.NanoGold}" />
</PanelContainer.PanelOverride>
</PanelContainer>
<!-- Right panel -->
<Control HorizontalExpand="True">
<VBoxContainer>
<!-- Player list -->
<cui:NanoHeading Text="{Loc 'Online Players'}" />
<MarginContainer VerticalExpand="True"
MarginRightOverride="3" MarginLeftOverride="3"
MarginBottomOverride="3" MarginTopOverride="3">
<cui:LobbyPlayerList Name="COnlinePlayerList"
HorizontalExpand="True"
VerticalExpand="True" />
</MarginContainer>
<!-- Server info -->
<cui:NanoHeading Text="{Loc 'Server Info'}" />
<MarginContainer VerticalExpand="True"
MarginRightOverride="3" MarginLeftOverride="3"
MarginBottomOverride="2" MarginTopOverride="3">
<cui:ServerInfo Name="CServerInfo" />
</MarginContainer>
</VBoxContainer>
<MarginContainer SizeFlagsHorizontal="ShrinkEnd" MarginTopOverride="8" MarginRightOverride="8">
<VBoxContainer Name="CVoteContainer" />
</MarginContainer>
</Control>
</HBoxContainer>
</VBoxContainer>
</MarginContainer>
</Control>
</Control>

View File

@@ -0,0 +1,153 @@
using System;
using Content.Shared;
using Robust.Client.UserInterface;
using Robust.Shared.Configuration;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
namespace Content.Client.UserInterface
{
/// <summary>
/// Wrapper for <see cref="ScalingViewport"/> that listens to configuration variables.
/// Also does NN-snapping within tolerances.
/// </summary>
public sealed class MainViewport : Control
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly ViewportManager _vpManager = default!;
public ScalingViewport Viewport { get; }
public MainViewport()
{
IoCManager.InjectDependencies(this);
Viewport = new ScalingViewport
{
AlwaysRender = true,
RenderScaleMode = ScalingViewportRenderScaleMode.CeilInt,
MouseFilter = MouseFilterMode.Stop
};
AddChild(Viewport);
}
protected override void EnteredTree()
{
base.EnteredTree();
_vpManager.AddViewport(this);
}
protected override void ExitedTree()
{
base.ExitedTree();
_vpManager.RemoveViewport(this);
}
public void UpdateCfg()
{
var stretch = _cfg.GetCVar(CCVars.ViewportStretch);
var renderScaleUp = _cfg.GetCVar(CCVars.ViewportScaleRender);
var fixedFactor = _cfg.GetCVar(CCVars.ViewportFixedScaleFactor);
if (stretch)
{
var snapFactor = CalcSnappingFactor();
if (snapFactor == null)
{
// Did not find a snap, enable stretching.
Viewport.FixedStretchSize = null;
Viewport.StretchMode = ScalingViewportStretchMode.Bilinear;
if (renderScaleUp)
{
Viewport.RenderScaleMode = ScalingViewportRenderScaleMode.CeilInt;
}
else
{
Viewport.RenderScaleMode = ScalingViewportRenderScaleMode.Fixed;
Viewport.FixedRenderScale = 1;
}
return;
}
// Found snap, set fixed factor and run non-stretching code.
fixedFactor = snapFactor.Value;
}
Viewport.FixedStretchSize = Viewport.ViewportSize * fixedFactor;
Viewport.StretchMode = ScalingViewportStretchMode.Nearest;
if (renderScaleUp)
{
Viewport.RenderScaleMode = ScalingViewportRenderScaleMode.Fixed;
Viewport.FixedRenderScale = fixedFactor;
}
else
{
// Snapping but forced to render scale at scale 1 so...
// At least we can NN.
Viewport.RenderScaleMode = ScalingViewportRenderScaleMode.Fixed;
Viewport.FixedRenderScale = 1;
}
}
private int? CalcSnappingFactor()
{
// Margin tolerance is tolerance of "the window is too big"
// where we add a margin to the viewport to make it fit.
var cfgToleranceMargin = _cfg.GetCVar(CCVars.ViewportSnapToleranceMargin);
// Clip tolerance is tolerance of "the window is too small"
// where we are clipping the viewport to make it fit.
var cfgToleranceClip = _cfg.GetCVar(CCVars.ViewportSnapToleranceClip);
// Calculate if the viewport, when rendered at an integer scale,
// is close enough to the control size to enable "snapping" to NN,
// potentially cutting a tiny bit off/leaving a margin.
//
// Idea here is that if you maximize the window at 1080p or 1440p
// we are close enough to an integer scale (2x and 3x resp) that we should "snap" to it.
// Just do it iteratively.
// I'm sure there's a smarter approach that needs one try with math but I'm dumb.
for (var i = 1; i <= 10; i++)
{
var toleranceMargin = i * cfgToleranceMargin;
var toleranceClip = i * cfgToleranceClip;
var scaled = (Vector2) Viewport.ViewportSize * i;
var (dx, dy) = PixelSize - scaled;
// The rule for which snap fits is that at LEAST one axis needs to be in the tolerance size wise.
// One axis MAY be larger but not smaller than tolerance.
// Obviously if it's too small it's bad, and if it's too big on both axis we should stretch up.
if (Fits(dx) && Fits(dy) || Fits(dx) && Larger(dy) || Larger(dx) && Fits(dy))
{
// Found snap that fits.
return i;
}
bool Larger(float a)
{
return a > toleranceMargin;
}
bool Fits(float a)
{
return a <= toleranceMargin && a >= -toleranceClip;
}
}
return null;
}
protected override void Resized()
{
base.Resized();
UpdateCfg();
}
}
}

View File

@@ -1,14 +1,10 @@
using System;
using Content.Client.GameObjects.Components.HUD.Inventory;
using Content.Shared;
using Content.Shared.Prototypes.HUD;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
@@ -39,6 +35,11 @@ namespace Content.Client.UserInterface
private readonly OptionButton LightingPresetOption;
private readonly OptionButton _uiScaleOption;
private readonly OptionButton _hudThemeOption;
private readonly CheckBox _viewportStretchCheckBox;
private readonly CheckBox _viewportLowResCheckBox;
private readonly Slider _viewportScaleSlider;
private readonly Control _viewportScaleBox;
private readonly Label _viewportScaleText;
public GraphicsControl(IConfigurationManager cfg, IPrototypeManager proMan)
{
@@ -121,12 +122,55 @@ namespace Content.Client.UserInterface
}
});
contents.AddChild(new Placeholder()
_viewportStretchCheckBox = new CheckBox
{
VerticalExpand = true,
PlaceholderText = Loc.GetString("ui-options-placeholder-viewport")
Text = Loc.GetString("ui-options-vp-stretch")
};
_viewportStretchCheckBox.OnToggled += _ =>
{
UpdateViewportScale();
UpdateApplyButton();
};
_viewportScaleSlider = new Slider
{
MinValue = 1,
MaxValue = 5,
Rounded = true,
MinWidth = 200
};
_viewportScaleSlider.OnValueChanged += _ =>
{
UpdateApplyButton();
UpdateViewportScale();
};
_viewportLowResCheckBox = new CheckBox { Text = Loc.GetString("ui-options-vp-low-res")};
_viewportLowResCheckBox.OnToggled += OnCheckBoxToggled;
contents.AddChild(new HBoxContainer
{
Children =
{
_viewportStretchCheckBox,
(_viewportScaleBox = new HBoxContainer
{
Children =
{
(_viewportScaleText = new Label
{
Margin = new Thickness(8, 0)
}),
_viewportScaleSlider,
}
})
}
});
contents.AddChild(_viewportLowResCheckBox);
vBox.AddChild(contents);
vBox.AddChild(new StripeBack
@@ -145,6 +189,12 @@ namespace Content.Client.UserInterface
LightingPresetOption.SelectId(GetConfigLightingQuality());
_uiScaleOption.SelectId(GetConfigUIScalePreset(ConfigUIScale));
_hudThemeOption.SelectId(_cfg.GetCVar(CCVars.HudTheme));
_viewportScaleSlider.Value = _cfg.GetCVar(CCVars.ViewportFixedScaleFactor);
_viewportStretchCheckBox.Pressed = _cfg.GetCVar(CCVars.ViewportStretch);
_viewportLowResCheckBox.Pressed = !_cfg.GetCVar(CCVars.ViewportScaleRender);
UpdateViewportScale();
UpdateApplyButton();
AddChild(vBox);
}
@@ -173,6 +223,9 @@ namespace Content.Client.UserInterface
_cfg.SetCVar(CVars.DisplayWindowMode,
(int) (FullscreenCheckBox.Pressed ? WindowMode.Fullscreen : WindowMode.Windowed));
_cfg.SetCVar(CVars.DisplayUIScale, UIScaleOptions[_uiScaleOption.SelectedId]);
_cfg.SetCVar(CCVars.ViewportStretch, _viewportStretchCheckBox.Pressed);
_cfg.SetCVar(CCVars.ViewportFixedScaleFactor, (int) _viewportScaleSlider.Value);
_cfg.SetCVar(CCVars.ViewportScaleRender, !_viewportLowResCheckBox.Pressed);
_cfg.SaveToFile();
UpdateApplyButton();
}
@@ -195,8 +248,18 @@ namespace Content.Client.UserInterface
var isLightingQualitySame = LightingPresetOption.SelectedId == GetConfigLightingQuality();
var isHudThemeSame = _hudThemeOption.SelectedId == _cfg.GetCVar(CCVars.HudTheme);
var isUIScaleSame = MathHelper.CloseTo(UIScaleOptions[_uiScaleOption.SelectedId], ConfigUIScale);
ApplyButton.Disabled = isVSyncSame && isFullscreenSame && isLightingQualitySame && isHudThemeSame &&
isUIScaleSame;
var isVPStretchSame = _viewportStretchCheckBox.Pressed == _cfg.GetCVar(CCVars.ViewportStretch);
var isVPScaleSame = (int) _viewportScaleSlider.Value == _cfg.GetCVar(CCVars.ViewportFixedScaleFactor);
var isVPResSame = _viewportLowResCheckBox.Pressed == !_cfg.GetCVar(CCVars.ViewportScaleRender);
ApplyButton.Disabled = isVSyncSame &&
isFullscreenSame &&
isLightingQualitySame &&
isUIScaleSame &&
isVPStretchSame &&
isVPScaleSame &&
isVPResSame &&
isHudThemeSame;
}
private bool ConfigIsFullscreen =>
@@ -261,6 +324,12 @@ namespace Content.Client.UserInterface
return 0;
}
private void UpdateViewportScale()
{
_viewportScaleBox.Visible = !_viewportStretchCheckBox.Pressed;
_viewportScaleText.Text = Loc.GetString("ui-options-vp-scale", ("scale", _viewportScaleSlider.Value));
}
}
}
}

View File

@@ -0,0 +1,47 @@
using Content.Client.Interfaces.Parallax;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Random;
using Robust.Shared.ViewVariables;
namespace Content.Client.UserInterface
{
/// <summary>
/// Renders the parallax background as a UI control.
/// </summary>
public sealed class ParallaxControl : Control
{
[Dependency] private readonly IParallaxManager _parallaxManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[ViewVariables(VVAccess.ReadWrite)] public Vector2i Offset { get; set; }
public ParallaxControl()
{
IoCManager.InjectDependencies(this);
Offset = (_random.Next(0, 1000), _random.Next(0, 1000));
RectClipContent = true;
}
protected override void Draw(DrawingHandleScreen handle)
{
var tex = _parallaxManager.ParallaxTexture;
if (tex == null)
return;
var size = tex.Size;
var ourSize = PixelSize;
for (var x = -size.X + Offset.X; x < ourSize.X; x += size.X)
{
for (var y = -size.Y + Offset.Y; y < ourSize.Y; y += size.Y)
{
handle.DrawTexture(tex, (x, y));
}
}
}
}
}

View File

@@ -0,0 +1,340 @@
using System;
using System.Collections.Generic;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
using SixLabors.ImageSharp.PixelFormats;
namespace Content.Client.UserInterface
{
/// <summary>
/// Viewport control that has a fixed viewport size and scales it appropriately.
/// </summary>
public sealed class ScalingViewport : Control, IViewportControl
{
[Dependency] private readonly IClyde _clyde = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
// Internal viewport creation is deferred.
private IClydeViewport? _viewport;
private IEye? _eye;
private Vector2i _viewportSize;
private int _curRenderScale;
private ScalingViewportStretchMode _stretchMode = ScalingViewportStretchMode.Bilinear;
private ScalingViewportRenderScaleMode _renderScaleMode = ScalingViewportRenderScaleMode.Fixed;
private int _fixedRenderScale = 1;
private readonly List<CopyPixelsDelegate<Rgba32>> _queuedScreenshots = new();
public int CurrentRenderScale => _curRenderScale;
/// <summary>
/// The eye to render.
/// </summary>
public IEye? Eye
{
get => _eye;
set
{
_eye = value;
if (_viewport != null)
_viewport.Eye = value;
}
}
/// <summary>
/// The size, in unscaled pixels, of the internal viewport.
/// </summary>
/// <remarks>
/// The actual viewport may have render scaling applied based on parameters.
/// </remarks>
public Vector2i ViewportSize
{
get => _viewportSize;
set
{
_viewportSize = value;
InvalidateViewport();
}
}
// Do not need to InvalidateViewport() since it doesn't affect viewport creation.
[ViewVariables(VVAccess.ReadWrite)] public Vector2i? FixedStretchSize { get; set; }
[ViewVariables(VVAccess.ReadWrite)]
public ScalingViewportStretchMode StretchMode
{
get => _stretchMode;
set
{
_stretchMode = value;
InvalidateViewport();
}
}
[ViewVariables(VVAccess.ReadWrite)]
public ScalingViewportRenderScaleMode RenderScaleMode
{
get => _renderScaleMode;
set
{
_renderScaleMode = value;
InvalidateViewport();
}
}
[ViewVariables(VVAccess.ReadWrite)]
public int FixedRenderScale
{
get => _fixedRenderScale;
set
{
_fixedRenderScale = value;
InvalidateViewport();
}
}
public ScalingViewport()
{
IoCManager.InjectDependencies(this);
RectClipContent = true;
}
protected override void KeyBindDown(GUIBoundKeyEventArgs args)
{
base.KeyBindDown(args);
if (args.Handled)
return;
_inputManager.ViewportKeyEvent(this, args);
}
protected override void KeyBindUp(GUIBoundKeyEventArgs args)
{
base.KeyBindUp(args);
if (args.Handled)
return;
_inputManager.ViewportKeyEvent(this, args);
}
protected override void FrameUpdate(FrameEventArgs args)
{
EnsureViewportCreated();
}
protected override void Draw(DrawingHandleScreen handle)
{
DebugTools.AssertNotNull(_viewport);
_viewport!.Render();
if (_queuedScreenshots.Count != 0)
{
var callbacks = _queuedScreenshots.ToArray();
_viewport.RenderTarget.CopyPixelsToMemory<Rgba32>(image =>
{
foreach (var callback in callbacks)
{
callback(image);
}
});
_queuedScreenshots.Clear();
}
var drawBox = GetDrawBox();
var drawBoxGlobal = drawBox.Translated(GlobalPixelPosition);
_viewport.RenderScreenOverlaysBelow(handle, this, drawBoxGlobal);
handle.DrawTextureRect(_viewport.RenderTarget.Texture, drawBox);
_viewport.RenderScreenOverlaysAbove(handle, this, drawBoxGlobal);
}
public void Screenshot(CopyPixelsDelegate<Rgba32> callback)
{
_queuedScreenshots.Add(callback);
}
// Draw box in pixel coords to draw the viewport at.
private UIBox2i GetDrawBox()
{
DebugTools.AssertNotNull(_viewport);
var vpSize = _viewport!.Size;
var ourSize = (Vector2) PixelSize;
if (FixedStretchSize == null)
{
var (ratioX, ratioY) = ourSize / vpSize;
var ratio = Math.Min(ratioX, ratioY);
var size = vpSize * ratio;
// Size
var pos = (ourSize - size) / 2;
return (UIBox2i) UIBox2.FromDimensions(pos, size);
}
else
{
// Center only, no scaling.
var pos = (ourSize - FixedStretchSize.Value) / 2;
return (UIBox2i) UIBox2.FromDimensions(pos, FixedStretchSize.Value);
}
}
private void RegenerateViewport()
{
DebugTools.AssertNull(_viewport);
var vpSizeBase = ViewportSize;
var ourSize = PixelSize;
var (ratioX, ratioY) = ourSize / (Vector2) vpSizeBase;
var ratio = Math.Min(ratioX, ratioY);
var renderScale = 1;
switch (_renderScaleMode)
{
case ScalingViewportRenderScaleMode.CeilInt:
renderScale = (int) Math.Ceiling(ratio);
break;
case ScalingViewportRenderScaleMode.FloorInt:
renderScale = (int) Math.Floor(ratio);
break;
case ScalingViewportRenderScaleMode.Fixed:
renderScale = _fixedRenderScale;
break;
}
// Always has to be at least one to avoid passing 0,0 to the viewport constructor
renderScale = Math.Max(1, renderScale);
_curRenderScale = renderScale;
_viewport = _clyde.CreateViewport(
ViewportSize * renderScale,
new TextureSampleParameters
{
Filter = StretchMode == ScalingViewportStretchMode.Bilinear,
});
_viewport.RenderScale = (renderScale, renderScale);
_viewport.Eye = _eye;
}
protected override void Resized()
{
base.Resized();
InvalidateViewport();
}
private void InvalidateViewport()
{
_viewport?.Dispose();
_viewport = null;
}
public MapCoordinates ScreenToMap(Vector2 coords)
{
if (_eye == null)
return default;
EnsureViewportCreated();
var matrix = Matrix3.Invert(LocalToScreenMatrix());
return _viewport!.LocalToWorld(matrix.Transform(coords));
}
public Vector2 WorldToScreen(Vector2 map)
{
if (_eye == null)
return default;
EnsureViewportCreated();
var vpLocal = _viewport!.WorldToLocal(map);
var matrix = LocalToScreenMatrix();
return matrix.Transform(vpLocal);
}
private Matrix3 LocalToScreenMatrix()
{
DebugTools.AssertNotNull(_viewport);
var drawBox = GetDrawBox();
var scaleFactor = drawBox.Size / (Vector2) _viewport!.Size;
if (scaleFactor == (0, 0))
// Basically a nonsense scenario, at least make sure to return something that can be inverted.
return Matrix3.Identity;
var scale = Matrix3.CreateScale(scaleFactor);
var translate = Matrix3.CreateTranslation(GlobalPixelPosition + drawBox.TopLeft);
return scale * translate;
}
private void EnsureViewportCreated()
{
if (_viewport == null)
{
RegenerateViewport();
}
DebugTools.AssertNotNull(_viewport);
}
}
/// <summary>
/// Defines how the viewport is stretched if it does not match the size of the control perfectly.
/// </summary>
public enum ScalingViewportStretchMode
{
/// <summary>
/// Bilinear sampling is used.
/// </summary>
Bilinear = 0,
/// <summary>
/// Nearest neighbor sampling is used.
/// </summary>
Nearest,
}
/// <summary>
/// Defines how the base render scale of the viewport is selected.
/// </summary>
public enum ScalingViewportRenderScaleMode
{
/// <summary>
/// <see cref="ScalingViewport.FixedRenderScale"/> is used.
/// </summary>
Fixed = 0,
/// <summary>
/// Floor to the closest integer scale possible.
/// </summary>
FloorInt,
/// <summary>
/// Ceiling to the closest integer scale possible.
/// </summary>
CeilInt
}
}

View File

@@ -0,0 +1,15 @@
using Robust.Client.UserInterface.CustomControls;
namespace Content.Client.UserInterface
{
public static class ViewportExt
{
public static int GetRenderScale(this IViewportControl viewport)
{
if (viewport is ScalingViewport svp)
return svp.CurrentRenderScale;
return 1;
}
}
}

View File

@@ -0,0 +1,42 @@
using System.Collections.Generic;
using Content.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.IoC;
namespace Content.Client.UserInterface
{
/// <summary>
/// Event proxy for <see cref="MainViewport"/> to listen to config events.
/// </summary>
// ReSharper disable once ClassNeverInstantiated.Global
public sealed class ViewportManager
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
private readonly List<MainViewport> _viewports = new();
public void Initialize()
{
_cfg.OnValueChanged(CCVars.ViewportStretch, _ => UpdateCfg());
_cfg.OnValueChanged(CCVars.ViewportSnapToleranceClip, _ => UpdateCfg());
_cfg.OnValueChanged(CCVars.ViewportSnapToleranceMargin, _ => UpdateCfg());
_cfg.OnValueChanged(CCVars.ViewportScaleRender, _ => UpdateCfg());
_cfg.OnValueChanged(CCVars.ViewportFixedScaleFactor, _ => UpdateCfg());
}
private void UpdateCfg()
{
_viewports.ForEach(v => v.UpdateCfg());
}
public void AddViewport(MainViewport vp)
{
_viewports.Add(vp);
}
public void RemoveViewport(MainViewport vp)
{
_viewports.Remove(vp);
}
}
}