// Copyright (c) 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 { /// /// Provides in-editor play mode simulation. /// /// public class GameWindow : EditorWindow { private readonly RenderOutputControl _viewport; private readonly GameRoot _guiRoot; private bool _showGUI = true, _editGUI = true; private bool _showDebugDraw = false; private bool _audioMuted = false; private float _audioVolume = 1; 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 _defaultViewportScaling = new List(); private List _customViewportScaling = new List(); private float _viewportAspectRatio = 1; private float _windowAspectRatio = 1; private bool _useAspect = false; private bool _freeAspect = true; private List _focusOptions = new List() { new PlayModeFocusOptions { Name = "None", Tooltip = "Don't change focus.", FocusOption = InterfaceOptions.PlayModeFocus.None, }, new PlayModeFocusOptions { Name = "Game Window", Tooltip = "Focus the Game Window.", FocusOption = InterfaceOptions.PlayModeFocus.GameWindow, }, new PlayModeFocusOptions { Name = "Game Window Then Restore", Tooltip = "Focus the Game Window. On play mode end restore focus to the previous window.", FocusOption = InterfaceOptions.PlayModeFocus.GameWindowThenRestore, }, }; /// /// Gets the viewport. /// public RenderOutputControl Viewport => _viewport; /// /// Gets or sets a value indicating whether show game GUI in the view or keep it hidden. /// public bool ShowGUI { get => _showGUI; set { if (value != _showGUI) { _showGUI = value; _guiRoot.Visible = value; } } } /// /// Gets or sets a value indicating whether allow editing game GUI in the view or keep it visible-only. /// public bool EditGUI { get => _editGUI; set { if (value != _editGUI) { _editGUI = value; _guiRoot.Editable = value; } } } /// /// Gets or sets a value indicating whether show Debug Draw shapes in the view or keep it hidden. /// public bool ShowDebugDraw { get => _showDebugDraw; set => _showDebugDraw = value; } /// /// Gets or set a value indicating whether Audio is muted. /// public bool AudioMuted { get => _audioMuted; set { Audio.MasterVolume = value ? 0 : AudioVolume; _audioMuted = value; } } /// /// Gets or sets a value that set the audio volume. /// public float AudioVolume { get => _audioVolume; set { if (!AudioMuted) Audio.MasterVolume = value; _audioVolume = value; } } /// /// Gets or sets a value indicating whether the game window is maximized (only in play mode). /// private bool IsMaximized { get => _isMaximized; set { if (_isMaximized == value) return; _isMaximized = value; if (value) { IsFloating = true; var rootWindow = RootWindow; rootWindow.Maximize(); } else { IsFloating = false; } } } /// /// Gets or sets a value indicating whether the game window is floating (popup, only in play mode). /// 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); } } } /// /// Gets or sets a value indicating whether the game window is borderless (only in play mode). /// 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; } } } /// /// 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. /// public bool CenterMouseOnFocus { get; set; } /// /// Gets or sets a value indicating what panel should be focused when play mode start. /// public InterfaceOptions.PlayModeFocus FocusOnPlayOption { 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 PlayModeFocusOptions { /// /// The name. /// public string Name; /// /// The tooltip. /// public string Tooltip; /// /// The type of focus. /// public InterfaceOptions.PlayModeFocus FocusOption; /// /// If the option is active. /// public bool Active; } /// /// Root control for game UI preview in Editor. Supports basic UI editing via . /// private class GameRoot : UIEditorRoot { internal bool Editable = true; public override bool EnableInputs => !Time.GamePaused && Editor.IsPlayMode; public override bool EnableSelecting => (!Editor.IsPlayMode || Time.GamePaused) && Editable; public override TransformGizmo TransformGizmo => Editor.Instance.MainTransformGizmo; } /// /// Initializes a new instance of the class. /// /// The editor. public GameWindow(Editor editor) : base(editor, true, ScrollBars.None) { Title = "Game"; Icon = editor.Icons.Play64; 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); #if USE_PROFILER 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."); }); #endif 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(); }); InputActions.Add(options => options.FocusConsoleCommand, () => Editor.Instance.Windows.OutputLogWin.FocusCommand()); } 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; } /// protected override bool CanOpenContentFinder => !Editor.IsPlayMode; /// protected override bool CanUseNavigation => false; /// public override void OnPlayBegin() { _gameStartTime = Time.UnscaledGameTime; } /// 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(); } /// 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(); } } /// 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; } // Edit GUI { var button = menu.AddButton("Edit GUI"); var checkbox = new CheckBox(140, 2, EditGUI) { Parent = button }; checkbox.StateChanged += x => EditGUI = 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; } // Clear Debug Draw if (DebugDraw.CanClear()) { var button = menu.AddButton("Clear Debug Draw"); button.CloseMenuOnClick = false; button.Clicked += () => DebugDraw.Clear(); } menu.AddSeparator(); // Mute Audio { var button = menu.AddButton("Mute Audio"); button.CloseMenuOnClick = false; var checkbox = new CheckBox(140, 2, AudioMuted) { Parent = button }; checkbox.StateChanged += x => AudioMuted = x.Checked; } // Audio Volume { var button = menu.AddButton("Audio Volume"); button.CloseMenuOnClick = false; var slider = new FloatValueBox(AudioVolume, 140, 2, 50, 0, 1) { Parent = button }; slider.ValueChanged += () => AudioVolume = slider.Value; } 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(); }; }; } /// 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); } } } /// /// Unlocks the mouse if game window is focused during play mode. /// 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(); } } /// /// Focuses the game viewport. Shows the window if hidden or unselected. /// public void FocusGameViewport() { if (!IsDocked) { ShowFloating(); } else if (!IsSelected) { SelectTab(false); RootWindow?.Window?.Focus(); } Focus(); } /// /// Apply the selected window mode to the game window. /// /// 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); } } /// /// Takes the screenshot of the current viewport. /// public void TakeScreenshot() { Screenshot.Capture(_viewport.Task); } /// /// Takes the screenshot of the current viewport. /// /// The output file path. public void TakeScreenshot(string path) { Screenshot.Capture(_viewport.Task, path); } /// 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); } /// 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; } } /// 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; } } /// public override Control OnNavigate(NavDirection direction, Float2 location, Control caller, List 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; } /// public override bool OnMouseDown(Float2 location, MouseButton button) { var result = base.OnMouseDown(location, button); // Catch user focus if (!ContainsFocus) Focus(); return result; } /// public override bool UseLayoutData => true; /// public override void OnLayoutSerialize(XmlWriter writer) { writer.WriteAttributeString("ShowGUI", ShowGUI.ToString()); writer.WriteAttributeString("EditGUI", EditGUI.ToString()); writer.WriteAttributeString("ShowDebugDraw", ShowDebugDraw.ToString()); writer.WriteAttributeString("DefaultViewportScaling", JsonSerializer.Serialize(_defaultViewportScaling)); writer.WriteAttributeString("CustomViewportScaling", JsonSerializer.Serialize(_customViewportScaling)); } /// public override void OnLayoutDeserialize(XmlElement node) { if (bool.TryParse(node.GetAttribute("ShowGUI"), out bool value1)) ShowGUI = value1; if (bool.TryParse(node.GetAttribute("EditGUI"), out value1)) EditGUI = 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]); } } /// public override void OnLayoutDeserialize() { ShowGUI = true; EditGUI = true; ShowDebugDraw = false; } } }