diff --git a/Source/Editor/Windows/GameWindow.cs b/Source/Editor/Windows/GameWindow.cs index 8084d638e..9054678a4 100644 --- a/Source/Editor/Windows/GameWindow.cs +++ b/Source/Editor/Windows/GameWindow.cs @@ -8,6 +8,7 @@ using FlaxEditor.GUI.Input; using FlaxEditor.Options; using FlaxEngine; using FlaxEngine.GUI; +using FlaxEngine.Json; namespace FlaxEditor.Windows { @@ -27,6 +28,14 @@ namespace FlaxEditor.Windows private GUI.Docking.DockState _maximizeRestoreDockState; private GUI.Docking.DockPanel _maximizeRestoreDockTo; private CursorLockMode _cursorLockMode = CursorLockMode.None; + + // Viewport scaling variables + private List _defaultViewportScaling = new List(); + private List _customViewportScaling = new List(); + private float _viewportAspectRatio = 1; + private float _windowAspectRatio = 1; + private bool _useAspect = false; + private bool _freeAspect = true; /// /// Gets the viewport. @@ -106,6 +115,35 @@ namespace FlaxEditor.Windows /// public bool FocusOnPlay { get; set; } + private enum ViewportScaleType + { + Resolution = 0, + Aspect = 1, + } + + private class ViewportScaleOptions + { + /// + /// The name. + /// + public string Label; + + /// + /// The Type of scaling to do. + /// + public ViewportScaleType ScaleType; + + /// + /// The width and height to scale by. + /// + public Int2 Size; + + /// + /// If the scaling is active. + /// + public bool Active; + } + private class GameRoot : ContainerControl { public bool EnableEvents => !Time.GamePaused; @@ -255,6 +293,12 @@ namespace FlaxEditor.Windows Parent = _viewport }; RootControl.GameRoot = _guiRoot; + + SizeChanged += control => + { + ResizeViewport(); + }; + Editor.StateMachine.PlayingState.SceneDuplicating += PlayingStateOnSceneDuplicating; Editor.StateMachine.PlayingState.SceneRestored += PlayingStateOnSceneRestored; @@ -267,6 +311,85 @@ namespace FlaxEditor.Windows InputActions.Add(options => options.StepFrame, Editor.Simulation.RequestPlayOneFrame); } + private void ChangeViewportRatio(ViewportScaleOptions v) + { + if (v == null) + return; + + if (v.Size.Y <= 0 || v.Size.X <= 0) + { + return; + } + + if (string.Equals(v.Label, "Free Aspect") && v.Size == new Int2(1,1)) + { + _freeAspect = true; + _useAspect = true; + } + else + { + switch (v.ScaleType) + { + case ViewportScaleType.Aspect: + _useAspect = true; + _freeAspect = false; + break; + case ViewportScaleType.Resolution: + _useAspect = false; + _freeAspect = false; + break; + } + } + + _viewportAspectRatio = (float)v.Size.X / v.Size.Y; + + if (!_freeAspect) + { + if (!_useAspect) + { + _viewport.KeepAspectRatio = true; + _viewport.CustomResolution = new Int2(v.Size.X, v.Size.Y); + } + else + { + _viewport.CustomResolution = new Int2?(); + _viewport.KeepAspectRatio = true; + } + } + else + { + _viewport.CustomResolution = new Int2?(); + _viewport.KeepAspectRatio = false; + } + + ResizeViewport(); + } + + private void ResizeViewport() + { + if (!_freeAspect) + { + _windowAspectRatio = Width / Height; + } + else + { + _windowAspectRatio = 1; + } + + var scaleWidth = _viewportAspectRatio / _windowAspectRatio; + var scaleHeight = _windowAspectRatio / _viewportAspectRatio; + + if (scaleHeight < 1) + { + _viewport.Bounds = new Rectangle(0, Height * (1 - scaleHeight) / 2, Width, Height * scaleHeight); + } + else + { + _viewport.Bounds = new Rectangle(Width * (1 - scaleWidth) / 2, 0, Width * scaleWidth, Height); + } + PerformLayout(); + } + private void OnPostRender(GPUContext context, ref RenderContext renderContext) { // Debug Draw shapes @@ -372,6 +495,53 @@ namespace FlaxEditor.Windows resolutionValue.ValueChanged += () => _viewport.ResolutionScale = resolutionValue.Value; } + // Viewport aspect ratio + { + // Create default scaling options if they dont exist from deserialization. + if (_defaultViewportScaling.Count == 0) + { + _defaultViewportScaling.Add(new ViewportScaleOptions + { + Label = "Free Aspect", + ScaleType = ViewportScaleType.Aspect, + Size = new Int2(1,1), + Active = true, + }); + _defaultViewportScaling.Add(new ViewportScaleOptions + { + Label = "16:9 Aspect", + ScaleType = ViewportScaleType.Aspect, + Size = new Int2(16,9), + Active = false, + }); + _defaultViewportScaling.Add(new ViewportScaleOptions + { + Label = "16:10 Aspect", + ScaleType = ViewportScaleType.Aspect, + Size = new Int2(16,10), + Active = false, + }); + _defaultViewportScaling.Add(new ViewportScaleOptions + { + Label = "1920x1080 Resolution", + ScaleType = ViewportScaleType.Resolution, + Size = new Int2(1920,1080), + Active = false, + }); + _defaultViewportScaling.Add(new ViewportScaleOptions + { + Label = "2560x1440 Resolution", + ScaleType = ViewportScaleType.Resolution, + Size = new Int2(2560,1440), + Active = false, + }); + } + + var vsMenu = menu.AddChildMenu("Viewport Size").ContextMenu; + + CreateViewportSizingContextMenu(vsMenu); + } + // Take Screenshot { var takeScreenshot = menu.AddButton("Take Screenshot"); @@ -400,6 +570,241 @@ namespace FlaxEditor.Windows menu.AddSeparator(); } + private void CreateViewportSizingContextMenu(ContextMenu vsMenu) + { + // add default viewport sizing options + for (int i = 0; i < _defaultViewportScaling.Count; i++) + { + var button = vsMenu.AddButton(_defaultViewportScaling[i].Label); + button.CloseMenuOnClick = false; + button.Icon = _defaultViewportScaling[i].Active ? Style.Current.CheckBoxTick : SpriteHandle.Invalid; + button.Tag = _defaultViewportScaling[i]; + if (_defaultViewportScaling[i].Active) + ChangeViewportRatio(_defaultViewportScaling[i]); + + button.Clicked += () => + { + if (button.Tag == null) + return; + + // Reset selected icon on all buttons + foreach (var child in vsMenu.Items) + { + if (child is ContextMenuButton cmb) + { + var v = (ViewportScaleOptions)cmb.Tag; + if (cmb == button) + { + v.Active = true; + button.Icon = Style.Current.CheckBoxTick; + ChangeViewportRatio(v); + } + else if (v.Active) + { + cmb.Icon = SpriteHandle.Invalid; + v.Active = false; + } + } + } + }; + } + vsMenu.AddSeparator(); + + // Add custom viewport options + for (int i = 0; i < _customViewportScaling.Count; i++) + { + var childCM = vsMenu.AddChildMenu(_customViewportScaling[i].Label); + childCM.CloseMenuOnClick = false; + childCM.Icon = _customViewportScaling[i].Active ? Style.Current.CheckBoxTick : SpriteHandle.Invalid; + childCM.Tag = _customViewportScaling[i]; + if (_customViewportScaling[i].Active) + ChangeViewportRatio(_customViewportScaling[i]); + var applyButton = childCM.ContextMenu.AddButton("Apply"); + applyButton.Tag = childCM.Tag = _customViewportScaling[i]; + applyButton.CloseMenuOnClick = false; + applyButton.Clicked += () => + { + if (childCM.Tag == null) + return; + + // Reset selected icon on all buttons + foreach (var child in vsMenu.Items) + { + if (child is ContextMenuButton cmb) + { + var v = (ViewportScaleOptions)child.Tag; + if (child == childCM) + { + v.Active = true; + childCM.Icon = Style.Current.CheckBoxTick; + ChangeViewportRatio(v); + } + else if (v.Active) + { + cmb.Icon = SpriteHandle.Invalid; + v.Active = false; + } + } + } + }; + + var deleteButton = childCM.ContextMenu.AddButton("Delete"); + deleteButton.CloseMenuOnClick = false; + deleteButton.Clicked += () => + { + if (childCM.Tag == null) + return; + + var v = (ViewportScaleOptions)childCM.Tag; + if (v.Active) + { + v.Active = false; + _defaultViewportScaling[0].Active = true; + ChangeViewportRatio(_defaultViewportScaling[0]); + } + _customViewportScaling.Remove(v); + vsMenu.DisposeAllItems(); + CreateViewportSizingContextMenu(vsMenu); + vsMenu.PerformLayout(); + }; + } + + vsMenu.AddSeparator(); + + // Add button + var add = vsMenu.AddButton("Add..."); + add.CloseMenuOnClick = false; + add.Clicked += () => + { + var popup = new ContextMenuBase + { + Size = new Float2(230, 125), + ClipChildren = false, + CullChildren = false, + }; + popup.Show(add, new Float2(add.Width, 0)); + + var nameLabel = new Label + { + Parent = popup, + AnchorPreset = AnchorPresets.TopLeft, + Text = "Name", + HorizontalAlignment = TextAlignment.Near, + }; + nameLabel.LocalX += 10; + nameLabel.LocalY += 10; + + var nameTextBox = new TextBox + { + Parent = popup, + AnchorPreset = AnchorPresets.TopLeft, + IsMultiline = false, + }; + nameTextBox.LocalX += 100; + nameTextBox.LocalY += 10; + + var typeLabel = new Label + { + Parent = popup, + AnchorPreset = AnchorPresets.TopLeft, + Text = "Type", + HorizontalAlignment = TextAlignment.Near, + }; + typeLabel.LocalX += 10; + typeLabel.LocalY += 35; + + var typeDropdown = new Dropdown + { + Parent = popup, + AnchorPreset = AnchorPresets.TopLeft, + Items = { "Aspect", "Resolution" }, + SelectedItem = "Aspect", + Width = nameTextBox.Width + }; + typeDropdown.LocalY += 35; + typeDropdown.LocalX += 100; + + var whLabel = new Label + { + Parent = popup, + AnchorPreset = AnchorPresets.TopLeft, + Text = "Width & Height", + HorizontalAlignment = TextAlignment.Near, + }; + whLabel.LocalX += 10; + whLabel.LocalY += 60; + + var wValue = new IntValueBox(16) + { + Parent = popup, + AnchorPreset = AnchorPresets.TopLeft, + MinValue = 1, + Width = 55, + }; + wValue.LocalY += 60; + wValue.LocalX += 100; + + var hValue = new IntValueBox(9) + { + Parent = popup, + AnchorPreset = AnchorPresets.TopLeft, + MinValue = 1, + Width = 55, + }; + hValue.LocalY += 60; + hValue.LocalX += 165; + + var submitButton = new Button + { + Parent = popup, + AnchorPreset = AnchorPresets.TopLeft, + Text = "Submit", + Width = 70, + }; + submitButton.LocalX += 40; + submitButton.LocalY += 90; + + submitButton.Clicked += () => + { + Enum.TryParse(typeDropdown.SelectedItem, out ViewportScaleType type); + + var combineString = type == ViewportScaleType.Aspect ? ":" : "x"; + var name = nameTextBox.Text + " (" + wValue.Value + combineString + hValue.Value + ") " + typeDropdown.SelectedItem; + + var newViewportOption = new ViewportScaleOptions + { + ScaleType = type, + Label = name, + Size = new Int2(wValue.Value, hValue.Value), + }; + + _customViewportScaling.Add(newViewportOption); + vsMenu.DisposeAllItems(); + CreateViewportSizingContextMenu(vsMenu); + vsMenu.PerformLayout(); + }; + + var cancelButton = new Button + { + Parent = popup, + AnchorPreset = AnchorPresets.TopLeft, + Text = "Cancel", + Width = 70, + }; + cancelButton.LocalX += 120; + cancelButton.LocalY += 90; + + cancelButton.Clicked += () => + { + nameTextBox.Clear(); + typeDropdown.SelectedItem = "Aspect"; + hValue.Value = 9; + wValue.Value = 16; + popup.Hide(); + }; + }; + } + /// public override void Draw() { @@ -624,6 +1029,8 @@ namespace FlaxEditor.Windows { writer.WriteAttributeString("ShowGUI", ShowGUI.ToString()); writer.WriteAttributeString("ShowDebugDraw", ShowDebugDraw.ToString()); + writer.WriteAttributeString("DefaultViewportScaling", JsonSerializer.Serialize(_defaultViewportScaling)); + writer.WriteAttributeString("CustomViewportScaling", JsonSerializer.Serialize(_customViewportScaling)); } /// @@ -633,6 +1040,24 @@ namespace FlaxEditor.Windows ShowGUI = value1; if (bool.TryParse(node.GetAttribute("ShowDebugDraw"), out value1)) ShowDebugDraw = value1; + if (node.HasAttribute("CustomViewportScaling")) + _customViewportScaling = JsonSerializer.Deserialize>(node.GetAttribute("CustomViewportScaling")); + + for (int i = 0; i < _customViewportScaling.Count; i++) + { + if (_customViewportScaling[i].Active) + ChangeViewportRatio(_customViewportScaling[i]); + } + + if (node.HasAttribute("DefaultViewportScaling")) + _defaultViewportScaling = JsonSerializer.Deserialize>(node.GetAttribute("DefaultViewportScaling")); + + for (int i = 0; i < _defaultViewportScaling.Count; i++) + { + if (_defaultViewportScaling[i].Active) + ChangeViewportRatio(_defaultViewportScaling[i]); + } + } ///