1177 lines
42 KiB
C#
1177 lines
42 KiB
C#
// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Xml;
|
|
using FlaxEditor.Gizmo;
|
|
using FlaxEditor.GUI.ContextMenu;
|
|
using FlaxEditor.GUI.Input;
|
|
using FlaxEditor.Options;
|
|
using FlaxEngine;
|
|
using FlaxEngine.GUI;
|
|
using FlaxEngine.Json;
|
|
|
|
namespace FlaxEditor.Windows
|
|
{
|
|
/// <summary>
|
|
/// Provides in-editor play mode simulation.
|
|
/// </summary>
|
|
/// <seealso cref="FlaxEditor.Windows.EditorWindow" />
|
|
public class GameWindow : EditorWindow
|
|
{
|
|
private readonly RenderOutputControl _viewport;
|
|
private readonly GameRoot _guiRoot;
|
|
private bool _showGUI = true;
|
|
private bool _showDebugDraw = false;
|
|
private bool _isMaximized = false, _isUnlockingMouse = false;
|
|
private bool _isFloating = false, _isBorderless = false;
|
|
private bool _cursorVisible = true;
|
|
private float _gameStartTime;
|
|
private GUI.Docking.DockState _maximizeRestoreDockState;
|
|
private GUI.Docking.DockPanel _maximizeRestoreDockTo;
|
|
private CursorLockMode _cursorLockMode = CursorLockMode.None;
|
|
|
|
// Viewport scaling variables
|
|
private List<ViewportScaleOptions> _defaultViewportScaling = new List<ViewportScaleOptions>();
|
|
private List<ViewportScaleOptions> _customViewportScaling = new List<ViewportScaleOptions>();
|
|
private float _viewportAspectRatio = 1;
|
|
private float _windowAspectRatio = 1;
|
|
private bool _useAspect = false;
|
|
private bool _freeAspect = true;
|
|
|
|
private List<PlayModeFocusOptions> _focusOptions = new List<PlayModeFocusOptions>()
|
|
{
|
|
new PlayModeFocusOptions
|
|
{
|
|
Name = "None",
|
|
Tooltip = "Don't change focus.",
|
|
FocusOption = InterfaceOptions.PlayModeFocus.None,
|
|
},
|
|
new PlayModeFocusOptions
|
|
{
|
|
Name = "Game Panel",
|
|
Tooltip = "Focus the Game panel.",
|
|
FocusOption = InterfaceOptions.PlayModeFocus.GamePanel,
|
|
},
|
|
new PlayModeFocusOptions
|
|
{
|
|
Name = "Game Panel Then Back",
|
|
Tooltip = "Focus the Game panel. On play mode end focus the previous panel again.",
|
|
FocusOption = InterfaceOptions.PlayModeFocus.GamePanelThenBack,
|
|
},
|
|
};
|
|
|
|
/// <summary>
|
|
/// Gets the viewport.
|
|
/// </summary>
|
|
public RenderOutputControl Viewport => _viewport;
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether show game GUI in the view or keep it hidden.
|
|
/// </summary>
|
|
public bool ShowGUI
|
|
{
|
|
get => _showGUI;
|
|
set
|
|
{
|
|
if (value != _showGUI)
|
|
{
|
|
_showGUI = value;
|
|
_guiRoot.Visible = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether show Debug Draw shapes in the view or keep it hidden.
|
|
/// </summary>
|
|
public bool ShowDebugDraw
|
|
{
|
|
get => _showDebugDraw;
|
|
set => _showDebugDraw = value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether the game window is maximized (only in play mode).
|
|
/// </summary>
|
|
private bool IsMaximized
|
|
{
|
|
get => _isMaximized;
|
|
set
|
|
{
|
|
if (_isMaximized == value)
|
|
return;
|
|
_isMaximized = value;
|
|
if (value)
|
|
{
|
|
IsFloating = true;
|
|
var rootWindow = RootWindow;
|
|
rootWindow.Maximize();
|
|
}
|
|
else
|
|
{
|
|
IsFloating = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether the game window is floating (popup, only in play mode).
|
|
/// </summary>
|
|
private bool IsFloating
|
|
{
|
|
get => _isFloating;
|
|
set
|
|
{
|
|
if (_isFloating == value)
|
|
return;
|
|
_isFloating = value;
|
|
var rootWindow = RootWindow;
|
|
if (value)
|
|
{
|
|
_maximizeRestoreDockTo = _dockedTo;
|
|
_maximizeRestoreDockState = _dockedTo.TryGetDockState(out _);
|
|
if (_maximizeRestoreDockState != GUI.Docking.DockState.Float)
|
|
{
|
|
var monitorBounds = Platform.GetMonitorBounds(PointToScreen(Size * 0.5f));
|
|
var size = DefaultSize;
|
|
var location = monitorBounds.Location + monitorBounds.Size * 0.5f - size * 0.5f;
|
|
ShowFloating(location, size, WindowStartPosition.Manual);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Restore
|
|
if (rootWindow != null)
|
|
rootWindow.Restore();
|
|
if (_maximizeRestoreDockTo != null && _maximizeRestoreDockTo.IsDisposing)
|
|
_maximizeRestoreDockTo = null;
|
|
Show(_maximizeRestoreDockState, _maximizeRestoreDockTo);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether the game window is borderless (only in play mode).
|
|
/// </summary>
|
|
private bool IsBorderless
|
|
{
|
|
get => _isBorderless;
|
|
set
|
|
{
|
|
if (_isBorderless == value)
|
|
return;
|
|
_isBorderless = value;
|
|
if (value)
|
|
{
|
|
IsFloating = true;
|
|
var rootWindow = RootWindow;
|
|
var monitorBounds = Platform.GetMonitorBounds(rootWindow.RootWindow.Window.ClientPosition);
|
|
rootWindow.Window.Position = monitorBounds.Location;
|
|
rootWindow.Window.SetBorderless(true);
|
|
rootWindow.Window.ClientSize = monitorBounds.Size;
|
|
}
|
|
else
|
|
{
|
|
IsFloating = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether center mouse position on window focus in play mode. Helps when working with games that lock mouse cursor.
|
|
/// </summary>
|
|
public bool CenterMouseOnFocus { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating what panel should be focused when play mode start.
|
|
/// </summary>
|
|
public InterfaceOptions.PlayModeFocus FocusOnPlayOption { get; set; }
|
|
|
|
private enum ViewportScaleType
|
|
{
|
|
Resolution = 0,
|
|
Aspect = 1,
|
|
}
|
|
|
|
private class ViewportScaleOptions
|
|
{
|
|
/// <summary>
|
|
/// The name.
|
|
/// </summary>
|
|
public string Label;
|
|
|
|
/// <summary>
|
|
/// The Type of scaling to do.
|
|
/// </summary>
|
|
public ViewportScaleType ScaleType;
|
|
|
|
/// <summary>
|
|
/// The width and height to scale by.
|
|
/// </summary>
|
|
public Int2 Size;
|
|
|
|
/// <summary>
|
|
/// If the scaling is active.
|
|
/// </summary>
|
|
public bool Active;
|
|
}
|
|
|
|
private class PlayModeFocusOptions
|
|
{
|
|
/// <summary>
|
|
/// The name.
|
|
/// </summary>
|
|
public string Name;
|
|
|
|
/// <summary>
|
|
/// The tooltip.
|
|
/// </summary>
|
|
public string Tooltip;
|
|
|
|
/// <summary>
|
|
/// The type of focus.
|
|
/// </summary>
|
|
public InterfaceOptions.PlayModeFocus FocusOption;
|
|
|
|
/// <summary>
|
|
/// If the option is active.
|
|
/// </summary>
|
|
public bool Active;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Root control for game UI preview in Editor. Supports basic UI editing via <see cref="UIEditorRoot"/>.
|
|
/// </summary>
|
|
private class GameRoot : UIEditorRoot
|
|
{
|
|
public override bool EnableInputs => !Time.GamePaused && Editor.IsPlayMode;
|
|
public override bool EnableSelecting => !Editor.IsPlayMode || Time.GamePaused;
|
|
public override TransformGizmo TransformGizmo => Editor.Instance.MainTransformGizmo;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="GameWindow"/> class.
|
|
/// </summary>
|
|
/// <param name="editor">The editor.</param>
|
|
public GameWindow(Editor editor)
|
|
: base(editor, true, ScrollBars.None)
|
|
{
|
|
Title = "Game";
|
|
AutoFocus = true;
|
|
|
|
var task = MainRenderTask.Instance;
|
|
|
|
// Setup viewport
|
|
_viewport = new RenderOutputControl(task)
|
|
{
|
|
AnchorPreset = AnchorPresets.StretchAll,
|
|
Offsets = Margin.Zero,
|
|
AutoFocus = false,
|
|
Parent = this
|
|
};
|
|
task.PostRender += OnPostRender;
|
|
|
|
// Override the game GUI root
|
|
_guiRoot = new GameRoot
|
|
{
|
|
Parent = _viewport
|
|
};
|
|
RootControl.GameRoot = _guiRoot.UIRoot;
|
|
|
|
SizeChanged += control => { ResizeViewport(); };
|
|
|
|
Editor.StateMachine.PlayingState.SceneDuplicating += PlayingStateOnSceneDuplicating;
|
|
Editor.StateMachine.PlayingState.SceneRestored += PlayingStateOnSceneRestored;
|
|
|
|
// Link editor options
|
|
Editor.Options.OptionsChanged += OnOptionsChanged;
|
|
OnOptionsChanged(Editor.Options.Options);
|
|
|
|
InputActions.Add(options => options.TakeScreenshot, () => Screenshot.Capture(string.Empty));
|
|
InputActions.Add(options => options.DebuggerUnlockMouse, UnlockMouseInPlay);
|
|
InputActions.Add(options => options.ToggleFullscreen, () => { if (Editor.IsPlayMode) IsMaximized = !IsMaximized; });
|
|
InputActions.Add(options => options.Play, Editor.Instance.Simulation.DelegatePlayOrStopPlayInEditor);
|
|
InputActions.Add(options => options.Pause, Editor.Instance.Simulation.RequestResumeOrPause);
|
|
InputActions.Add(options => options.StepFrame, Editor.Instance.Simulation.RequestPlayOneFrame);
|
|
InputActions.Add(options => options.ProfilerStartStop, () =>
|
|
{
|
|
bool recording = !Editor.Instance.Windows.ProfilerWin.LiveRecording;
|
|
Editor.Instance.Windows.ProfilerWin.LiveRecording = recording;
|
|
Editor.Instance.UI.AddStatusMessage($"Profiling {(recording ? "started" : "stopped")}.");
|
|
});
|
|
InputActions.Add(options => options.ProfilerClear, () =>
|
|
{
|
|
Editor.Instance.Windows.ProfilerWin.Clear();
|
|
Editor.Instance.UI.AddStatusMessage($"Profiling results cleared.");
|
|
});
|
|
InputActions.Add(options => options.Save, () =>
|
|
{
|
|
if (Editor.IsPlayMode)
|
|
return;
|
|
Editor.Instance.SaveAll();
|
|
});
|
|
InputActions.Add(options => options.Undo, () =>
|
|
{
|
|
if (Editor.IsPlayMode)
|
|
return;
|
|
Editor.Instance.PerformUndo();
|
|
Focus();
|
|
});
|
|
InputActions.Add(options => options.Redo, () =>
|
|
{
|
|
if (Editor.IsPlayMode)
|
|
return;
|
|
Editor.Instance.PerformRedo();
|
|
Focus();
|
|
});
|
|
InputActions.Add(options => options.Cut, () =>
|
|
{
|
|
if (Editor.IsPlayMode)
|
|
return;
|
|
Editor.Instance.SceneEditing.Cut();
|
|
});
|
|
InputActions.Add(options => options.Copy, () =>
|
|
{
|
|
if (Editor.IsPlayMode)
|
|
return;
|
|
Editor.Instance.SceneEditing.Copy();
|
|
});
|
|
InputActions.Add(options => options.Paste, () =>
|
|
{
|
|
if (Editor.IsPlayMode)
|
|
return;
|
|
Editor.Instance.SceneEditing.Paste();
|
|
});
|
|
InputActions.Add(options => options.Duplicate, () =>
|
|
{
|
|
if (Editor.IsPlayMode)
|
|
return;
|
|
Editor.Instance.SceneEditing.Duplicate();
|
|
});
|
|
InputActions.Add(options => options.Delete, () =>
|
|
{
|
|
if (Editor.IsPlayMode)
|
|
return;
|
|
Editor.Instance.SceneEditing.Delete();
|
|
});
|
|
}
|
|
|
|
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", StringComparison.Ordinal) && 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);
|
|
}
|
|
_viewport.SyncBackbufferSize();
|
|
PerformLayout();
|
|
}
|
|
|
|
private void OnPostRender(GPUContext context, ref RenderContext renderContext)
|
|
{
|
|
// Debug Draw shapes
|
|
if (_showDebugDraw)
|
|
{
|
|
var task = _viewport.Task;
|
|
|
|
// Draw actors debug shapes manually if editor viewport is hidden (game viewport task is always rendered before editor viewports)
|
|
var editWindowViewport = Editor.Windows.EditWin.Viewport;
|
|
if (editWindowViewport.Task.LastUsedFrame != Engine.FrameCount)
|
|
{
|
|
var drawDebugData = editWindowViewport.DebugDrawData;
|
|
drawDebugData.Clear();
|
|
var selectedParents = editWindowViewport.TransformGizmo.SelectedParents;
|
|
if (selectedParents.Count > 0)
|
|
{
|
|
for (int i = 0; i < selectedParents.Count; i++)
|
|
{
|
|
if (selectedParents[i].IsActiveInHierarchy)
|
|
selectedParents[i].OnDebugDraw(drawDebugData);
|
|
}
|
|
}
|
|
unsafe
|
|
{
|
|
fixed (IntPtr* actors = drawDebugData.ActorsPtrs)
|
|
{
|
|
DebugDraw.DrawActors(new IntPtr(actors), drawDebugData.ActorsCount, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
DebugDraw.Draw(ref renderContext, task.OutputView);
|
|
}
|
|
}
|
|
|
|
private void OnOptionsChanged(EditorOptions options)
|
|
{
|
|
CenterMouseOnFocus = options.Interface.CenterMouseOnGameWinFocus;
|
|
FocusOnPlayOption = options.Interface.FocusOnPlayMode;
|
|
}
|
|
|
|
private void PlayingStateOnSceneDuplicating()
|
|
{
|
|
// Remove remaining GUI controls so loaded scene can add own GUI
|
|
//_guiRoot.DisposeChildren();
|
|
|
|
// Show GUI
|
|
//_guiRoot.Visible = _showGUI;
|
|
}
|
|
|
|
private void PlayingStateOnSceneRestored()
|
|
{
|
|
// Remove remaining GUI controls so loaded scene can add own GUI
|
|
//_guiRoot.DisposeChildren();
|
|
|
|
// Hide GUI
|
|
//_guiRoot.Visible = false;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override bool CanOpenContentFinder => !Editor.IsPlayMode;
|
|
|
|
/// <inheritdoc />
|
|
protected override bool CanUseNavigation => false;
|
|
|
|
/// <inheritdoc />
|
|
public override void OnPlayBegin()
|
|
{
|
|
_gameStartTime = Time.UnscaledGameTime;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void OnPlayEnd()
|
|
{
|
|
IsFloating = false;
|
|
IsMaximized = false;
|
|
IsBorderless = false;
|
|
Cursor = CursorType.Default;
|
|
Screen.CursorLock = CursorLockMode.None;
|
|
if (Screen.MainWindow.IsMouseTracking)
|
|
Screen.MainWindow.EndTrackingMouse();
|
|
RootControl.GameRoot.EndMouseCapture();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void OnMouseLeave()
|
|
{
|
|
base.OnMouseLeave();
|
|
|
|
// Remove focus from game window when mouse moves out and the cursor is hidden during game
|
|
if (ContainsFocus && Parent != null && Editor.IsPlayMode && !Screen.CursorVisible && Screen.CursorLock == CursorLockMode.None)
|
|
{
|
|
Parent.Focus();
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void OnShowContextMenu(ContextMenu menu)
|
|
{
|
|
base.OnShowContextMenu(menu);
|
|
|
|
// Focus on play
|
|
{
|
|
var pfMenu = menu.AddChildMenu("Focus On Play Override").ContextMenu;
|
|
|
|
GenerateFocusOptionsContextMenu(pfMenu);
|
|
|
|
pfMenu.AddSeparator();
|
|
|
|
var button = pfMenu.AddButton("Remove override");
|
|
button.TooltipText = "Reset the override to the value set in the editor options.";
|
|
button.Clicked += () => FocusOnPlayOption = Editor.Instance.Options.Options.Interface.FocusOnPlayMode;
|
|
}
|
|
|
|
menu.AddSeparator();
|
|
|
|
// Viewport Brightness
|
|
{
|
|
var brightness = menu.AddButton("Viewport Brightness");
|
|
brightness.CloseMenuOnClick = false;
|
|
var brightnessValue = new FloatValueBox(_viewport.Brightness, 140, 2, 50.0f, 0.001f, 10.0f, 0.001f)
|
|
{
|
|
Parent = brightness
|
|
};
|
|
brightnessValue.ValueChanged += () => _viewport.Brightness = brightnessValue.Value;
|
|
}
|
|
|
|
// Viewport Resolution
|
|
{
|
|
var resolution = menu.AddButton("Viewport Resolution");
|
|
resolution.CloseMenuOnClick = false;
|
|
var resolutionValue = new FloatValueBox(_viewport.ResolutionScale, 140, 2, 50.0f, 0.1f, 4.0f, 0.001f)
|
|
{
|
|
Parent = resolution
|
|
};
|
|
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 (Full HD)",
|
|
ScaleType = ViewportScaleType.Resolution,
|
|
Size = new Int2(1920, 1080),
|
|
Active = false,
|
|
});
|
|
_defaultViewportScaling.Add(new ViewportScaleOptions
|
|
{
|
|
Label = "2560x1440 Resolution (2K)",
|
|
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");
|
|
takeScreenshot.Clicked += TakeScreenshot;
|
|
}
|
|
|
|
menu.AddSeparator();
|
|
|
|
// Show GUI
|
|
{
|
|
var button = menu.AddButton("Show GUI");
|
|
button.CloseMenuOnClick = false;
|
|
var checkbox = new CheckBox(140, 2, ShowGUI) { Parent = button };
|
|
checkbox.StateChanged += x => ShowGUI = x.Checked;
|
|
}
|
|
|
|
// Show Debug Draw
|
|
{
|
|
var button = menu.AddButton("Show Debug Draw");
|
|
button.CloseMenuOnClick = false;
|
|
var checkbox = new CheckBox(140, 2, ShowDebugDraw) { Parent = button };
|
|
checkbox.StateChanged += x => ShowDebugDraw = x.Checked;
|
|
}
|
|
|
|
menu.MinimumWidth = 200;
|
|
menu.AddSeparator();
|
|
}
|
|
|
|
private void GenerateFocusOptionsContextMenu(ContextMenu pfMenu)
|
|
{
|
|
foreach (PlayModeFocusOptions f in _focusOptions)
|
|
{
|
|
f.Active = f.FocusOption == FocusOnPlayOption;
|
|
|
|
var button = pfMenu.AddButton(f.Name);
|
|
button.CloseMenuOnClick = false;
|
|
button.Tag = f;
|
|
button.TooltipText = f.Tooltip;
|
|
button.Icon = f.Active ? Style.Current.CheckBoxTick : SpriteHandle.Invalid;
|
|
button.Clicked += () =>
|
|
{
|
|
foreach (var child in pfMenu.Items)
|
|
{
|
|
if (child is ContextMenuButton cmb && cmb.Tag is PlayModeFocusOptions p)
|
|
{
|
|
if (cmb == button)
|
|
{
|
|
p.Active = true;
|
|
button.Icon = Style.Current.CheckBoxTick;
|
|
FocusOnPlayOption = p.FocusOption;
|
|
}
|
|
else if (p.Active)
|
|
{
|
|
cmb.Icon = SpriteHandle.Invalid;
|
|
p.Active = false;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
private void CreateViewportSizingContextMenu(ContextMenu vsMenu)
|
|
{
|
|
// Add default viewport sizing options
|
|
for (int i = 0; i < _defaultViewportScaling.Count; i++)
|
|
{
|
|
var viewportScale = _defaultViewportScaling[i];
|
|
var button = vsMenu.AddButton(viewportScale.Label);
|
|
button.CloseMenuOnClick = false;
|
|
button.Icon = viewportScale.Active ? Style.Current.CheckBoxTick : SpriteHandle.Invalid;
|
|
button.Tag = viewportScale;
|
|
if (viewportScale.Active)
|
|
ChangeViewportRatio(viewportScale);
|
|
|
|
button.Clicked += () =>
|
|
{
|
|
if (button.Tag == null)
|
|
return;
|
|
|
|
// Reset selected icon on all buttons
|
|
foreach (var child in vsMenu.Items)
|
|
{
|
|
if (child is ContextMenuButton cmb && cmb.Tag is ViewportScaleOptions v)
|
|
{
|
|
if (cmb == button)
|
|
{
|
|
v.Active = true;
|
|
button.Icon = Style.Current.CheckBoxTick;
|
|
ChangeViewportRatio(v);
|
|
}
|
|
else if (v.Active)
|
|
{
|
|
cmb.Icon = SpriteHandle.Invalid;
|
|
v.Active = false;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}
|
|
if (_defaultViewportScaling.Count != 0)
|
|
vsMenu.AddSeparator();
|
|
|
|
// Add custom viewport options
|
|
for (int i = 0; i < _customViewportScaling.Count; i++)
|
|
{
|
|
var viewportScale = _customViewportScaling[i];
|
|
var childCM = vsMenu.AddChildMenu(viewportScale.Label);
|
|
childCM.CloseMenuOnClick = false;
|
|
childCM.Icon = viewportScale.Active ? Style.Current.CheckBoxTick : SpriteHandle.Invalid;
|
|
childCM.Tag = viewportScale;
|
|
if (viewportScale.Active)
|
|
ChangeViewportRatio(viewportScale);
|
|
|
|
var applyButton = childCM.ContextMenu.AddButton("Apply");
|
|
applyButton.Tag = childCM.Tag = viewportScale;
|
|
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 && cmb.Tag is ViewportScaleOptions v)
|
|
{
|
|
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();
|
|
};
|
|
}
|
|
if (_customViewportScaling.Count != 0)
|
|
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();
|
|
};
|
|
};
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void Draw()
|
|
{
|
|
base.Draw();
|
|
|
|
if (Camera.MainCamera == null)
|
|
{
|
|
var style = Style.Current;
|
|
Render2D.DrawText(style.FontLarge, "No camera", new Rectangle(Float2.Zero, Size), style.ForegroundDisabled, TextAlignment.Center, TextAlignment.Center);
|
|
}
|
|
|
|
// Play mode hints and overlay
|
|
if (Editor.StateMachine.IsPlayMode)
|
|
{
|
|
var style = Style.Current;
|
|
float time = Time.UnscaledGameTime - _gameStartTime;
|
|
float timeout = 3.0f;
|
|
float fadeOutTime = 1.0f;
|
|
float animTime = time - timeout;
|
|
if (animTime < 0)
|
|
{
|
|
var alpha = Mathf.Saturate(-animTime / fadeOutTime);
|
|
var rect = new Rectangle(new Float2(6), Size - 12);
|
|
var text = $"Press {Editor.Options.Options.Input.DebuggerUnlockMouse} to unlock the mouse";
|
|
Render2D.DrawText(style.FontSmall, text, rect + new Float2(1.0f), style.Background * alpha, TextAlignment.Near, TextAlignment.Far);
|
|
Render2D.DrawText(style.FontSmall, text, rect, style.Foreground * alpha, TextAlignment.Near, TextAlignment.Far);
|
|
}
|
|
|
|
timeout = 1.0f;
|
|
fadeOutTime = 0.6f;
|
|
animTime = time - timeout;
|
|
if (animTime < 0)
|
|
{
|
|
float alpha = Mathf.Saturate(-animTime / fadeOutTime);
|
|
Render2D.DrawRectangle(new Rectangle(new Float2(4), Size - 8), style.SelectionBorder * alpha);
|
|
}
|
|
|
|
// Add overlay during debugger breakpoint hang
|
|
if (Editor.Simulation.IsDuringBreakpointHang)
|
|
{
|
|
var bounds = new Rectangle(Float2.Zero, Size);
|
|
Render2D.FillRectangle(bounds, new Color(0.0f, 0.0f, 0.0f, 0.2f));
|
|
Render2D.DrawText(Style.Current.FontLarge, "Debugger breakpoint hit...", bounds, Color.White, TextAlignment.Center, TextAlignment.Center);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Unlocks the mouse if game window is focused during play mode.
|
|
/// </summary>
|
|
public void UnlockMouseInPlay()
|
|
{
|
|
if (Editor.StateMachine.IsPlayMode)
|
|
{
|
|
// Cache cursor
|
|
_cursorVisible = Screen.CursorVisible;
|
|
_cursorLockMode = Screen.CursorLock;
|
|
Screen.CursorVisible = true;
|
|
if (Screen.CursorLock == CursorLockMode.Clipped)
|
|
Screen.CursorLock = CursorLockMode.None;
|
|
|
|
// Defocus
|
|
_isUnlockingMouse = true;
|
|
Focus(null);
|
|
_isUnlockingMouse = false;
|
|
Editor.Windows.MainWindow.Focus();
|
|
if (Editor.Windows.PropertiesWin.IsDocked)
|
|
Editor.Windows.PropertiesWin.Focus();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Focuses the game viewport. Shows the window if hidden or unselected.
|
|
/// </summary>
|
|
public void FocusGameViewport()
|
|
{
|
|
if (!IsDocked)
|
|
{
|
|
ShowFloating();
|
|
}
|
|
else if (!IsSelected)
|
|
{
|
|
SelectTab(false);
|
|
RootWindow?.Window?.Focus();
|
|
}
|
|
Focus();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Apply the selected window mode to the game window.
|
|
/// </summary>
|
|
/// <param name="mode"></param>
|
|
public void SetWindowMode(InterfaceOptions.GameWindowMode mode)
|
|
{
|
|
switch (mode)
|
|
{
|
|
case InterfaceOptions.GameWindowMode.Docked:
|
|
break;
|
|
case InterfaceOptions.GameWindowMode.PopupWindow:
|
|
IsFloating = true;
|
|
break;
|
|
case InterfaceOptions.GameWindowMode.MaximizedWindow:
|
|
IsMaximized = true;
|
|
break;
|
|
case InterfaceOptions.GameWindowMode.BorderlessWindow:
|
|
IsBorderless = true;
|
|
break;
|
|
default: throw new ArgumentOutOfRangeException(nameof(mode), mode, null);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Takes the screenshot of the current viewport.
|
|
/// </summary>
|
|
public void TakeScreenshot()
|
|
{
|
|
Screenshot.Capture(_viewport.Task);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Takes the screenshot of the current viewport.
|
|
/// </summary>
|
|
/// <param name="path">The output file path.</param>
|
|
public void TakeScreenshot(string path)
|
|
{
|
|
Screenshot.Capture(_viewport.Task, path);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override bool OnKeyDown(KeyboardKeys key)
|
|
{
|
|
// Prevent closing the game window tab during a play session
|
|
if (Editor.StateMachine.IsPlayMode && Editor.Options.Options.Input.CloseTab.Process(this, key))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return base.OnKeyDown(key);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void OnStartContainsFocus()
|
|
{
|
|
base.OnStartContainsFocus();
|
|
|
|
if (Editor.StateMachine.IsPlayMode && !Editor.StateMachine.PlayingState.IsPaused)
|
|
{
|
|
// Center mouse in play mode
|
|
if (CenterMouseOnFocus)
|
|
{
|
|
var center = PointToWindow(Size * 0.5f);
|
|
Root.MousePosition = center;
|
|
}
|
|
|
|
// Restore cursor
|
|
if (_cursorLockMode != CursorLockMode.None)
|
|
Screen.CursorLock = _cursorLockMode;
|
|
if (!_cursorVisible)
|
|
Screen.CursorVisible = false;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void OnEndContainsFocus()
|
|
{
|
|
base.OnEndContainsFocus();
|
|
|
|
if (!_isUnlockingMouse)
|
|
{
|
|
// Cache cursor
|
|
_cursorVisible = Screen.CursorVisible;
|
|
_cursorLockMode = Screen.CursorLock;
|
|
|
|
// Restore cursor visibility (could be hidden by the game)
|
|
if (!_cursorVisible)
|
|
Screen.CursorVisible = true;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override Control OnNavigate(NavDirection direction, Float2 location, Control caller, List<Control> visited)
|
|
{
|
|
// Block leaking UI navigation focus outside the game window
|
|
if (IsFocused && caller != this)
|
|
{
|
|
// Pick the first UI control if game UI is not focused yet
|
|
foreach (var child in _guiRoot.Children)
|
|
{
|
|
if (child.Visible)
|
|
return child.OnNavigate(direction, Float2.Zero, this, visited);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override bool OnMouseDown(Float2 location, MouseButton button)
|
|
{
|
|
var result = base.OnMouseDown(location, button);
|
|
|
|
// Catch user focus
|
|
if (!ContainsFocus)
|
|
Focus();
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override bool UseLayoutData => true;
|
|
|
|
/// <inheritdoc />
|
|
public override void OnLayoutSerialize(XmlWriter writer)
|
|
{
|
|
writer.WriteAttributeString("ShowGUI", ShowGUI.ToString());
|
|
writer.WriteAttributeString("ShowDebugDraw", ShowDebugDraw.ToString());
|
|
writer.WriteAttributeString("DefaultViewportScaling", JsonSerializer.Serialize(_defaultViewportScaling));
|
|
writer.WriteAttributeString("CustomViewportScaling", JsonSerializer.Serialize(_customViewportScaling));
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void OnLayoutDeserialize(XmlElement node)
|
|
{
|
|
if (bool.TryParse(node.GetAttribute("ShowGUI"), out bool value1))
|
|
ShowGUI = value1;
|
|
if (bool.TryParse(node.GetAttribute("ShowDebugDraw"), out value1))
|
|
ShowDebugDraw = value1;
|
|
if (node.HasAttribute("CustomViewportScaling"))
|
|
_customViewportScaling = JsonSerializer.Deserialize<List<ViewportScaleOptions>>(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<List<ViewportScaleOptions>>(node.GetAttribute("DefaultViewportScaling"));
|
|
|
|
for (int i = 0; i < _defaultViewportScaling.Count; i++)
|
|
{
|
|
if (_defaultViewportScaling[i].Active)
|
|
ChangeViewportRatio(_defaultViewportScaling[i]);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void OnLayoutDeserialize()
|
|
{
|
|
ShowGUI = true;
|
|
ShowDebugDraw = false;
|
|
}
|
|
}
|
|
}
|