diff --git a/Content/Editor/MaterialTemplates/PostProcess.shader b/Content/Editor/MaterialTemplates/PostProcess.shader index f20bdc610..927d63c8c 100644 --- a/Content/Editor/MaterialTemplates/PostProcess.shader +++ b/Content/Editor/MaterialTemplates/PostProcess.shader @@ -18,6 +18,7 @@ float TimeParam; float4 ViewInfo; float4 ScreenSize; float4 TemporalAAJitter; +float4x4 InverseViewProjectionMatrix; @1META_CB_END // Shader resources @@ -62,6 +63,14 @@ MaterialInput GetMaterialInput(PixelInput input) return result; } +// Gets world space position at given pixel coordinate with given device depth +float3 GetWorldPos(float2 uv, float deviceDepth) +{ + float4 clipPos = float4(uv * float2(2.0, -2.0) + float2(-1.0, 1.0), deviceDepth, 1.0); + float4 wsPos = mul(clipPos, InverseViewProjectionMatrix); + return wsPos.xyz / wsPos.w; +} + // Transforms a vector from tangent space to world space float3 TransformTangentVectorToWorld(MaterialInput input, float3 tangentVector) { diff --git a/Development/Scripts/Windows/GetMSBuildPath.bat b/Development/Scripts/Windows/GetMSBuildPath.bat index 4945c1015..fac1db727 100644 --- a/Development/Scripts/Windows/GetMSBuildPath.bat +++ b/Development/Scripts/Windows/GetMSBuildPath.bat @@ -11,6 +11,16 @@ for /f "delims=" %%i in ('"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer goto End ) ) +for /f "delims=" %%i in ('"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere" -latest -prerelease -products * -requires Microsoft.Component.MSBuild -property installationPath') do ( + if exist "%%i\MSBuild\15.0\Bin\MSBuild.exe" ( + set MSBUILD_PATH="%%i\MSBuild\15.0\Bin\MSBuild.exe" + goto End + ) + if exist "%%i\MSBuild\Current\Bin\MSBuild.exe" ( + set MSBUILD_PATH="%%i\MSBuild\Current\Bin\MSBuild.exe" + goto End + ) +) :VsWhereNotFound if exist "%ProgramFiles(x86)%\MSBuild\14.0\bin\MSBuild.exe" ( diff --git a/Source/Editor/Content/Proxy/AnimationGraphFunctionProxy.cs b/Source/Editor/Content/Proxy/AnimationGraphFunctionProxy.cs index c4ea2e09a..a8092a162 100644 --- a/Source/Editor/Content/Proxy/AnimationGraphFunctionProxy.cs +++ b/Source/Editor/Content/Proxy/AnimationGraphFunctionProxy.cs @@ -11,6 +11,7 @@ namespace FlaxEditor.Content /// A asset proxy object. /// /// + [ContentContextMenu("New/Animation/Animation Graph Function")] public class AnimationGraphFunctionProxy : BinaryAssetProxy { /// diff --git a/Source/Editor/Content/Proxy/AnimationGraphProxy.cs b/Source/Editor/Content/Proxy/AnimationGraphProxy.cs index 3c445c104..7f43b8c7b 100644 --- a/Source/Editor/Content/Proxy/AnimationGraphProxy.cs +++ b/Source/Editor/Content/Proxy/AnimationGraphProxy.cs @@ -11,6 +11,7 @@ namespace FlaxEditor.Content /// A asset proxy object. /// /// + [ContentContextMenu("New/Animation/Animation Graph")] public class AnimationGraphProxy : BinaryAssetProxy { /// diff --git a/Source/Editor/Content/Proxy/AnimationProxy.cs b/Source/Editor/Content/Proxy/AnimationProxy.cs index bda0fa98d..eb8c892ab 100644 --- a/Source/Editor/Content/Proxy/AnimationProxy.cs +++ b/Source/Editor/Content/Proxy/AnimationProxy.cs @@ -14,6 +14,7 @@ namespace FlaxEditor.Content /// A asset proxy object. /// /// + [ContentContextMenu("New/Animation/Animation")] public class AnimationProxy : BinaryAssetProxy { /// diff --git a/Source/Editor/Content/Proxy/CSharpScriptProxy.cs b/Source/Editor/Content/Proxy/CSharpScriptProxy.cs index 332695695..a871d0589 100644 --- a/Source/Editor/Content/Proxy/CSharpScriptProxy.cs +++ b/Source/Editor/Content/Proxy/CSharpScriptProxy.cs @@ -12,6 +12,7 @@ namespace FlaxEditor.Content /// Context proxy object for C# script files. /// /// + [ContentContextMenu("New/C# Script")] public class CSharpScriptProxy : ScriptProxy { /// diff --git a/Source/Editor/Content/Proxy/CollisionDataProxy.cs b/Source/Editor/Content/Proxy/CollisionDataProxy.cs index cca8a8192..b6ff2e75f 100644 --- a/Source/Editor/Content/Proxy/CollisionDataProxy.cs +++ b/Source/Editor/Content/Proxy/CollisionDataProxy.cs @@ -38,6 +38,7 @@ namespace FlaxEditor.Content /// A asset proxy object. /// /// + [ContentContextMenu("New/Physics/Collision Data")] class CollisionDataProxy : BinaryAssetProxy { /// diff --git a/Source/Editor/Content/Proxy/CppProxy.cs b/Source/Editor/Content/Proxy/CppProxy.cs index d975b8778..6581dccd2 100644 --- a/Source/Editor/Content/Proxy/CppProxy.cs +++ b/Source/Editor/Content/Proxy/CppProxy.cs @@ -75,6 +75,7 @@ namespace FlaxEditor.Content /// Context proxy object for C++ script files. /// /// + [ContentContextMenu("New/C++/C++ Script")] public class CppScriptProxy : CppProxy { /// @@ -98,6 +99,7 @@ namespace FlaxEditor.Content /// Context proxy object for C++ Json Asset files. /// /// + [ContentContextMenu("New/C++/C++ Function Library")] public class CppStaticClassProxy : CppProxy { /// @@ -115,6 +117,7 @@ namespace FlaxEditor.Content /// Context proxy object for C++ Json Asset files. /// /// + [ContentContextMenu("New/C++/C++ Json Asset")] public class CppAssetProxy : CppProxy { /// diff --git a/Source/Editor/Content/Proxy/GameplayGlobalsProxy.cs b/Source/Editor/Content/Proxy/GameplayGlobalsProxy.cs index 7c8a7268d..af391a8c4 100644 --- a/Source/Editor/Content/Proxy/GameplayGlobalsProxy.cs +++ b/Source/Editor/Content/Proxy/GameplayGlobalsProxy.cs @@ -11,6 +11,7 @@ namespace FlaxEditor.Content /// A asset proxy object. /// /// + [ContentContextMenu("New/Gameplay Globals")] public class GameplayGlobalsProxy : BinaryAssetProxy { /// diff --git a/Source/Editor/Content/Proxy/JsonAssetProxy.cs b/Source/Editor/Content/Proxy/JsonAssetProxy.cs index 3f2863f74..8bfaae384 100644 --- a/Source/Editor/Content/Proxy/JsonAssetProxy.cs +++ b/Source/Editor/Content/Proxy/JsonAssetProxy.cs @@ -22,6 +22,7 @@ namespace FlaxEditor.Content /// Json assets proxy. /// /// + [ContentContextMenu("New/Json Asset")] public abstract class JsonAssetProxy : JsonAssetBaseProxy { /// diff --git a/Source/Editor/Content/Proxy/MaterialFunctionProxy.cs b/Source/Editor/Content/Proxy/MaterialFunctionProxy.cs index 702f04706..f8c868cb5 100644 --- a/Source/Editor/Content/Proxy/MaterialFunctionProxy.cs +++ b/Source/Editor/Content/Proxy/MaterialFunctionProxy.cs @@ -11,6 +11,7 @@ namespace FlaxEditor.Content /// A asset proxy object. /// /// + [ContentContextMenu("New/Material/Material Function")] public class MaterialFunctionProxy : BinaryAssetProxy { /// diff --git a/Source/Editor/Content/Proxy/MaterialInstanceProxy.cs b/Source/Editor/Content/Proxy/MaterialInstanceProxy.cs index 663c191ef..60ff9bc51 100644 --- a/Source/Editor/Content/Proxy/MaterialInstanceProxy.cs +++ b/Source/Editor/Content/Proxy/MaterialInstanceProxy.cs @@ -14,6 +14,7 @@ namespace FlaxEditor.Content /// A asset proxy object. /// /// + [ContentContextMenu("New/Material/Material Instance")] public class MaterialInstanceProxy : BinaryAssetProxy { private MaterialPreview _preview; diff --git a/Source/Editor/Content/Proxy/MaterialProxy.cs b/Source/Editor/Content/Proxy/MaterialProxy.cs index e2e74eb14..68c9c81b7 100644 --- a/Source/Editor/Content/Proxy/MaterialProxy.cs +++ b/Source/Editor/Content/Proxy/MaterialProxy.cs @@ -15,6 +15,7 @@ namespace FlaxEditor.Content /// A asset proxy object. /// /// + [ContentContextMenu("New/Material/Material")] public class MaterialProxy : BinaryAssetProxy { private MaterialPreview _preview; diff --git a/Source/Editor/Content/Proxy/ParticleEmitterFunctionProxy.cs b/Source/Editor/Content/Proxy/ParticleEmitterFunctionProxy.cs index 24cc22690..b508bb818 100644 --- a/Source/Editor/Content/Proxy/ParticleEmitterFunctionProxy.cs +++ b/Source/Editor/Content/Proxy/ParticleEmitterFunctionProxy.cs @@ -11,6 +11,7 @@ namespace FlaxEditor.Content /// A asset proxy object. /// /// + [ContentContextMenu("New/Particles/Particle Emitter Function")] public class ParticleEmitterFunctionProxy : BinaryAssetProxy { /// diff --git a/Source/Editor/Content/Proxy/ParticleEmitterProxy.cs b/Source/Editor/Content/Proxy/ParticleEmitterProxy.cs index 089c614da..be07720d2 100644 --- a/Source/Editor/Content/Proxy/ParticleEmitterProxy.cs +++ b/Source/Editor/Content/Proxy/ParticleEmitterProxy.cs @@ -15,6 +15,7 @@ namespace FlaxEditor.Content /// A asset proxy object. /// /// + [ContentContextMenu("New/Particles/Particle Emitter")] public class ParticleEmitterProxy : BinaryAssetProxy { private ParticleEmitterPreview _preview; diff --git a/Source/Editor/Content/Proxy/ParticleSystemProxy.cs b/Source/Editor/Content/Proxy/ParticleSystemProxy.cs index c19e84a78..38301afdf 100644 --- a/Source/Editor/Content/Proxy/ParticleSystemProxy.cs +++ b/Source/Editor/Content/Proxy/ParticleSystemProxy.cs @@ -39,6 +39,7 @@ namespace FlaxEditor.Content /// A asset proxy object. /// /// + [ContentContextMenu("New/Particles/Particle System")] public class ParticleSystemProxy : BinaryAssetProxy { private ParticleSystemPreview _preview; diff --git a/Source/Editor/Content/Proxy/PrefabProxy.cs b/Source/Editor/Content/Proxy/PrefabProxy.cs index 64eb1d9f1..a4610b25a 100644 --- a/Source/Editor/Content/Proxy/PrefabProxy.cs +++ b/Source/Editor/Content/Proxy/PrefabProxy.cs @@ -15,6 +15,7 @@ namespace FlaxEditor.Content /// Content proxy for . /// /// + [ContentContextMenu("New/Prefab")] public sealed class PrefabProxy : JsonAssetBaseProxy { private PrefabPreview _preview; diff --git a/Source/Editor/Content/Proxy/SceneProxy.cs b/Source/Editor/Content/Proxy/SceneProxy.cs index 7856fa064..cecd825a1 100644 --- a/Source/Editor/Content/Proxy/SceneProxy.cs +++ b/Source/Editor/Content/Proxy/SceneProxy.cs @@ -10,6 +10,7 @@ namespace FlaxEditor.Content /// Content proxy for . /// /// + [ContentContextMenu("New/Scene")] public sealed class SceneProxy : JsonAssetBaseProxy { /// diff --git a/Source/Editor/Content/Proxy/SettingsProxy.cs b/Source/Editor/Content/Proxy/SettingsProxy.cs index 0bbd2830f..651ba62d2 100644 --- a/Source/Editor/Content/Proxy/SettingsProxy.cs +++ b/Source/Editor/Content/Proxy/SettingsProxy.cs @@ -11,6 +11,7 @@ namespace FlaxEditor.Content /// Content proxy for json settings assets (e.g or ). /// /// + [ContentContextMenu("New/Settings")] public class SettingsProxy : JsonAssetProxy { private readonly Type _type; diff --git a/Source/Editor/Content/Proxy/ShaderSourceProxy.cs b/Source/Editor/Content/Proxy/ShaderSourceProxy.cs index 86020c997..9b2ec22d5 100644 --- a/Source/Editor/Content/Proxy/ShaderSourceProxy.cs +++ b/Source/Editor/Content/Proxy/ShaderSourceProxy.cs @@ -13,6 +13,7 @@ namespace FlaxEditor.Content /// Context proxy object for shader source files (represented by ). /// /// + [ContentContextMenu("New/Shader Source")] public class ShaderSourceProxy : ContentProxy { /// diff --git a/Source/Editor/Content/Proxy/SkeletonMaskProxy.cs b/Source/Editor/Content/Proxy/SkeletonMaskProxy.cs index 2a90820b0..ab78a1e09 100644 --- a/Source/Editor/Content/Proxy/SkeletonMaskProxy.cs +++ b/Source/Editor/Content/Proxy/SkeletonMaskProxy.cs @@ -11,6 +11,7 @@ namespace FlaxEditor.Content /// A asset proxy object. /// /// + [ContentContextMenu("New/Animation/Skeleton Mask")] public class SkeletonMaskProxy : BinaryAssetProxy { /// diff --git a/Source/Editor/Content/Proxy/VisualScriptProxy.cs b/Source/Editor/Content/Proxy/VisualScriptProxy.cs index b065a4fcd..afd105131 100644 --- a/Source/Editor/Content/Proxy/VisualScriptProxy.cs +++ b/Source/Editor/Content/Proxy/VisualScriptProxy.cs @@ -15,6 +15,7 @@ namespace FlaxEditor.Content /// A asset proxy object. /// /// + [ContentContextMenu("New/Visual Script")] public class VisualScriptProxy : BinaryAssetProxy, IScriptTypesContainer { internal VisualScriptProxy() diff --git a/Source/Editor/CustomEditors/Dedicated/ParticleEffectEditor.cs b/Source/Editor/CustomEditors/Dedicated/ParticleEffectEditor.cs index c78700bf7..3356768fd 100644 --- a/Source/Editor/CustomEditors/Dedicated/ParticleEffectEditor.cs +++ b/Source/Editor/CustomEditors/Dedicated/ParticleEffectEditor.cs @@ -94,6 +94,12 @@ namespace FlaxEditor.CustomEditors.Dedicated } Refresh(); var parameters = effect.Parameters; + if (parameters.Length == 0) + { + base.RefreshRootChild(); + return; + } + for (int i = 0; i < ChildrenEditors.Count; i++) { if (_isActive != effect.IsActive || _parametersVersion != effect.ParametersVersion) diff --git a/Source/Editor/GUI/Input/ValueBox.cs b/Source/Editor/GUI/Input/ValueBox.cs index 5038dab20..7f4fb51e0 100644 --- a/Source/Editor/GUI/Input/ValueBox.cs +++ b/Source/Editor/GUI/Input/ValueBox.cs @@ -248,7 +248,7 @@ namespace FlaxEditor.GUI.Input // Hide cursor and cache location Cursor = CursorType.Hidden; - _mouseClickedPosition = location; + _mouseClickedPosition = PointToWindow(location); _cursorChanged = true; SlidingStart?.Invoke(); @@ -293,7 +293,7 @@ namespace FlaxEditor.GUI.Input if (button == MouseButton.Left && _isSliding) { // End sliding and return mouse to original location - Root.MousePosition = ScreenPos + _mouseClickedPosition; + RootWindow.MousePosition = _mouseClickedPosition; EndSliding(); return true; } diff --git a/Source/Editor/Modules/SourceCodeEditing/CachedTypesCollection.cs b/Source/Editor/Modules/SourceCodeEditing/CachedTypesCollection.cs index 32e0fe17f..cacaa1db4 100644 --- a/Source/Editor/Modules/SourceCodeEditing/CachedTypesCollection.cs +++ b/Source/Editor/Modules/SourceCodeEditing/CachedTypesCollection.cs @@ -57,7 +57,7 @@ namespace FlaxEditor.Modules.SourceCodeEditing _list.Clear(); _hasValidData = true; - Editor.Log("Searching for valid " + _type); + Editor.Log("Searching for valid " + (_type != ScriptType.Null ? _type.ToString() : "types")); Profiler.BeginEvent("Search " + _type); var start = DateTime.Now; diff --git a/Source/Editor/Utilities/Utils.cs b/Source/Editor/Utilities/Utils.cs index 5fdaae91a..74ffb721b 100644 --- a/Source/Editor/Utilities/Utils.cs +++ b/Source/Editor/Utilities/Utils.cs @@ -738,9 +738,10 @@ namespace FlaxEditor.Utilities var settings = CreateWindowSettings.Default; settings.ActivateWhenFirstShown = true; - settings.AllowMaximize = false; + settings.AllowMaximize = true; settings.AllowMinimize = false; - settings.HasSizingFrame = false; + settings.HasSizingFrame = true; + settings.HasBorder = true; settings.StartPosition = WindowStartPosition.CenterParent; settings.Size = new Float2(500, 600) * (parentWindow?.DpiScale ?? Platform.DpiScale); settings.Parent = parentWindow; @@ -754,10 +755,25 @@ namespace FlaxEditor.Utilities }; copyButton.Clicked += () => Clipboard.Text = source; - var sourceTextBox = new TextBox(true, 2, copyButton.Bottom + 4, settings.Size.X - 4); - sourceTextBox.Height = settings.Size.Y - sourceTextBox.Top - 2; + var backPanel = new Panel + { + AnchorPreset = AnchorPresets.StretchAll, + Offsets = new Margin(0, 0, copyButton.Bottom + 4, 0), + ScrollBars = ScrollBars.Both, + IsScrollable = true, + Parent = dialog.GUI, + }; + + var sourceTextBox = new TextBox(true, 0, 0, 0); + sourceTextBox.Parent = backPanel; + sourceTextBox.AnchorPreset = AnchorPresets.HorizontalStretchTop; sourceTextBox.Text = source; - sourceTextBox.Parent = dialog.GUI; + sourceTextBox.Height = sourceTextBox.TextSize.Y; + sourceTextBox.IsReadOnly = true; + sourceTextBox.IsMultilineScrollable = false; + sourceTextBox.IsScrollable = true; + + backPanel.SizeChanged += control => { sourceTextBox.Width = (control.Size.X >= sourceTextBox.TextSize.X) ? control.Width : sourceTextBox.TextSize.X + 30; }; dialog.Show(); dialog.Focus(); diff --git a/Source/Editor/Viewport/EditorViewport.cs b/Source/Editor/Viewport/EditorViewport.cs index 96b0abf04..4e44ae833 100644 --- a/Source/Editor/Viewport/EditorViewport.cs +++ b/Source/Editor/Viewport/EditorViewport.cs @@ -539,6 +539,7 @@ namespace FlaxEditor.Viewport // Orthographic { var ortho = ViewWidgetButtonMenu.AddButton("Orthographic"); + ortho.CloseMenuOnClick = false; var orthoValue = new CheckBox(90, 2, _isOrtho) { Parent = ortho @@ -578,6 +579,7 @@ namespace FlaxEditor.Viewport // Field of View { var fov = ViewWidgetButtonMenu.AddButton("Field Of View"); + fov.CloseMenuOnClick = false; var fovValue = new FloatValueBox(1, 90, 2, 70.0f, 35.0f, 160.0f, 0.1f) { Parent = fov @@ -594,6 +596,7 @@ namespace FlaxEditor.Viewport // Ortho Scale { var orthoSize = ViewWidgetButtonMenu.AddButton("Ortho Scale"); + orthoSize.CloseMenuOnClick = false; var orthoSizeValue = new FloatValueBox(_orthoSize, 90, 2, 70.0f, 0.001f, 100000.0f, 0.01f) { Parent = orthoSize @@ -610,6 +613,7 @@ namespace FlaxEditor.Viewport // Near Plane { var nearPlane = ViewWidgetButtonMenu.AddButton("Near Plane"); + nearPlane.CloseMenuOnClick = false; var nearPlaneValue = new FloatValueBox(2.0f, 90, 2, 70.0f, 0.001f, 1000.0f) { Parent = nearPlane @@ -621,6 +625,7 @@ namespace FlaxEditor.Viewport // Far Plane { var farPlane = ViewWidgetButtonMenu.AddButton("Far Plane"); + farPlane.CloseMenuOnClick = false; var farPlaneValue = new FloatValueBox(1000, 90, 2, 70.0f, 10.0f) { Parent = farPlane @@ -632,6 +637,7 @@ namespace FlaxEditor.Viewport // Brightness { var brightness = ViewWidgetButtonMenu.AddButton("Brightness"); + brightness.CloseMenuOnClick = false; var brightnessValue = new FloatValueBox(1.0f, 90, 2, 70.0f, 0.001f, 10.0f, 0.001f) { Parent = brightness @@ -643,6 +649,7 @@ namespace FlaxEditor.Viewport // Resolution { var resolution = ViewWidgetButtonMenu.AddButton("Resolution"); + resolution.CloseMenuOnClick = false; var resolutionValue = new FloatValueBox(1.0f, 90, 2, 70.0f, 0.1f, 4.0f, 0.001f) { Parent = resolution @@ -654,6 +661,7 @@ namespace FlaxEditor.Viewport // Invert Panning { var invert = ViewWidgetButtonMenu.AddButton("Invert Panning"); + invert.CloseMenuOnClick = false; var invertValue = new CheckBox(90, 2, _invertPanning) { Parent = invert diff --git a/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs b/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs index c80ecf0c5..402fa7d52 100644 --- a/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs +++ b/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs @@ -263,21 +263,22 @@ namespace FlaxEditor.Windows.Assets } if (attribute == null) continue; - var splitPath = attribute.Path.Split('/'); + var parts = attribute.Path.Split('/'); ContextMenuChildMenu childCM = null; bool mainCM = true; - for (int i = 0; i < splitPath?.Length; i++) + for (int i = 0; i < parts.Length; i++) { - if (i == splitPath.Length - 1) + var part = parts[i].Trim(); + if (i == parts.Length - 1) { if (mainCM) { - contextMenu.AddButton(splitPath[i].Trim(), () => Spawn(actorType.Type)); + contextMenu.AddButton(part, () => Spawn(actorType.Type)); mainCM = false; } - else + else if (childCM != null) { - childCM?.ContextMenu.AddButton(splitPath[i].Trim(), () => Spawn(actorType.Type)); + childCM.ContextMenu.AddButton(part, () => Spawn(actorType.Type)); childCM.ContextMenu.AutoSort = true; } } @@ -285,14 +286,15 @@ namespace FlaxEditor.Windows.Assets { if (mainCM) { - childCM = contextMenu.GetOrAddChildMenu(splitPath[i].Trim()); + childCM = contextMenu.GetOrAddChildMenu(part); + childCM.ContextMenu.AutoSort = true; mainCM = false; } - else + else if (childCM != null) { - childCM = childCM?.ContextMenu.GetOrAddChildMenu(splitPath[i].Trim()); + childCM = childCM.ContextMenu.GetOrAddChildMenu(part); + childCM.ContextMenu.AutoSort = true; } - childCM.ContextMenu.AutoSort = true; } } } diff --git a/Source/Editor/Windows/ContentWindow.ContextMenu.cs b/Source/Editor/Windows/ContentWindow.ContextMenu.cs index 7368c4efd..89f13d6a3 100644 --- a/Source/Editor/Windows/ContentWindow.ContextMenu.cs +++ b/Source/Editor/Windows/ContentWindow.ContextMenu.cs @@ -3,6 +3,7 @@ using System; using FlaxEditor.Content; using FlaxEditor.GUI.ContextMenu; +using FlaxEditor.Scripting; using FlaxEngine; using FlaxEngine.Assertions; using FlaxEngine.Json; @@ -39,7 +40,6 @@ namespace FlaxEditor.Windows // Create context menu ContextMenuButton b; - ContextMenuChildMenu c; ContextMenu cm = new ContextMenu { Tag = item @@ -150,20 +150,83 @@ namespace FlaxEditor.Windows cm.AddButton("New folder", NewFolder); } - c = cm.AddChildMenu("New"); - c.ContextMenu.Tag = item; - c.ContextMenu.AutoSort = true; - int newItems = 0; - for (int i = 0; i < Editor.ContentDatabase.Proxy.Count; i++) + // Loop through each proxy and user defined json type and add them to the context menu + var actorType = new ScriptType(typeof(Actor)); + var scriptType = new ScriptType(typeof(Script)); + foreach (var type in Editor.CodeEditing.All.Get()) { - var p = Editor.ContentDatabase.Proxy[i]; + if (type.IsAbstract) + continue; + if (actorType.IsAssignableFrom(type) || scriptType.IsAssignableFrom(type)) + continue; + + // Get attribute + ContentContextMenuAttribute attribute = null; + foreach (var typeAttribute in type.GetAttributes(true)) + { + if (typeAttribute is ContentContextMenuAttribute contentContextMenuAttribute) + { + attribute = contentContextMenuAttribute; + break; + } + } + if (attribute == null) + continue; + + // Get context proxy + ContentProxy p; + if (type.Type.IsSubclassOf(typeof(ContentProxy))) + { + p = Editor.ContentDatabase.Proxy.Find(x => x.GetType() == type.Type); + } + else + { + // User can use attribute to put their own assets into the content context menu + var generic = typeof(SpawnableJsonAssetProxy<>).MakeGenericType(type.Type); + var instance = Activator.CreateInstance(generic); + p = instance as AssetProxy; + } + if (p == null) + continue; + if (p.CanCreate(folder)) { - c.ContextMenu.AddButton(p.Name, () => NewItem(p)); - newItems++; + var parts = attribute.Path.Split('/'); + ContextMenuChildMenu childCM = null; + bool mainCM = true; + for (int i = 0; i < parts?.Length; i++) + { + var part = parts[i].Trim(); + if (i == parts.Length - 1) + { + if (mainCM) + { + cm.AddButton(part, () => NewItem(p)); + mainCM = false; + } + else if (childCM != null) + { + childCM.ContextMenu.AddButton(part, () => NewItem(p)); + childCM.ContextMenu.AutoSort = true; + } + } + else + { + if (mainCM) + { + childCM = cm.GetOrAddChildMenu(part); + childCM.ContextMenu.AutoSort = true; + mainCM = false; + } + else if (childCM != null) + { + childCM = childCM.ContextMenu.GetOrAddChildMenu(part); + childCM.ContextMenu.AutoSort = true; + } + } + } } } - c.Enabled = newItems > 0; if (folder.CanHaveAssets) { diff --git a/Source/Editor/Windows/SceneTreeWindow.ContextMenu.cs b/Source/Editor/Windows/SceneTreeWindow.ContextMenu.cs index a9c946c46..1378ace9c 100644 --- a/Source/Editor/Windows/SceneTreeWindow.ContextMenu.cs +++ b/Source/Editor/Windows/SceneTreeWindow.ContextMenu.cs @@ -65,7 +65,6 @@ namespace FlaxEditor.Windows { if (actorType.IsAbstract) continue; - ActorContextMenuAttribute attribute = null; foreach (var e in actorType.GetAttributes(true)) { @@ -77,40 +76,42 @@ namespace FlaxEditor.Windows } if (attribute == null) continue; - var splitPath = attribute?.Path.Split('/'); + var parts = attribute.Path.Split('/'); ContextMenuChildMenu childCM = convertMenu; bool mainCM = true; - for (int i = 0; i < splitPath?.Length; i++) + for (int i = 0; i < parts.Length; i++) { - if (i == splitPath.Length - 1) + var part = parts[i].Trim(); + if (i == parts.Length - 1) { if (mainCM) { - convertMenu.ContextMenu.AddButton(splitPath[i].Trim(), () => Editor.SceneEditing.Convert(actorType.Type)); + convertMenu.ContextMenu.AddButton(part, () => Editor.SceneEditing.Convert(actorType.Type)); mainCM = false; } else { - childCM?.ContextMenu.AddButton(splitPath[i].Trim(), () => Editor.SceneEditing.Convert(actorType.Type)); + childCM.ContextMenu.AddButton(part, () => Editor.SceneEditing.Convert(actorType.Type)); childCM.ContextMenu.AutoSort = true; } } else { // Remove new path for converting menu - if (splitPath[i] == "New") + if (parts[i] == "New") continue; if (mainCM) { - childCM = convertMenu.ContextMenu.GetOrAddChildMenu(splitPath[i].Trim()); + childCM = convertMenu.ContextMenu.GetOrAddChildMenu(part); + childCM.ContextMenu.AutoSort = true; mainCM = false; } else { - childCM = childCM?.ContextMenu.GetOrAddChildMenu(splitPath[i].Trim()); + childCM = childCM.ContextMenu.GetOrAddChildMenu(part); + childCM.ContextMenu.AutoSort = true; } - childCM.ContextMenu.AutoSort = true; } } } diff --git a/Source/Engine/Graphics/Materials/MaterialInfo.h b/Source/Engine/Graphics/Materials/MaterialInfo.h index 876246e0c..dfdcdc9be 100644 --- a/Source/Engine/Graphics/Materials/MaterialInfo.h +++ b/Source/Engine/Graphics/Materials/MaterialInfo.h @@ -435,6 +435,11 @@ API_ENUM() enum class MaterialSceneTextures /// The material shading mode. /// ShadingModel = 10, + + /// + /// The scene world-space position (relative to the render view origin). + /// + WorldPosition = 11, }; /// diff --git a/Source/Engine/Graphics/Materials/MaterialParams.cpp b/Source/Engine/Graphics/Materials/MaterialParams.cpp index 85c47a683..538380e7c 100644 --- a/Source/Engine/Graphics/Materials/MaterialParams.cpp +++ b/Source/Engine/Graphics/Materials/MaterialParams.cpp @@ -439,6 +439,7 @@ void MaterialParameter::Bind(BindMeta& meta) const switch (type) { case MaterialSceneTextures::SceneDepth: + case MaterialSceneTextures::WorldPosition: view = meta.CanSampleDepth ? meta.Buffers->DepthBuffer->GetDescription().Flags & GPUTextureFlags::ReadOnlyDepthView ? meta.Buffers->DepthBuffer->ViewReadOnlyDepth() diff --git a/Source/Engine/Graphics/Materials/PostFxMaterialShader.cpp b/Source/Engine/Graphics/Materials/PostFxMaterialShader.cpp index 4c2caec84..ff1ff3fb6 100644 --- a/Source/Engine/Graphics/Materials/PostFxMaterialShader.cpp +++ b/Source/Engine/Graphics/Materials/PostFxMaterialShader.cpp @@ -18,6 +18,7 @@ PACK_STRUCT(struct PostFxMaterialShaderData { Float4 ViewInfo; Float4 ScreenSize; Float4 TemporalAAJitter; + Matrix InverseViewProjectionMatrix; }); void PostFxMaterialShader::Bind(BindParameters& params) @@ -44,6 +45,7 @@ void PostFxMaterialShader::Bind(BindParameters& params) // Setup material constants { Matrix::Transpose(view.View, materialData->ViewMatrix); + Matrix::Transpose(view.IVP, materialData->InverseViewProjectionMatrix); materialData->ViewPos = view.Position; materialData->ViewFar = view.Far; materialData->ViewDir = view.Direction; diff --git a/Source/Engine/Graphics/Shaders/Cache/ShaderAssetBase.cpp b/Source/Engine/Graphics/Shaders/Cache/ShaderAssetBase.cpp index 9ad9f894e..656424a8b 100644 --- a/Source/Engine/Graphics/Shaders/Cache/ShaderAssetBase.cpp +++ b/Source/Engine/Graphics/Shaders/Cache/ShaderAssetBase.cpp @@ -113,6 +113,11 @@ bool ShaderAssetBase::Save() bool IsValidShaderCache(DataContainer& shaderCache, Array& includes) { + if (shaderCache.Length() == 0) + { + return false; + } + MemoryReadStream stream(shaderCache.Get(), shaderCache.Length()); // Read cache format version diff --git a/Source/Engine/Particles/Graph/GPU/ParticleEmitterGraph.GPU.Textures.cpp b/Source/Engine/Particles/Graph/GPU/ParticleEmitterGraph.GPU.Textures.cpp index 823ddbccd..05059f05e 100644 --- a/Source/Engine/Particles/Graph/GPU/ParticleEmitterGraph.GPU.Textures.cpp +++ b/Source/Engine/Particles/Graph/GPU/ParticleEmitterGraph.GPU.Textures.cpp @@ -245,6 +245,9 @@ void ParticleEmitterGPUGenerator::ProcessGroupTextures(Box* box, Node* node, Val value = writeLocal(VariantType::Int, String::Format(TEXT("(int)({0}.a * 3.999)"), gBuffer1Sample.Value), node); break; } + case MaterialSceneTextures::WorldPosition: + value = Value::Zero; // Not implemented + break; default: { // Sample single texture diff --git a/Source/Engine/Physics/PhysicalMaterial.h b/Source/Engine/Physics/PhysicalMaterial.h index 0d8356546..b46c1a294 100644 --- a/Source/Engine/Physics/PhysicalMaterial.h +++ b/Source/Engine/Physics/PhysicalMaterial.h @@ -8,7 +8,7 @@ /// /// Physical materials are used to define the response of a physical object when interacting dynamically with the world. /// -API_CLASS() class FLAXENGINE_API PhysicalMaterial final : public ISerializable +API_CLASS(Attributes = "ContentContextMenu(\"New/Physics/Physical Material\")") class FLAXENGINE_API PhysicalMaterial final : public ISerializable { API_AUTO_SERIALIZATION(); DECLARE_SCRIPTING_TYPE_MINIMAL(PhysicalMaterial); diff --git a/Source/Engine/Scripting/Attributes/Editor/ContentContextMenu.cs b/Source/Engine/Scripting/Attributes/Editor/ContentContextMenu.cs new file mode 100644 index 000000000..ee9956ede --- /dev/null +++ b/Source/Engine/Scripting/Attributes/Editor/ContentContextMenu.cs @@ -0,0 +1,26 @@ +using System; + +namespace FlaxEngine +{ + /// + /// This attribute is used to show content items that can be created in the content browser context menu. Separate the subcontext menus with a /. + /// + [Serializable] + [AttributeUsage(AttributeTargets.Class)] + public class ContentContextMenuAttribute : Attribute + { + /// + /// The path to be used in the context menu + /// + public string Path; + + /// + /// Initializes a new instance of the class. + /// + /// The path to use to create the context menu + public ContentContextMenuAttribute(string path) + { + Path = path; + } + } +} diff --git a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Texture.cpp b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Texture.cpp deleted file mode 100644 index 87ce827fb..000000000 --- a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Texture.cpp +++ /dev/null @@ -1,166 +0,0 @@ -// Copyright (c) 2012-2022 Wojciech Figat. All rights reserved. - -#if COMPILE_WITH_MATERIAL_GRAPH - -#include "MaterialGenerator.h" - -MaterialValue* MaterialGenerator::sampleTextureRaw(Node* caller, Value& value, Box* box, SerializedMaterialParam* texture) -{ - ASSERT(texture && box); - - // Cache data - const auto parent = box->GetParent>(); - const bool isCubemap = texture->Type == MaterialParameterType::CubeTexture; - const bool isArray = texture->Type == MaterialParameterType::GPUTextureArray; - const bool isVolume = texture->Type == MaterialParameterType::GPUTextureVolume; - const bool isNormalMap = texture->Type == MaterialParameterType::NormalMap; - const bool canUseSample = CanUseSample(_treeType); - MaterialGraphBox* valueBox = parent->GetBox(1); - - // Check if has variable assigned - if (texture->Type != MaterialParameterType::Texture - && texture->Type != MaterialParameterType::NormalMap - && texture->Type != MaterialParameterType::SceneTexture - && texture->Type != MaterialParameterType::GPUTexture - && texture->Type != MaterialParameterType::GPUTextureVolume - && texture->Type != MaterialParameterType::GPUTextureCube - && texture->Type != MaterialParameterType::GPUTextureArray - && texture->Type != MaterialParameterType::CubeTexture) - { - OnError(caller, box, TEXT("No parameter for texture sample node.")); - return nullptr; - } - - // Check if it's 'Object' box that is using only texture object without sampling - if (box->ID == 6) - { - // Return texture object - value.Value = texture->ShaderName; - value.Type = VariantType::Object; - return nullptr; - } - - // Check if hasn't been sampled during that tree eating - if (valueBox->Cache.IsInvalid()) - { - // Check if use custom UVs - String uv; - MaterialGraphBox* uvBox = parent->GetBox(0); - bool useCustomUVs = uvBox->HasConnection(); - bool use3dUVs = isCubemap || isArray || isVolume; - if (useCustomUVs) - { - // Get custom UVs - auto textureParamId = texture->ID; - ASSERT(textureParamId.IsValid()); - MaterialValue v = tryGetValue(uvBox, getUVs); - uv = MaterialValue::Cast(v, use3dUVs ? VariantType::Float3 : VariantType::Float2).Value; - - // Restore texture (during tryGetValue pointer could go invalid) - texture = findParam(textureParamId); - ASSERT(texture); - } - else - { - // Use default UVs - uv = use3dUVs ? TEXT("float3(input.TexCoord.xy, 0)") : TEXT("input.TexCoord.xy"); - } - - // Select sampler - // TODO: add option for texture groups and per texture options like wrap mode etc. - // TODO: changing texture sampler option - const Char* sampler = TEXT("SamplerLinearWrap"); - - // Sample texture - if (isNormalMap) - { - const Char* format = canUseSample ? TEXT("{0}.Sample({1}, {2}).xyz") : TEXT("{0}.SampleLevel({1}, {2}, 0).xyz"); - - // Sample encoded normal map - const String sampledValue = String::Format(format, texture->ShaderName, sampler, uv); - const auto normalVector = writeLocal(VariantType::Float3, sampledValue, parent); - - // Decode normal vector - _writer.Write(TEXT("\t{0}.xy = {0}.xy * 2.0 - 1.0;\n"), normalVector.Value); - _writer.Write(TEXT("\t{0}.z = sqrt(saturate(1.0 - dot({0}.xy, {0}.xy)));\n"), normalVector.Value); - valueBox->Cache = normalVector; - } - else - { - // Select format string based on texture type - const Char* format; - /*if (isCubemap) - { - MISSING_CODE("sampling cube maps and 3d texture in material generator"); - //format = TEXT("SAMPLE_CUBEMAP({0}, {1})"); - } - else*/ - { - /*if (useCustomUVs) - { - createGradients(writer, parent); - format = TEXT("SAMPLE_TEXTURE_GRAD({0}, {1}, {2}, {3})"); - } - else*/ - { - format = canUseSample ? TEXT("{0}.Sample({1}, {2})") : TEXT("{0}.SampleLevel({1}, {2}, 0)"); - } - } - - // Sample texture - String sampledValue = String::Format(format, texture->ShaderName, sampler, uv, _ddx.Value, _ddy.Value); - valueBox->Cache = writeLocal(VariantType::Float4, sampledValue, parent); - } - } - - return &valueBox->Cache; -} - -void MaterialGenerator::sampleTexture(Node* caller, Value& value, Box* box, SerializedMaterialParam* texture) -{ - const auto sample = sampleTextureRaw(caller, value, box, texture); - if (sample == nullptr) - return; - - // Set result values based on box ID - switch (box->ID) - { - case 1: - value = *sample; - break; - case 2: - value.Value = sample->Value + _subs[0]; - break; - case 3: - value.Value = sample->Value + _subs[1]; - break; - case 4: - value.Value = sample->Value + _subs[2]; - break; - case 5: - value.Value = sample->Value + _subs[3]; - break; - default: CRASH; - break; - } - value.Type = box->Type.Type; -} - -void MaterialGenerator::sampleSceneDepth(Node* caller, Value& value, Box* box) -{ - // Sample depth buffer - auto param = findOrAddSceneTexture(MaterialSceneTextures::SceneDepth); - const auto depthSample = sampleTextureRaw(caller, value, box, ¶m); - if (depthSample == nullptr) - return; - - // Linearize raw device depth - linearizeSceneDepth(caller, *depthSample, value); -} - -void MaterialGenerator::linearizeSceneDepth(Node* caller, const Value& depth, Value& value) -{ - value = writeLocal(VariantType::Float, String::Format(TEXT("ViewInfo.w / ({0}.x - ViewInfo.z)"), depth.Value), caller); -} - -#endif diff --git a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp index 0c71dc1ba..d6ae2719f 100644 --- a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp +++ b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp @@ -4,6 +4,165 @@ #include "MaterialGenerator.h" +MaterialValue* MaterialGenerator::sampleTextureRaw(Node* caller, Value& value, Box* box, SerializedMaterialParam* texture) +{ + ASSERT(texture && box); + + // Cache data + const auto parent = box->GetParent>(); + const bool isCubemap = texture->Type == MaterialParameterType::CubeTexture; + const bool isArray = texture->Type == MaterialParameterType::GPUTextureArray; + const bool isVolume = texture->Type == MaterialParameterType::GPUTextureVolume; + const bool isNormalMap = texture->Type == MaterialParameterType::NormalMap; + const bool canUseSample = CanUseSample(_treeType); + MaterialGraphBox* valueBox = parent->GetBox(1); + + // Check if has variable assigned + if (texture->Type != MaterialParameterType::Texture + && texture->Type != MaterialParameterType::NormalMap + && texture->Type != MaterialParameterType::SceneTexture + && texture->Type != MaterialParameterType::GPUTexture + && texture->Type != MaterialParameterType::GPUTextureVolume + && texture->Type != MaterialParameterType::GPUTextureCube + && texture->Type != MaterialParameterType::GPUTextureArray + && texture->Type != MaterialParameterType::CubeTexture) + { + OnError(caller, box, TEXT("No parameter for texture sample node.")); + return nullptr; + } + + // Check if it's 'Object' box that is using only texture object without sampling + if (box->ID == 6) + { + // Return texture object + value.Value = texture->ShaderName; + value.Type = VariantType::Object; + return nullptr; + } + + // Check if hasn't been sampled during that tree eating + if (valueBox->Cache.IsInvalid()) + { + // Check if use custom UVs + String uv; + MaterialGraphBox* uvBox = parent->GetBox(0); + bool useCustomUVs = uvBox->HasConnection(); + bool use3dUVs = isCubemap || isArray || isVolume; + if (useCustomUVs) + { + // Get custom UVs + auto textureParamId = texture->ID; + ASSERT(textureParamId.IsValid()); + MaterialValue v = tryGetValue(uvBox, getUVs); + uv = MaterialValue::Cast(v, use3dUVs ? VariantType::Float3 : VariantType::Float2).Value; + + // Restore texture (during tryGetValue pointer could go invalid) + texture = findParam(textureParamId); + ASSERT(texture); + } + else + { + // Use default UVs + uv = use3dUVs ? TEXT("float3(input.TexCoord.xy, 0)") : TEXT("input.TexCoord.xy"); + } + + // Select sampler + // TODO: add option for texture groups and per texture options like wrap mode etc. + // TODO: changing texture sampler option + const Char* sampler = TEXT("SamplerLinearWrap"); + + // Sample texture + if (isNormalMap) + { + const Char* format = canUseSample ? TEXT("{0}.Sample({1}, {2}).xyz") : TEXT("{0}.SampleLevel({1}, {2}, 0).xyz"); + + // Sample encoded normal map + const String sampledValue = String::Format(format, texture->ShaderName, sampler, uv); + const auto normalVector = writeLocal(VariantType::Float3, sampledValue, parent); + + // Decode normal vector + _writer.Write(TEXT("\t{0}.xy = {0}.xy * 2.0 - 1.0;\n"), normalVector.Value); + _writer.Write(TEXT("\t{0}.z = sqrt(saturate(1.0 - dot({0}.xy, {0}.xy)));\n"), normalVector.Value); + valueBox->Cache = normalVector; + } + else + { + // Select format string based on texture type + const Char* format; + /*if (isCubemap) + { + MISSING_CODE("sampling cube maps and 3d texture in material generator"); + //format = TEXT("SAMPLE_CUBEMAP({0}, {1})"); + } + else*/ + { + /*if (useCustomUVs) + { + createGradients(writer, parent); + format = TEXT("SAMPLE_TEXTURE_GRAD({0}, {1}, {2}, {3})"); + } + else*/ + { + format = canUseSample ? TEXT("{0}.Sample({1}, {2})") : TEXT("{0}.SampleLevel({1}, {2}, 0)"); + } + } + + // Sample texture + String sampledValue = String::Format(format, texture->ShaderName, sampler, uv, _ddx.Value, _ddy.Value); + valueBox->Cache = writeLocal(VariantType::Float4, sampledValue, parent); + } + } + + return &valueBox->Cache; +} + +void MaterialGenerator::sampleTexture(Node* caller, Value& value, Box* box, SerializedMaterialParam* texture) +{ + const auto sample = sampleTextureRaw(caller, value, box, texture); + if (sample == nullptr) + return; + + // Set result values based on box ID + switch (box->ID) + { + case 1: + value = *sample; + break; + case 2: + value.Value = sample->Value + _subs[0]; + break; + case 3: + value.Value = sample->Value + _subs[1]; + break; + case 4: + value.Value = sample->Value + _subs[2]; + break; + case 5: + value.Value = sample->Value + _subs[3]; + break; + default: CRASH; + break; + } + value.Type = box->Type.Type; +} + +void MaterialGenerator::sampleSceneDepth(Node* caller, Value& value, Box* box) +{ + // Sample depth buffer + auto param = findOrAddSceneTexture(MaterialSceneTextures::SceneDepth); + const auto depthSample = sampleTextureRaw(caller, value, box, ¶m); + if (depthSample == nullptr) + return; + + // Linearize raw device depth + linearizeSceneDepth(caller, *depthSample, value); +} + +void MaterialGenerator::linearizeSceneDepth(Node* caller, const Value& depth, Value& value) +{ + value = writeLocal(VariantType::Float, String::Format(TEXT("ViewInfo.w / ({0}.x - ViewInfo.z)"), depth.Value), caller); +} + void MaterialGenerator::ProcessGroupTextures(Box* box, Node* node, Value& value) { switch (node->TypeID) @@ -257,6 +416,23 @@ void MaterialGenerator::ProcessGroupTextures(Box* box, Node* node, Value& value) value = writeLocal(VariantType::Int, String::Format(TEXT("(int)({0}.a * 3.999)"), gBuffer1Sample->Value), node); break; } + case MaterialSceneTextures::WorldPosition: + { + auto depthParam = findOrAddSceneTexture(MaterialSceneTextures::SceneDepth); + auto depthSample = sampleTextureRaw(node, value, box, &depthParam); + if (depthSample == nullptr) + break; + const auto parent = box->GetParent>(); + MaterialGraphBox* uvBox = parent->GetBox(0); + bool useCustomUVs = uvBox->HasConnection(); + String uv; + if (useCustomUVs) + uv = MaterialValue::Cast(tryGetValue(uvBox, getUVs), VariantType::Float2).Value; + else + uv = TEXT("input.TexCoord.xy"); + value = writeLocal(VariantType::Float3, String::Format(TEXT("GetWorldPos({1}, {0}.rgb)"), depthSample->Value, uv), node); + break; + } default: { // Sample single texture diff --git a/Source/Engine/UI/GUI/Common/TextBoxBase.cs b/Source/Engine/UI/GUI/Common/TextBoxBase.cs index d78bd3d6b..340459c8f 100644 --- a/Source/Engine/UI/GUI/Common/TextBoxBase.cs +++ b/Source/Engine/UI/GUI/Common/TextBoxBase.cs @@ -229,6 +229,11 @@ namespace FlaxEngine.GUI [EditorOrder(529)] public bool ClipText { get; set; } = true; + /// + /// Gets or sets a value indicating whether you can scroll the text in the text box (eg. with a mouse wheel). + /// + public bool IsMultilineScrollable { get; set; } = true; + /// /// Gets or sets textbox background color when the control is selected (has focus). /// @@ -1157,7 +1162,7 @@ namespace FlaxEngine.GUI return true; // Multiline scroll - if (IsMultiline && _text.Length != 0) + if (IsMultiline && _text.Length != 0 && IsMultilineScrollable) { TargetViewOffset = Float2.Clamp(_targetViewOffset - new Float2(0, delta * 10.0f), Float2.Zero, new Float2(_targetViewOffset.X, _textSize.Y)); return true;