diff --git a/Content/Editor/Fonts/Inconsolata-Regular.flax b/Content/Editor/Fonts/Inconsolata-Regular.flax index d85ef610a..6046f2793 100644 --- a/Content/Editor/Fonts/Inconsolata-Regular.flax +++ b/Content/Editor/Fonts/Inconsolata-Regular.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:139e2f54a268f4febc753aaa9807a33a6eb2dd84e788c5b8b4f50ca8683d2b3b -size 93116 +oid sha256:e931314736f3fd3aaa7e22a83af1fc8512824e6d6c83cde9edfc21d69d83ad23 +size 93121 diff --git a/Content/Editor/Fonts/NotoSansSC-Regular.flax b/Content/Editor/Fonts/NotoSansSC-Regular.flax index 8f8f00157..c458b3255 100644 --- a/Content/Editor/Fonts/NotoSansSC-Regular.flax +++ b/Content/Editor/Fonts/NotoSansSC-Regular.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ddc7af5fab1dd4ad858ad2abf11c6c243d908345720892cd56242fbc9c33b02 -size 10560900 +oid sha256:b19ad5b09e6fe82f7547e13d8b5dc63b84fb8253654a1f98ef784109b3d33e80 +size 10560905 diff --git a/Content/Editor/Fonts/Roboto-Regular.flax b/Content/Editor/Fonts/Roboto-Regular.flax index 01251cac6..fca17395b 100644 --- a/Content/Editor/Fonts/Roboto-Regular.flax +++ b/Content/Editor/Fonts/Roboto-Regular.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c0584b3a3c186364464de155edece5cb6a8c46000676ac82a45f951f3a67fac5 -size 172194 +oid sha256:b96520c348ef1b63e7514dd739ce7440503e8b4462edd830a271b20dd60730c2 +size 172199 diff --git a/Content/Editor/Fonts/SegMDL2.flax b/Content/Editor/Fonts/SegMDL2.flax index b627849d9..ea9e3c4e2 100644 --- a/Content/Editor/Fonts/SegMDL2.flax +++ b/Content/Editor/Fonts/SegMDL2.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f7a9d723d6342b8a484dfb2ee784cef506b3311a6092be0e9de35f941d698ff5 -size 204150 +oid sha256:eeeb5ea36b751130cce7b03127f7d1c26b05e906252c3532ed83a3e154a82bf9 +size 204155 diff --git a/Content/Editor/Fonts/Segoe Media Center Regular.flax b/Content/Editor/Fonts/Segoe Media Center Regular.flax index 6c17d96bf..06efc4cf0 100644 --- a/Content/Editor/Fonts/Segoe Media Center Regular.flax +++ b/Content/Editor/Fonts/Segoe Media Center Regular.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c21443a111daede658cb634f56b4ae0838dbb2eaa75d4ba8ba74a4c77df44de8 -size 72798 +oid sha256:0c203e22557bbdb86f6b4c13f9fc954d88fff92911cc90706508be57506a5dd6 +size 72803 diff --git a/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs b/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs index aa1952525..16fe417b8 100644 --- a/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs +++ b/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs @@ -55,6 +55,21 @@ namespace FlaxEditor.GUI.ContextMenu private Window _window; private Control _previouslyFocused; + private static bool IsFinite(float value) + { + return !float.IsNaN(value) && !float.IsInfinity(value); + } + + private static bool IsValidWindowBounds(Float2 location, Float2 size) + { + return IsFinite(location.X) && + IsFinite(location.Y) && + IsFinite(size.X) && + IsFinite(size.Y) && + size.X > 0.0f && + size.Y > 0.0f; + } + /// /// Gets a value indicating whether use automatic popup direction fix based on the screen dimensions. /// @@ -190,6 +205,11 @@ namespace FlaxEditor.GUI.ContextMenu var dpiSize = Size * dpiScale; var locationWS = parent.PointToWindow(location); var locationSS = parentWin.PointToScreen(locationWS); + if (!IsValidWindowBounds(locationSS, dpiSize)) + { + Hide(); + return; + } var monitorBounds = Platform.GetMonitorBounds(locationSS); var rightBottomLocationSS = locationSS + dpiSize; bool isUp = false, isLeft = false; @@ -405,7 +425,31 @@ namespace FlaxEditor.GUI.ContextMenu { if (_window != null) { - _window.ClientSize = Size * _window.DpiScale; + var size = Size * _window.DpiScale; + if (!IsValidWindowBounds(_window.ClientBounds.Location, size)) + return; + _window.ClientSize = size; + } + } + + /// + /// Gets the popup window location in screen coordinates. + /// + protected Float2 WindowLocation => _window != null ? _window.ClientBounds.Location : Float2.Zero; + + /// + /// Moves the popup window to the given screen-space location. + /// + /// The popup window location in screen coordinates. + protected void MoveWindowTo(Float2 screenLocation) + { + if (_window != null) + { + var bounds = _window.ClientBounds; + if (!IsValidWindowBounds(screenLocation, bounds.Size)) + return; + bounds.Location = screenLocation; + _window.ClientBounds = bounds; } } diff --git a/Source/Editor/GUI/CurveEditor.Base.cs b/Source/Editor/GUI/CurveEditor.Base.cs index fc9abc08e..04b4b2f72 100644 --- a/Source/Editor/GUI/CurveEditor.Base.cs +++ b/Source/Editor/GUI/CurveEditor.Base.cs @@ -179,6 +179,28 @@ namespace FlaxEditor.GUI UpdateKeyframes(); } + internal static string GetComponentLabel(int component, Type valueType) + { + if (valueType == typeof(Color) || valueType == typeof(Color32)) + { + switch (component) + { + case 0: return "r"; + case 1: return "g"; + case 2: return "b"; + case 3: return "a"; + } + } + switch (component) + { + case 0: return "x"; + case 1: return "y"; + case 2: return "z"; + case 3: return "w"; + default: return (component + 1).ToString(); + } + } + /// /// Evaluates the animation curve value at the specified time. /// diff --git a/Source/Editor/GUI/CurveEditor.Contents.cs b/Source/Editor/GUI/CurveEditor.Contents.cs index 8cc2e24d8..1b928ae79 100644 --- a/Source/Editor/GUI/CurveEditor.Contents.cs +++ b/Source/Editor/GUI/CurveEditor.Contents.cs @@ -19,6 +19,16 @@ namespace FlaxEditor.GUI /// protected class ContentsBase : ContainerControl { + private const float DragStartDistance = 1.5f; + private const float DragStartDistanceSquared = DragStartDistance * DragStartDistance; + + private enum SelectionMode + { + Replace, + Add, + Remove, + } + private readonly CurveEditor _editor; internal bool _leftMouseDown; private bool _rightMouseDown; @@ -46,6 +56,77 @@ namespace FlaxEditor.GUI _editor = editor; } + private KeyframePoint GetKeyframePointAt(Float2 location, bool cycle) + { + return GetKeyframePointAt(location, cycle, out _); + } + + private KeyframePoint GetKeyframePointAt(Float2 location, bool cycle, out bool isStacked) + { + KeyframePoint firstHit = null; + KeyframePoint selectedHit = null; + KeyframePoint nextHitAfterSelected = null; + int hitsCount = 0; + for (int i = 0; i < _editor._points.Count; i++) + { + var point = _editor._points[i]; + if (!point.Visible || !point.Bounds.Contains(ref location)) + continue; + + if (hitsCount == 0) + firstHit = point; + if (selectedHit != null && nextHitAfterSelected == null) + nextHitAfterSelected = point; + if (selectedHit == null && point.IsSelected) + selectedHit = point; + hitsCount++; + } + isStacked = hitsCount > 1; + if (hitsCount == 0) + return null; + if (hitsCount == 1) + return firstHit; + if (selectedHit != null) + return cycle ? nextHitAfterSelected ?? firstHit : selectedHit; + return firstHit; + } + + private void SelectKeyframePoint(KeyframePoint keyframe, bool addToSelection) + { + if (!addToSelection) + { + if (_editor.KeyframesEditorContext != null) + _editor.KeyframesEditorContext.OnKeyframesDeselect(_editor); + else + _editor.ClearSelection(); + } + keyframe.IsSelected = true; + if (_editor.ShowCollapsed) + { + for (int i = 0; i < _editor._points.Count; i++) + { + var point = _editor._points[i]; + if (point.Index == keyframe.Index) + point.IsSelected = true; + } + } + _editor.UpdateTangents(); + } + + private void SelectKeyframeComponent(int keyframeIndex, int component) + { + if (_editor.KeyframesEditorContext != null) + _editor.KeyframesEditorContext.OnKeyframesDeselect(_editor); + else + _editor.ClearSelection(); + for (int i = 0; i < _editor._points.Count; i++) + { + var point = _editor._points[i]; + point.IsSelected = point.Index == keyframeIndex && point.Component == component; + } + _editor.UpdateTangents(); + } + private void UpdateSelectionRectangle() { var selectionRect = Rectangle.FromPoints(_leftMouseDownPos, _mousePos); @@ -55,14 +136,41 @@ namespace FlaxEditor.GUI UpdateSelection(ref selectionRect); } + private SelectionMode GetSelectionMode() + { + if (Root.GetKey(KeyboardKeys.Alt)) + return SelectionMode.Remove; + if (Root.GetKey(KeyboardKeys.Shift)) + return SelectionMode.Add; + return SelectionMode.Replace; + } + internal void UpdateSelection(ref Rectangle selectionRect) { + var mode = GetSelectionMode(); + // Find controls to select var children = _children; for (int i = 0; i < children.Count; i++) { if (children[i] is KeyframePoint p) - p.IsSelected = p.Bounds.Intersects(ref selectionRect); + { + var intersects = p.Bounds.Intersects(ref selectionRect); + switch (mode) + { + case SelectionMode.Replace: + p.IsSelected = intersects; + break; + case SelectionMode.Add: + if (intersects) + p.IsSelected = true; + break; + case SelectionMode.Remove: + if (intersects) + p.IsSelected = false; + break; + } + } } _editor.UpdateTangents(); } @@ -82,19 +190,15 @@ namespace FlaxEditor.GUI _editor.OnEditingStart(); } - internal void OnMove(Float2 location) + private bool MoveSelectedKeyframes(Float2 location) { - // Skip updating keyframes until move actual starts to be meaningful - if (Float2.Distance(ref _movingSelectionStartPosLock, ref location) < 1.5f) - return; - _movingSelectionStartPosLock = Float2.Minimum; - var viewRect = _editor._mainPanel.GetClientArea(); var locationKeyframes = PointToKeyframes(location, ref viewRect); var accessor = _editor.Accessor; var components = accessor.GetCurveComponents(); var snapEnabled = Root.GetKey(KeyboardKeys.Control); var snapGrid = snapEnabled ? _editor.GetGridSnap() : Float2.One; + var moved = false; for (var i = 0; i < _editor._points.Count; i++) { var p = _editor._points[i]; @@ -154,7 +258,23 @@ namespace FlaxEditor.GUI } _editor.SetKeyframeInternal(p.Index, time, value, p.Component); + moved = true; } + } + + return moved; + } + + internal void OnMove(Float2 location) + { + // Skip updating keyframes until move actual starts to be meaningful + if (Float2.Distance(ref _movingSelectionStartPosLock, ref location) < 1.5f) + return; + _movingSelectionStartPosLock = Float2.Minimum; + + var moved = MoveSelectedKeyframes(location); + if (moved) + { _editor.UpdateKeyframes(); _editor.UpdateTooltips(); if (_editor.EnablePanning == UseMode.On) @@ -168,13 +288,15 @@ namespace FlaxEditor.GUI internal void OnMoveEnd(Float2 location) { + _isMovingSelection = false; if (_movedKeyframes) { _editor.OnEdited(); _editor.OnEditingEnd(); + _editor.UpdateKeyframes(); + _editor.UpdateTooltips(); _movedKeyframes = false; } - _isMovingSelection = false; } /// @@ -199,26 +321,32 @@ namespace FlaxEditor.GUI _mousePos = location; // Start moving selection if movement started from the keyframe - if (_leftMouseDown && !_isMovingSelection && GetChildAt(_leftMouseDownPos) is KeyframePoint) + var leftMouseDownOverKeyframe = _leftMouseDown && GetKeyframePointAt(_leftMouseDownPos, false) != null; + if (leftMouseDownOverKeyframe && !_isMovingSelection) { + if (Float2.DistanceSquared(ref _leftMouseDownPos, ref location) < DragStartDistanceSquared) + return; + if (_editor.KeyframesEditorContext != null) - _editor.KeyframesEditorContext.OnKeyframesMove(_editor, this, location, true, false); + _editor.KeyframesEditorContext.OnKeyframesMove(_editor, this, _leftMouseDownPos, true, false); else - OnMoveStart(location); + OnMoveStart(_leftMouseDownPos); } // Moving view if (_rightMouseDown) { - var movingViewPos = Parent.PointToParent(PointToParent(location)); - var delta = movingViewPos - _movingViewLastPos; - if (_editor.CustomViewPanning != null) - delta = _editor.CustomViewPanning(delta); - delta *= GetUseModeMask(_editor.EnablePanning); - if (delta.LengthSquared > 0.01f) + var mousePosition = Root.MousePosition; + var mouseDelta = mousePosition - _movingViewLastPos; + if (mouseDelta.LengthSquared > 0.01f) { - _editor._mainPanel.ViewOffset += delta; - _movingViewLastPos = movingViewPos; + var delta = mouseDelta; + if (_editor.CustomViewPanning != null) + delta = _editor.CustomViewPanning(delta); + var viewDelta = delta * GetUseModeMask(_editor.EnablePanning); + if (viewDelta.LengthSquared > 0.0f) + _editor.ViewOffset += viewDelta; + _movingViewLastPos = mousePosition; _movedView = true; if (_editor.CustomViewPanning != null) { @@ -257,7 +385,7 @@ namespace FlaxEditor.GUI var tangent = PointToKeyframes(location, ref viewRect).Y - value; if (Root.GetKey(KeyboardKeys.Control)) tangent = Float2.SnapToGrid(new Float2(0, tangent), _editor.GetGridSnap()).Y; // Snap tangent over Y axis - tangent = tangent * _editor.ViewScale.X * 2; + tangent = tangent * UnitsPerSecond / _movingTangent.TangentOffset; _movingTangent.TangentValue = tangent; _editor.UpdateTangents(); Cursor = CursorType.SizeNS; @@ -265,7 +393,7 @@ namespace FlaxEditor.GUI return; } // Selecting - else if (_leftMouseDown) + else if (_leftMouseDown && !leftMouseDownOverKeyframe) { UpdateSelectionRectangle(); return; @@ -321,12 +449,13 @@ namespace FlaxEditor.GUI _rightMouseDown = true; _rightMouseDownPos = location; _movedView = false; - _movingViewLastPos = Parent.PointToParent(PointToParent(location)); + _movingViewLastPos = Root.MousePosition; } // Check if any node is under the mouse var underMouse = GetChildAt(location); - if (underMouse is KeyframePoint keyframe) + var keyframe = underMouse is KeyframePoint ? GetKeyframePointAt(location, false) : null; + if (keyframe != null) { if (_leftMouseDown) { @@ -355,12 +484,7 @@ namespace FlaxEditor.GUI { // Select node if (!Root.GetKey(KeyboardKeys.Control)) - { - if (_editor.KeyframesEditorContext != null) - _editor.KeyframesEditorContext.OnKeyframesDeselect(_editor); - else - _editor.ClearSelection(); - } + SelectKeyframePoint(keyframe, false); _toggledSelection = true; keyframe.IsSelected = true; _editor.UpdateTangents(); @@ -402,11 +526,14 @@ namespace FlaxEditor.GUI { // Start selecting StartMouseCapture(); - if (_editor.KeyframesEditorContext != null) - _editor.KeyframesEditorContext.OnKeyframesDeselect(_editor); - else - _editor.ClearSelection(); - _editor.UpdateTangents(); + if (GetSelectionMode() == SelectionMode.Replace) + { + if (_editor.KeyframesEditorContext != null) + _editor.KeyframesEditorContext.OnKeyframesDeselect(_editor); + else + _editor.ClearSelection(); + _editor.UpdateTangents(); + } Focus(); return true; } @@ -453,11 +580,21 @@ namespace FlaxEditor.GUI OnMoveEnd(location); } // Toggle selection - else if (!_toggledSelection && Root.GetKey(KeyboardKeys.Control) && GetChildAt(location) is KeyframePoint keyframe) + else if (!_toggledSelection && Root.GetKey(KeyboardKeys.Control) && GetKeyframePointAt(location, false) is KeyframePoint keyframe) { keyframe.IsSelected = !keyframe.IsSelected; _editor.UpdateTangents(); } + // Select next stacked keyframe component only after a click has completed, not before a possible drag. + else if (!_toggledSelection && !Root.GetKey(KeyboardKeys.Control) && !Root.GetKey(KeyboardKeys.Shift) && + Float2.DistanceSquared(ref _leftMouseDownPos, ref location) < DragStartDistanceSquared && + GetKeyframePointAt(_leftMouseDownPos, false, out var mouseDownStacked) != null && + mouseDownStacked && + GetKeyframePointAt(location, true, out var mouseUpStacked) is KeyframePoint clickedKeyframe && + mouseUpStacked) + { + SelectKeyframePoint(clickedKeyframe, false); + } _isMovingSelection = false; _isMovingTangent = false; @@ -473,22 +610,12 @@ namespace FlaxEditor.GUI if (!_movedView) { var selectionCount = _editor.SelectionCount; - var point = GetChildAt(location) as KeyframePoint; - if (selectionCount == 0 && point != null) + var point = GetKeyframePointAt(location, false); + if (point != null && (selectionCount == 0 || !point.IsSelected)) { // Select node selectionCount = 1; - point.IsSelected = true; - if (_editor.ShowCollapsed) - { - for (int i = 0; i < _editor._points.Count; i++) - { - var p = _editor._points[i]; - if (p.Index == point.Index) - p.IsSelected = point.IsSelected; - } - } - _editor.UpdateTangents(); + SelectKeyframePoint(point, false); } var viewRect = _editor._mainPanel.GetClientArea(); @@ -500,6 +627,16 @@ namespace FlaxEditor.GUI { cm.AddButton(selectionCount == 1 ? "Edit keyframe" : "Edit keyframes", () => _editor.EditKeyframes(this, location)); } + var components = _editor.Accessor.GetCurveComponents(); + if (point != null && !_editor.ShowCollapsed && components > 1) + { + var componentMenu = cm.AddChildMenu("Select component"); + for (int i = 0; i < components; i++) + { + var component = i; + componentMenu.ContextMenu.AddButton(GetComponentLabel(component, typeof(T)), () => SelectKeyframeComponent(point.Index, component)); + } + } var totalSelectionCount = _editor.KeyframesEditorContext?.OnKeyframesSelectionCount() ?? selectionCount; if (totalSelectionCount > 0) { @@ -558,7 +695,7 @@ namespace FlaxEditor.GUI { // Add keyframe on double click var child = GetChildAt(location); - if (child is not KeyframePoint && + if (GetKeyframePointAt(location, false) == null && child is not TangentPoint && _editor.KeyframesCount < _editor.MaxKeyframes) { @@ -591,7 +728,10 @@ namespace FlaxEditor.GUI // Scale relative to the curve size var scale = new Float2(delta * 0.1f); _editor._mainPanel.GetDesireClientArea(out var mainPanelArea); - var curveScale = mainPanelArea.Size / _editor._contents.Size; + var contentsSize = _editor._contents.Size; + var curveScale = new Float2( + GetSafeZoomRatio(mainPanelArea.Width, contentsSize.X), + GetSafeZoomRatio(mainPanelArea.Height, contentsSize.Y)); scale *= curveScale; if (zoomAlt) scale.X = 0; // Scale Y axis only diff --git a/Source/Editor/GUI/CurveEditor.cs b/Source/Editor/GUI/CurveEditor.cs index 4fb727ea1..092cda707 100644 --- a/Source/Editor/GUI/CurveEditor.cs +++ b/Source/Editor/GUI/CurveEditor.cs @@ -63,17 +63,29 @@ namespace FlaxEditor.GUI private class Popup : ContextMenuBase { + private const float HeaderHeight = 12.0f; + private const float MinContentHeight = 120.0f; + private CustomEditorPresenter _presenter; private CurveEditor _editor; private List _keyframeIndices; + private Panel _panel; private bool _isDirty; + private bool _isDragging; + private Float2 _dragStartScreenPos; + private Float2 _dragStartWindowPos; public Popup(CurveEditor editor, object[] selection, List keyframeIndices = null, float maxHeight = 140.0f) : this(editor, maxHeight) { _presenter.Select(selection); _presenter.OpenAllGroups(); - Size = new Float2(Size.X, Mathf.Min(_presenter.ContainerControl.Size.Y, maxHeight)); + var maxContentHeight = Mathf.Max(maxHeight - HeaderHeight, 1.0f); + var desiredHeight = _presenter.ContainerControl.Size.Y; + if (desiredHeight <= 1.0f) + desiredHeight = maxContentHeight; + Size = new Float2(Size.X, HeaderHeight + Mathf.Min(Mathf.Max(desiredHeight, MinContentHeight), maxContentHeight)); + UpdateContentBounds(); _keyframeIndices = keyframeIndices; if (keyframeIndices != null && selection.Length != keyframeIndices.Count) throw new Exception(); @@ -84,18 +96,39 @@ namespace FlaxEditor.GUI _editor = editor; const float width = 340.0f; Size = new Float2(width, height); - var panel1 = new Panel(ScrollBars.Vertical) + _panel = new Panel(ScrollBars.Vertical) { - Bounds = new Rectangle(0, 0.0f, width, height), + Bounds = new Rectangle(0, HeaderHeight, width, Mathf.Max(height - HeaderHeight, 1.0f)), Parent = this }; _presenter = new CustomEditorPresenter(null); _presenter.Panel.AnchorPreset = AnchorPresets.HorizontalStretchTop; _presenter.Panel.IsScrollable = true; - _presenter.Panel.Parent = panel1; + _presenter.Panel.Parent = _panel; _presenter.Modified += OnModified; } + private void UpdateContentBounds() + { + if (_panel != null) + _panel.Bounds = new Rectangle(0, HeaderHeight, Width, Mathf.Max(Height - HeaderHeight, 1.0f)); + } + + private bool IsOverHeader(ref Float2 location) + { + return location.X >= 0.0f && location.X <= Width && location.Y >= 0.0f && location.Y <= HeaderHeight; + } + + private void EndDragging() + { + if (_isDragging) + { + _isDragging = false; + Cursor = CursorType.Default; + EndMouseCapture(); + } + } + private void OnModified() { if (!_isDirty) @@ -108,7 +141,7 @@ namespace FlaxEditor.GUI { for (int i = 0; i < _presenter.SelectionCount; i++) { - _editor.SetKeyframeInternal(_keyframeIndices[i], _presenter.Selection[i]); + _editor.SetKeyframeInternal(_keyframeIndices[i], _editor.GetKeyframeFromEditingProxy(_presenter.Selection[i])); } } else if (_presenter.Selection[0] is IAllKeyframesProxy proxy) @@ -129,12 +162,31 @@ namespace FlaxEditor.GUI base.OnShow(); } + /// + public override void Draw() + { + base.Draw(); + + var style = Style.Current; + Render2D.FillRectangle(new Rectangle(0, 0, Width, HeaderHeight), style.BackgroundHighlighted); + Render2D.FillRectangle(new Rectangle(0, HeaderHeight - 1.0f, Width, 1.0f), style.Background); + } + + /// + protected override void OnSizeChanged() + { + base.OnSizeChanged(); + + UpdateContentBounds(); + } + /// public override void Hide() { if (!Visible) return; + EndDragging(); Focus(null); if (_isDirty) @@ -145,13 +197,87 @@ namespace FlaxEditor.GUI if (_editor._popup == this) _editor._popup = null; + _presenter.Modified -= OnModified; _presenter = null; _editor = null; _keyframeIndices = null; + _panel = null; base.Hide(); } + /// + public override bool OnMouseDown(Float2 location, MouseButton button) + { + if (button == MouseButton.Left && IsOverHeader(ref location)) + { + _isDragging = true; + _dragStartScreenPos = FlaxEngine.Input.MouseScreenPosition; + _dragStartWindowPos = WindowLocation; + Cursor = CursorType.SizeAll; + StartMouseCapture(); + Focus(); + return true; + } + + return base.OnMouseDown(location, button); + } + + /// + public override void OnMouseMove(Float2 location) + { + if (_isDragging) + { + var screenPos = FlaxEngine.Input.MouseScreenPosition; + MoveWindowTo(_dragStartWindowPos + screenPos - _dragStartScreenPos); + Cursor = CursorType.SizeAll; + return; + } + + Cursor = IsOverHeader(ref location) ? CursorType.SizeAll : CursorType.Default; + + base.OnMouseMove(location); + } + + /// + public override bool OnMouseUp(Float2 location, MouseButton button) + { + if (button == MouseButton.Left && _isDragging) + { + EndDragging(); + return true; + } + + return base.OnMouseUp(location, button); + } + + /// + public override void OnMouseLeave() + { + if (!_isDragging) + Cursor = CursorType.Default; + + base.OnMouseLeave(); + } + + /// + public override void OnLostFocus() + { + EndDragging(); + Cursor = CursorType.Default; + + base.OnLostFocus(); + } + + /// + public override void OnEndMouseCapture() + { + _isDragging = false; + Cursor = CursorType.Default; + + base.OnEndMouseCapture(); + } + /// public override bool OnKeyDown(KeyboardKeys key) { @@ -208,7 +334,8 @@ namespace FlaxEditor.GUI { var style = Style.Current; var rect = new Rectangle(Float2.Zero, Size); - var color = Editor.ShowCollapsed ? style.ForegroundDisabled : Editor.Colors[Component]; + var axisColor = Editor.ShowCollapsed ? style.ForegroundDisabled : Editor.Colors[Component]; + var color = axisColor; if (IsSelected) color = Editor.ContainsFocus ? style.SelectionBorder : Color.Lerp(style.ForegroundDisabled, style.SelectionBorder, 0.4f); if (IsMouseOver) @@ -288,22 +415,14 @@ namespace FlaxEditor.GUI set => Editor.SetKeyframeTangentInternal(Index, IsIn, Component, value); } - internal float TangentOffset => 50.0f / Editor.ViewScale.X; + private const float TangentVisualOffset = 50.0f; + + internal float TangentOffset => TangentVisualOffset / Editor.ViewScale.X; /// public override void Draw() { - var style = Style.Current; - var thickness = 6.0f / Mathf.Max(Editor.ViewScale.X, 1.0f); - var size = Size; - var pointPos = PointFromParent(Point.Center); - Render2D.DrawLine(size * 0.5f, pointPos, style.ForegroundDisabled, thickness); - - var rect = new Rectangle(Float2.Zero, size); - var color = style.BorderSelected; - if (IsMouseOver) - color *= 1.1f; - Render2D.FillRectangle(rect, color); + // Drawn by the editor overlay to keep a constant screen-space size. } /// @@ -436,12 +555,41 @@ namespace FlaxEditor.GUI /// protected readonly TangentPoint[] _tangents = new TangentPoint[2]; + private static bool IsFinite(float value) + { + return !float.IsNaN(value) && !float.IsInfinity(value); + } + + /// + /// Returns the input value if finite, otherwise returns a finite fallback or zero. + /// + protected static float SanitizeFinite(float value, float fallback) + { + if (IsFinite(value)) + return value; + return IsFinite(fallback) ? fallback : 0.0f; + } + + private static float SanitizeViewScale(float value, float fallback) + { + if (!IsFinite(value)) + value = IsFinite(fallback) ? fallback : 1.0f; + return Mathf.Clamp(value, 0.0001f, 1000.0f); + } + + private static float GetSafeZoomRatio(float viewSize, float contentsSize) + { + return IsFinite(viewSize) && IsFinite(contentsSize) && viewSize > Mathf.Epsilon && contentsSize > Mathf.Epsilon ? viewSize / contentsSize : 0.0f; + } + /// public override Float2 ViewOffset { get => _mainPanel.ViewOffset; set { + value.X = SanitizeFinite(value.X, _mainPanel.ViewOffset.X); + value.Y = SanitizeFinite(value.Y, _mainPanel.ViewOffset.Y); _mainPanel.ViewOffset = value; _mainPanel.FastScroll(); } @@ -451,7 +599,13 @@ namespace FlaxEditor.GUI public override Float2 ViewScale { get => _contents.Scale; - set => _contents.Scale = Float2.Clamp(value, new Float2(0.0001f), new Float2(1000.0f)); + set + { + var scale = _contents.Scale; + value.X = SanitizeViewScale(value.X, scale.X); + value.Y = SanitizeViewScale(value.Y, scale.Y); + _contents.Scale = value; + } } /// @@ -705,6 +859,36 @@ namespace FlaxEditor.GUI /// The proxy object. protected abstract IAllKeyframesProxy GetAllKeyframesEditingProxy(); + /// + /// Creates an editing proxy for a single keyframe. + /// + /// The keyframe index. + /// The keyframe. + /// The keyframe editing proxy. + protected virtual object CreateKeyframeEditingProxy(int index, object keyframe) + { + return keyframe; + } + + /// + /// Gets keyframe data from its editing proxy. + /// + /// The keyframe editing proxy. + /// The keyframe data. + protected virtual object GetKeyframeFromEditingProxy(object proxy) + { + return proxy; + } + + /// + /// Gets the maximum height for keyframe editing popup. + /// + /// The maximum popup height. + protected virtual float GetKeyframeEditingPopupHeight() + { + return 320.0f; + } + /// /// Interface for keyframes editing proxy objects. /// @@ -740,8 +924,8 @@ namespace FlaxEditor.GUI var selection = new object[keyframeIndices.Count]; var keyframes = GetKeyframes(); for (int i = 0; i < keyframeIndices.Count; i++) - selection[i] = keyframes[keyframeIndices[i]]; - _popup = new Popup(this, selection, keyframeIndices); + selection[i] = CreateKeyframeEditingProxy(keyframeIndices[i], keyframes[keyframeIndices[i]]); + _popup = new Popup(this, selection, keyframeIndices, GetKeyframeEditingPopupHeight()); _popup.Show(control, pos); } @@ -1049,6 +1233,58 @@ namespace FlaxEditor.GUI /// The main panel client area used as a view bounds. protected abstract void DrawCurve(ref Rectangle viewRect); + private Float2 ContentsToEditor(Float2 location) + { + location = _contents.PointToParent(location); + return _mainPanel.PointToParent(location); + } + + private Float2 GetControlCenterInEditor(Control control) + { + var center = control.PointToParent(control.Size * 0.5f); + return ContentsToEditor(center); + } + + private void DrawTangentHandles() + { + var style = Style.Current; + for (int i = 0; i < _tangents.Length; i++) + { + var tangent = _tangents[i]; + if (!tangent.Visible || tangent.Point == null || !tangent.Point.Visible) + continue; + + var tangentCenter = GetControlCenterInEditor(tangent); + var keyframeCenter = GetControlCenterInEditor(tangent.Point); + Render2D.DrawLine(tangentCenter, keyframeCenter, style.ForegroundDisabled, 2.0f); + + var rect = new Rectangle(tangentCenter - KeyframesSize * 0.5f, KeyframesSize); + var color = style.BorderSelected; + if (tangent.IsMouseOver) + color *= 1.1f; + Render2D.FillRectangle(rect, color); + } + } + + private void DrawSelectedKeyframeLabels() + { + if (ShowCollapsed) + return; + + var style = Style.Current; + for (int i = 0; i < _points.Count; i++) + { + var point = _points[i]; + if (!point.Visible || !point.IsSelected) + continue; + + var center = GetControlCenterInEditor(point); + var label = GetComponentLabel(point.Component, ValueType); + var labelRect = new Rectangle(center.X + 12.0f, center.Y - 22.0f, 34.0f, 28.0f); + Render2D.DrawText(style.FontMedium, label, labelRect, Colors[point.Component], TextAlignment.Near, TextAlignment.Center, TextWrapping.NoWrap, 1.0f, 1.25f); + } + } + /// public override void Draw() { @@ -1097,14 +1333,18 @@ namespace FlaxEditor.GUI { var selectionRect = Rectangle.FromPoints ( - _mainPanel.PointToParent(_contents.PointToParent(_contents._leftMouseDownPos)), - _mainPanel.PointToParent(_contents.PointToParent(_contents._mousePos)) + ContentsToEditor(_contents._leftMouseDownPos), + ContentsToEditor(_contents._mousePos) ); Render2D.FillRectangle(selectionRect, style.Selection); Render2D.DrawRectangle(selectionRect, style.SelectionBorder); } base.Draw(); + Render2D.PushClip(ref viewRect); + DrawTangentHandles(); + DrawSelectedKeyframeLabels(); + Render2D.PopClip(); // Draw border if (ContainsFocus) @@ -1465,6 +1705,85 @@ namespace FlaxEditor.GUI } } + sealed class KeyframeProxy + { + [HideInEditor, NoSerialize] + public LinearCurveEditor Editor; + + [HideInEditor, NoSerialize] + public int Index; + + private float _time; + private readonly float _originalTime; + + [EditorDisplay("Time"), EditorOrder(0), VisibleIf(nameof(HasFPS))] + [Tooltip("The keyframe frame number.")] + public int Frame + { + get => HasFPS ? Mathf.FloorToInt(SanitizeFinite(_time, _originalTime) * Editor.FPS.Value) : 0; + set + { + if (Editor?.FPS.HasValue == true && Editor.FPS.Value > Mathf.Epsilon) + _time = value / Editor.FPS.Value; + } + } + + [EditorDisplay("Time"), EditorOrder(1), Limit(float.MinValue, float.MaxValue, 0.01f)] + [Tooltip("The time of the keyframe.")] + public float Time + { + get => _time; + set => _time = SanitizeFinite(value, _time); + } + + [EditorDisplay("Value"), EditorOrder(10), Limit(float.MinValue, float.MaxValue, 0.01f)] + [Tooltip("The value of the curve at keyframe.")] + public T Value; + + private bool HasFPS => Editor?.FPS.HasValue == true && Editor.FPS.Value > Mathf.Epsilon; + + public KeyframeProxy(LinearCurveEditor editor, int index, LinearCurve.Keyframe keyframe) + { + Editor = editor; + Index = index; + _time = _originalTime = keyframe.Time; + Value = keyframe.Value; + } + + private float GetValidTime() + { + if (Editor == null) + return SanitizeFinite(_time, _originalTime); + if (Editor.FPS.HasValue) + { + var fps = Editor.FPS.Value; + if (fps <= Mathf.Epsilon) + return SanitizeFinite(_time, _originalTime); + var frame = Mathf.FloorToInt(SanitizeFinite(_time, _originalTime) * fps); + var minFrame = int.MinValue; + var maxFrame = int.MaxValue; + if (Index > 0) + minFrame = Mathf.FloorToInt(Editor._keyframes[Index - 1].Time * fps) + 1; + if (Index < Editor._keyframes.Count - 1) + maxFrame = Mathf.FloorToInt(Editor._keyframes[Index + 1].Time * fps) - 1; + if (minFrame > maxFrame) + return _originalTime; + frame = Mathf.Clamp(frame, minFrame, maxFrame); + return frame / fps; + } + var minTime = Index > 0 ? Editor._keyframes[Index - 1].Time + Mathf.Epsilon : float.MinValue; + var maxTime = Index < Editor._keyframes.Count - 1 ? Editor._keyframes[Index + 1].Time - Mathf.Epsilon : float.MaxValue; + if (minTime > maxTime) + return _originalTime; + return Mathf.Clamp(SanitizeFinite(_time, _originalTime), minTime, maxTime); + } + + public LinearCurve.Keyframe ToKeyframe() + { + return new LinearCurve.Keyframe(GetValidTime(), Value); + } + } + sealed class AllKeyframesProxy : IAllKeyframesProxy { [HideInEditor, NoSerialize] @@ -1489,6 +1808,24 @@ namespace FlaxEditor.GUI }; } + /// + protected override object CreateKeyframeEditingProxy(int index, object keyframe) + { + return new KeyframeProxy(this, index, (LinearCurve.Keyframe)keyframe); + } + + /// + protected override object GetKeyframeFromEditingProxy(object proxy) + { + return proxy is KeyframeProxy keyframeProxy ? keyframeProxy.ToKeyframe() : proxy; + } + + /// + protected override float GetKeyframeEditingPopupHeight() + { + return 240.0f; + } + /// public override object[] GetKeyframes() { @@ -2213,6 +2550,95 @@ namespace FlaxEditor.GUI } } + sealed class KeyframeProxy + { + [HideInEditor, NoSerialize] + public BezierCurveEditor Editor; + + [HideInEditor, NoSerialize] + public int Index; + + private float _time; + private readonly float _originalTime; + + [EditorDisplay("Time"), EditorOrder(0), VisibleIf(nameof(HasFPS))] + [Tooltip("The keyframe frame number.")] + public int Frame + { + get => HasFPS ? Mathf.FloorToInt(SanitizeFinite(_time, _originalTime) * Editor.FPS.Value) : 0; + set + { + if (Editor?.FPS.HasValue == true && Editor.FPS.Value > Mathf.Epsilon) + _time = value / Editor.FPS.Value; + } + } + + [EditorDisplay("Time"), EditorOrder(1), Limit(float.MinValue, float.MaxValue, 0.01f)] + [Tooltip("The time of the keyframe.")] + public float Time + { + get => _time; + set => _time = SanitizeFinite(value, _time); + } + + [EditorDisplay("Value"), EditorOrder(10), Limit(float.MinValue, float.MaxValue, 0.01f)] + [Tooltip("The value of the curve at keyframe.")] + public T Value; + + [EditorDisplay("Tangents", "Tangent In"), EditorOrder(20), Limit(float.MinValue, float.MaxValue, 0.01f)] + [Tooltip("The input tangent (going from the previous key to this one) of the key.")] + public T TangentIn; + + [EditorDisplay("Tangents", "Tangent Out"), EditorOrder(21), Limit(float.MinValue, float.MaxValue, 0.01f)] + [Tooltip("The output tangent (going from this key to next one) of the key.")] + public T TangentOut; + + private bool HasFPS => Editor?.FPS.HasValue == true && Editor.FPS.Value > Mathf.Epsilon; + + public KeyframeProxy(BezierCurveEditor editor, int index, BezierCurve.Keyframe keyframe) + { + Editor = editor; + Index = index; + _time = _originalTime = keyframe.Time; + Value = keyframe.Value; + TangentIn = keyframe.TangentIn; + TangentOut = keyframe.TangentOut; + } + + private float GetValidTime() + { + if (Editor == null) + return SanitizeFinite(_time, _originalTime); + if (Editor.FPS.HasValue) + { + var fps = Editor.FPS.Value; + if (fps <= Mathf.Epsilon) + return SanitizeFinite(_time, _originalTime); + var frame = Mathf.FloorToInt(SanitizeFinite(_time, _originalTime) * fps); + var minFrame = int.MinValue; + var maxFrame = int.MaxValue; + if (Index > 0) + minFrame = Mathf.FloorToInt(Editor._keyframes[Index - 1].Time * fps) + 1; + if (Index < Editor._keyframes.Count - 1) + maxFrame = Mathf.FloorToInt(Editor._keyframes[Index + 1].Time * fps) - 1; + if (minFrame > maxFrame) + return _originalTime; + frame = Mathf.Clamp(frame, minFrame, maxFrame); + return frame / fps; + } + var minTime = Index > 0 ? Editor._keyframes[Index - 1].Time + Mathf.Epsilon : float.MinValue; + var maxTime = Index < Editor._keyframes.Count - 1 ? Editor._keyframes[Index + 1].Time - Mathf.Epsilon : float.MaxValue; + if (minTime > maxTime) + return _originalTime; + return Mathf.Clamp(SanitizeFinite(_time, _originalTime), minTime, maxTime); + } + + public BezierCurve.Keyframe ToKeyframe() + { + return new BezierCurve.Keyframe(GetValidTime(), Value, TangentIn, TangentOut); + } + } + sealed class AllKeyframesProxy : IAllKeyframesProxy { [HideInEditor, NoSerialize] @@ -2237,6 +2663,24 @@ namespace FlaxEditor.GUI }; } + /// + protected override object CreateKeyframeEditingProxy(int index, object keyframe) + { + return new KeyframeProxy(this, index, (BezierCurve.Keyframe)keyframe); + } + + /// + protected override object GetKeyframeFromEditingProxy(object proxy) + { + return proxy is KeyframeProxy keyframeProxy ? keyframeProxy.ToKeyframe() : proxy; + } + + /// + protected override float GetKeyframeEditingPopupHeight() + { + return 320.0f; + } + /// public override object[] GetKeyframes() { diff --git a/Source/Editor/GUI/Timeline/GUI/Background.cs b/Source/Editor/GUI/Timeline/GUI/Background.cs index ed9b8150b..d15e307af 100644 --- a/Source/Editor/GUI/Timeline/GUI/Background.cs +++ b/Source/Editor/GUI/Timeline/GUI/Background.cs @@ -36,6 +36,11 @@ namespace FlaxEditor.GUI.Timeline.GUI _timeline.OnKeyframesSelection(null, this, selectionRect); } + private bool HasSelectionModifier() + { + return Root.GetKey(KeyboardKeys.Shift) || Root.GetKey(KeyboardKeys.Alt); + } + /// public override bool OnMouseDown(Float2 location, MouseButton button) { @@ -48,7 +53,8 @@ namespace FlaxEditor.GUI.Timeline.GUI // Start selecting _isSelecting = true; _selectingStartPos = location; - _timeline.OnKeyframesDeselect(null); + if (!HasSelectionModifier()) + _timeline.OnKeyframesDeselect(null); Focus(); StartMouseCapture(); return true; diff --git a/Source/Editor/GUI/Timeline/GUI/KeyframesEditor.cs b/Source/Editor/GUI/Timeline/GUI/KeyframesEditor.cs index d6fa0e076..7b2500799 100644 --- a/Source/Editor/GUI/Timeline/GUI/KeyframesEditor.cs +++ b/Source/Editor/GUI/Timeline/GUI/KeyframesEditor.cs @@ -77,6 +77,13 @@ namespace FlaxEditor.GUI /// private class Contents : ContainerControl { + private enum SelectionMode + { + Replace, + Add, + Remove, + } + private readonly KeyframesEditor _editor; internal bool _leftMouseDown; private bool _rightMouseDown; @@ -108,14 +115,39 @@ namespace FlaxEditor.GUI UpdateSelection(ref selectionRect); } + private SelectionMode GetSelectionMode() + { + if (Root.GetKey(KeyboardKeys.Alt)) + return SelectionMode.Remove; + if (Root.GetKey(KeyboardKeys.Shift)) + return SelectionMode.Add; + return SelectionMode.Replace; + } + internal void UpdateSelection(ref Rectangle selectionRect) { + var mode = GetSelectionMode(); + // Find controls to select for (int i = 0; i < Children.Count; i++) { if (Children[i] is KeyframePoint p) { - p.IsSelected = p.Bounds.Intersects(ref selectionRect); + var intersects = p.Bounds.Intersects(ref selectionRect); + switch (mode) + { + case SelectionMode.Replace: + p.IsSelected = intersects; + break; + case SelectionMode.Add: + if (intersects) + p.IsSelected = true; + break; + case SelectionMode.Remove: + if (intersects) + p.IsSelected = false; + break; + } } } } @@ -138,6 +170,7 @@ namespace FlaxEditor.GUI { var viewRect = _editor._mainPanel.GetClientArea(); var locationKeyframes = PointToKeyframes(location, ref viewRect); + var moved = false; for (var i = 0; i < _editor._points.Count; i++) { var p = _editor._points[i]; @@ -160,7 +193,12 @@ namespace FlaxEditor.GUI // TODO: snapping keyframes to grid when moving _editor._keyframes[p.Index] = k; + moved = true; } + } + + if (moved) + { _editor.UpdateKeyframes(); if (_editor.EnablePanning) { @@ -351,10 +389,13 @@ namespace FlaxEditor.GUI { // Start selecting StartMouseCapture(); - if (_editor.KeyframesEditorContext != null) - _editor.KeyframesEditorContext.OnKeyframesDeselect(_editor); - else - _editor.ClearSelection(); + if (GetSelectionMode() == SelectionMode.Replace) + { + if (_editor.KeyframesEditorContext != null) + _editor.KeyframesEditorContext.OnKeyframesDeselect(_editor); + else + _editor.ClearSelection(); + } Focus(); return true; } diff --git a/Source/Editor/GUI/Timeline/Tracks/ActorTrack.cs b/Source/Editor/GUI/Timeline/Tracks/ActorTrack.cs index 4a0993cb9..109bf5ccd 100644 --- a/Source/Editor/GUI/Timeline/Tracks/ActorTrack.cs +++ b/Source/Editor/GUI/Timeline/Tracks/ActorTrack.cs @@ -3,7 +3,6 @@ using System; using System.IO; using System.Linq; -using FlaxEditor.CustomEditors; using FlaxEditor.GUI.ContextMenu; using FlaxEditor.GUI.Timeline.Undo; using FlaxEditor.SceneGraph; diff --git a/Source/Editor/GUI/Timeline/Tracks/CurvePropertyTrack.cs b/Source/Editor/GUI/Timeline/Tracks/CurvePropertyTrack.cs index e6246f4c5..7e974b54f 100644 --- a/Source/Editor/GUI/Timeline/Tracks/CurvePropertyTrack.cs +++ b/Source/Editor/GUI/Timeline/Tracks/CurvePropertyTrack.cs @@ -180,7 +180,7 @@ namespace FlaxEditor.GUI.Timeline.Tracks private void OnSplitterMoved(Float2 location) { - var height = Mathf.Clamp(PointToParent(location).Y, 40.0f, 1000.0f); + var height = Mathf.Clamp(_splitter.PointToParent(location).Y + Height - Curve.Height, 40.0f, 1000.0f); if (!Mathf.NearEqual(height, _expandedHeight)) { Height = _expandedHeight = height; diff --git a/Source/Editor/GUI/Timeline/Tracks/ObjectTrack.cs b/Source/Editor/GUI/Timeline/Tracks/ObjectTrack.cs index 1042e3014..f0de3584b 100644 --- a/Source/Editor/GUI/Timeline/Tracks/ObjectTrack.cs +++ b/Source/Editor/GUI/Timeline/Tracks/ObjectTrack.cs @@ -100,6 +100,7 @@ namespace FlaxEditor.GUI.Timeline.Tracks /// The object. protected virtual void OnObjectExistenceChanged(object obj) { + TooltipText = obj != null ? Editor.Instance.CodeDocs.GetTooltip(obj.GetType()) : "null"; } /// diff --git a/Source/Editor/Modules/UIModule.cs b/Source/Editor/Modules/UIModule.cs index ce81925e3..4b0ebbf77 100644 --- a/Source/Editor/Modules/UIModule.cs +++ b/Source/Editor/Modules/UIModule.cs @@ -829,6 +829,7 @@ namespace FlaxEditor.Modules tooltip.Append($" ({engineNickname})"); // Build info + tooltip.AppendLine(System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription); #if USE_LARGE_WORLDS tooltip.AppendLine("Large Worlds Enabled"); #endif diff --git a/Source/Editor/Options/InputOptions.cs b/Source/Editor/Options/InputOptions.cs index 7c577ad4a..be0ba7a7e 100644 --- a/Source/Editor/Options/InputOptions.cs +++ b/Source/Editor/Options/InputOptions.cs @@ -60,14 +60,14 @@ namespace FlaxEditor.Options public sealed class InputOptions { /// - /// Gets a value based on the current settings that indicates wether window shortcuts will be avaliable during play mode. + /// Gets a value based on the current settings that indicates whether window shortcuts will be available during play mode. /// - public static bool WindowShortcutsAvaliable => !Editor.IsPlayMode || Editor.Instance.Options.Options.Input.PlayModeWindowShortcutAvaliability == PlayModeShortcutAvailability.All; + public static bool WindowShortcutsAvailable => !Editor.IsPlayMode || Editor.Instance.Options.Options.Input.PlayModeWindowShortcutAvaliability == PlayModeShortcutAvailability.All; /// - /// Gets a value based on the current settings that indicates wether the profiler window shortcut will be avaliable during play mode. + /// Gets a value based on the current settings that indicates whether the profiler window shortcut will be available during play mode. /// - public static bool ProfilerShortcutAvaliable => WindowShortcutsAvaliable || Editor.Instance.Options.Options.Input.PlayModeWindowShortcutAvaliability == PlayModeShortcutAvailability.ProfilerOnly; + public static bool ProfilerShortcutAvailable => WindowShortcutsAvailable || Editor.Instance.Options.Options.Input.PlayModeWindowShortcutAvaliability == PlayModeShortcutAvailability.ProfilerOnly; #region Common diff --git a/Source/Editor/Options/VisualOptions.cs b/Source/Editor/Options/VisualOptions.cs index e0f0982f4..463dda590 100644 --- a/Source/Editor/Options/VisualOptions.cs +++ b/Source/Editor/Options/VisualOptions.cs @@ -106,28 +106,28 @@ namespace FlaxEditor.Options /// Gets or sets the output log text color. /// [DefaultValue(typeof(Color), "1,1,1,1")] - [EditorDisplay("Log", "Info Color"), EditorOrder(1500), Tooltip("The color used for info messages in the Debug and Output Log.")] + [EditorDisplay("Debug Log", "Info Color"), EditorOrder(1500), Tooltip("The color used for info messages in the Debug and Output Log.")] public Color LogInfoColor { get; set; } = Color.White; /// /// Gets or sets the output log text color for warnings /// [DefaultValue(typeof(Color), "1,1,0,1")] - [EditorDisplay("Log", "Warning Color"), EditorOrder(1501), Tooltip("The color used for warnings in the Debug and Output Log.")] + [EditorDisplay("Debug Log", "Warning Color"), EditorOrder(1501), Tooltip("The color used for warnings in the Debug and Output Log.")] public Color LogWarningColor { get; set; } = Color.Yellow; /// /// Gets or sets the output log text color for errors /// [DefaultValue(typeof(Color), "1,0,0,1")] - [EditorDisplay("Log", "Error Color"), EditorOrder(1502), Tooltip("The color used for errors in the Debug and Output Log.")] + [EditorDisplay("Debug Log", "Error Color"), EditorOrder(1502), Tooltip("The color used for errors in the Debug and Output Log.")] public Color LogErrorColor { get; set; } = Color.Red; /// - /// Gets or sets a value wether the Debug Log entry text color should use the set color. + /// Gets or sets a value whether the Debug Log entry text color should use the set color. /// [DefaultValue(true)] - [EditorDisplay("Log", "Color Debug Log Text"), EditorOrder(1503), Tooltip("Wether to use the set colors in the text of a Debug Log entry.")] + [EditorDisplay("Debug Log", "Color Debug Log Text"), EditorOrder(1503), Tooltip("Wether to use the set colors in the text of a Debug Log entry.")] public bool ColorDebugLogText = true; } } diff --git a/Source/Editor/SceneGraph/ActorNode.cs b/Source/Editor/SceneGraph/ActorNode.cs index 67f671756..e73bce2c8 100644 --- a/Source/Editor/SceneGraph/ActorNode.cs +++ b/Source/Editor/SceneGraph/ActorNode.cs @@ -312,7 +312,7 @@ namespace FlaxEditor.SceneGraph // Skip actors that should not be selected if (hit != null && _actor != null && (_actor.HideFlags & HideFlags.DontSelect) == HideFlags.DontSelect) { - hit = parentNode; + hit = null; } return hit; diff --git a/Source/Editor/SceneGraph/Actors/BoxBrushNode.cs b/Source/Editor/SceneGraph/Actors/BoxBrushNode.cs index 2e3d56c27..6fb05cc23 100644 --- a/Source/Editor/SceneGraph/Actors/BoxBrushNode.cs +++ b/Source/Editor/SceneGraph/Actors/BoxBrushNode.cs @@ -7,6 +7,7 @@ using Real = System.Single; #endif using System; +using System.ComponentModel; using FlaxEngine; namespace FlaxEditor.SceneGraph.Actors @@ -24,6 +25,67 @@ namespace FlaxEditor.SceneGraph.Actors /// public sealed class SideLinkNode : ActorChildNode { + private const float MinimumComponentExtent = 0.001f; + + internal static Vector3 GetCornerSigns(int cornerIndex) + { + switch (cornerIndex) + { + case 0: return new Vector3(1, 1, 1); + case 1: return new Vector3(1, 1, -1); + case 2: return new Vector3(-1, 1, -1); + case 3: return new Vector3(-1, 1, 1); + case 4: return new Vector3(1, -1, 1); + case 5: return new Vector3(1, -1, -1); + case 6: return new Vector3(-1, -1, -1); + default: return new Vector3(-1, -1, 1); + } + } + + internal static Vector3 GetComponentPoint(BoxBrush brush, Vector3 signs) + { + return brush.Transform.LocalToWorld(brush.Center + signs * brush.Size * 0.5f); + } + + internal static void SetComponentPoint(BoxBrush brush, Vector3 signs, Vector3 worldPoint) + { + var point = brush.Transform.WorldToLocal(worldPoint); + var center = brush.Center; + var size = brush.Size; + for (int axis = 0; axis < 3; axis++) + { + float sign = GetComponent(signs, axis); + if (Mathf.Abs(sign) < 0.5f) + continue; + float halfSize = GetComponent(size, axis) * 0.5f; + float opposite = GetComponent(center, axis) - sign * halfSize; + float target = GetComponent(point, axis); + if (sign > 0.0f) + target = Mathf.Max(target, opposite + MinimumComponentExtent); + else + target = Mathf.Min(target, opposite - MinimumComponentExtent); + SetComponent(ref center, axis, (target + opposite) * 0.5f); + SetComponent(ref size, axis, Mathf.Abs(target - opposite)); + } + brush.Center = center; + brush.Size = size; + } + + private static float GetComponent(Vector3 value, int axis) + { + return axis == 0 ? (float)value.X : axis == 1 ? (float)value.Y : (float)value.Z; + } + + private static void SetComponent(ref Vector3 value, int axis, float component) + { + if (axis == 0) + value.X = component; + else if (axis == 1) + value.Y = component; + else + value.Z = component; + } + private sealed class BrushSurfaceProxy { [HideInEditor] @@ -45,6 +107,7 @@ namespace FlaxEditor.SceneGraph.Actors } } + [DefaultValue(typeof(Float2), "1,1")] [EditorOrder(30), EditorDisplay("Brush", "UV Scale"), Limit(-1000, 1000, 0.01f)] [Tooltip("The surface texture coordinates scale.")] public Float2 TexCoordScale @@ -58,6 +121,7 @@ namespace FlaxEditor.SceneGraph.Actors } } + [DefaultValue(typeof(Float2), "0,0")] [EditorOrder(40), EditorDisplay("Brush", "UV Offset"), Limit(-1000, 1000, 0.01f)] [Tooltip("The surface texture coordinates offset.")] public Float2 TexCoordOffset @@ -71,6 +135,7 @@ namespace FlaxEditor.SceneGraph.Actors } } + [DefaultValue(0.0f)] [EditorOrder(50), EditorDisplay("Brush", "UV Rotation")] [Tooltip("The surface texture coordinates rotation angle (in degrees).")] public float TexCoordRotation @@ -84,6 +149,7 @@ namespace FlaxEditor.SceneGraph.Actors } } + [DefaultValue(1.0f)] [EditorOrder(20), EditorDisplay("Brush", "Scale In Lightmap"), Limit(0, 10000, 0.1f)] [Tooltip("The scale in lightmap (per surface).")] public float ScaleInLightmap @@ -164,6 +230,9 @@ namespace FlaxEditor.SceneGraph.Actors set { var actor = Brush; +#if true + SetComponentPoint(actor, _offset * 2.0f, value.Translation); +#else Transform localTrans = actor.Transform.WorldToLocal(value); var prevLocalOffset = _offset * actor.Size + actor.Center; var localOffset = Vector3.Abs(_offset) * 2.0f * localTrans.Translation; @@ -171,6 +240,7 @@ namespace FlaxEditor.SceneGraph.Actors float centerScale = Index % 2 == 0 ? 0.5f : -0.5f; actor.Size += localOffsetDelta; actor.Center += localOffsetDelta * centerScale; +#endif } } diff --git a/Source/Editor/Windows/Assets/AssetEditorWindow.cs b/Source/Editor/Windows/Assets/AssetEditorWindow.cs index 802686b76..c57e391b2 100644 --- a/Source/Editor/Windows/Assets/AssetEditorWindow.cs +++ b/Source/Editor/Windows/Assets/AssetEditorWindow.cs @@ -53,7 +53,7 @@ namespace FlaxEditor.Windows.Assets { Parent = this }; - _toolstrip.AddButton(editor.Icons.Search64, () => Editor.Windows.ContentWin.Select(_item)).LinkTooltip("Show and select in Content Window."); + _toolstrip.AddButton(editor.Icons.Search64, ShowInContent).LinkTooltip("Show and select in Content Window."); InputActions.Add(options => options.Save, Save); @@ -132,12 +132,29 @@ namespace FlaxEditor.Windows.Assets // Cancel closing return true; } + else + { + // Discard and close + DiscardChanges(); + } } } return base.OnClosing(reason); } + /// + /// Discards unsaved asset changes before closing the window. + /// + protected virtual void DiscardChanges() + { + } + + private void ShowInContent() + { + Editor.Windows.ContentWin.Select(_item); + } + /// protected override void OnClose() { @@ -166,7 +183,9 @@ namespace FlaxEditor.Windows.Assets base.OnDestroy(); } - /// + /// + /// Called when scripts reloading begins. + /// protected virtual void OnScriptsReloadBegin() { if (!IsHidden) @@ -257,7 +276,8 @@ namespace FlaxEditor.Windows.Assets base.OnShowContextMenu(menu); menu.AddButton("Save", Save).Enabled = IsEdited; - menu.AddButton("Copy name", () => Clipboard.Text = Item.NamePath); + menu.AddButton("Copy Name", () => Clipboard.Text = Item.NamePath); + menu.AddButton("Show In Content", ShowInContent); menu.AddSeparator(); } diff --git a/Source/Editor/Windows/Assets/FontWindow.cs b/Source/Editor/Windows/Assets/FontWindow.cs index e08a87b05..2f27a3f1b 100644 --- a/Source/Editor/Windows/Assets/FontWindow.cs +++ b/Source/Editor/Windows/Assets/FontWindow.cs @@ -25,6 +25,10 @@ namespace FlaxEditor.Windows.Assets [EditorOrder(5), EditorDisplay("Properties"), Tooltip("The rasterization mode used when generating font atlases.")] public FontRasterMode RasterMode; + [DefaultValue(32.0f), VisibleIf(nameof(IsMSDF))] + [EditorOrder(6), Limit(4, 512), EditorDisplay("Properties"), Tooltip("The font size used when generating MSDF font atlases.")] + public float MSDFSize; + [DefaultValue(FontHinting.Default)] [EditorOrder(10), EditorDisplay("Properties"), Tooltip("The font hinting used when rendering characters.")] public FontHinting Hinting; @@ -41,12 +45,15 @@ namespace FlaxEditor.Windows.Assets [EditorOrder(40), EditorDisplay("Properties"), Tooltip("Enables slant effect, emulating italic style.")] public bool Italic; + private bool IsMSDF => RasterMode == FontRasterMode.MSDF; + public void Get(out FontOptions options) { options = new FontOptions { Hinting = Hinting, RasterMode = RasterMode, + MSDFSize = MSDFSize, }; if (AntiAliasing) options.Flags |= FontFlags.AntiAliasing; @@ -63,6 +70,7 @@ namespace FlaxEditor.Windows.Assets Bold = (options.Flags & FontFlags.Bold) == FontFlags.Bold; Italic = (options.Flags & FontFlags.Italic) == FontFlags.Italic; RasterMode = options.RasterMode; + MSDFSize = options.MSDFSize; } } @@ -134,7 +142,6 @@ namespace FlaxEditor.Windows.Assets if (assetOptions != options) { Asset.Options = options; - Asset.Invalidate(); } } diff --git a/Source/Editor/Windows/Assets/JsonAssetWindow.cs b/Source/Editor/Windows/Assets/JsonAssetWindow.cs index 9cc1a6ab5..233bca18a 100644 --- a/Source/Editor/Windows/Assets/JsonAssetWindow.cs +++ b/Source/Editor/Windows/Assets/JsonAssetWindow.cs @@ -263,6 +263,15 @@ namespace FlaxEditor.Windows.Assets base.OnAssetLoaded(); } + /// + protected override void DiscardChanges() + { + Asset?.ClearInstance(); + _object = null; + + base.DiscardChanges(); + } + private void OpenOptionsContextMenu() { if (_optionsCM != null) diff --git a/Source/Editor/Windows/Assets/MaterialInstanceWindow.cs b/Source/Editor/Windows/Assets/MaterialInstanceWindow.cs index 0d621b64d..4b2d47b82 100644 --- a/Source/Editor/Windows/Assets/MaterialInstanceWindow.cs +++ b/Source/Editor/Windows/Assets/MaterialInstanceWindow.cs @@ -64,8 +64,14 @@ namespace FlaxEditor.Windows.Assets [CustomEditor(typeof(ParametersEditor))] private sealed class PropertiesProxy { + private struct ParameterState + { + public object Value; + public bool IsOverride; + } + private MaterialBase _restoreBase; - private Dictionary _restoreParams; + private Dictionary _restoreParams; [EditorDisplay("General"), Tooltip("The base material used to override it's properties")] public MaterialBase BaseMaterial @@ -181,9 +187,16 @@ namespace FlaxEditor.Windows.Assets var material = Window.Asset; _restoreBase = material.BaseMaterial; var parameters = material.Parameters; - _restoreParams = new Dictionary(); + _restoreParams = new Dictionary(); for (int i = 0; i < parameters.Length; i++) - _restoreParams[parameters[i].Name] = parameters[i].Value; + { + var p = parameters[i]; + _restoreParams[p.Name] = new ParameterState + { + Value = p.Value, + IsOverride = p.IsOverride, + }; + } } /// @@ -191,7 +204,7 @@ namespace FlaxEditor.Windows.Assets /// public void DiscardChanges() { - if (Window == null) + if (Window == null || _restoreParams == null) return; var material = Window.Asset; @@ -200,9 +213,10 @@ namespace FlaxEditor.Windows.Assets for (int i = 0; i < parameters.Length; i++) { var p = parameters[i]; - if (p.IsPublic && _restoreParams.TryGetValue(p.Name, out var value)) + if (p.IsPublic && _restoreParams.TryGetValue(p.Name, out var state)) { - p.Value = value; + p.Value = state.Value; + p.IsOverride = state.IsOverride; } } } diff --git a/Source/Editor/Windows/Assets/ModelBaseWindow.cs b/Source/Editor/Windows/Assets/ModelBaseWindow.cs index 89ff1ff96..eb22396f0 100644 --- a/Source/Editor/Windows/Assets/ModelBaseWindow.cs +++ b/Source/Editor/Windows/Assets/ModelBaseWindow.cs @@ -837,6 +837,14 @@ namespace FlaxEditor.Windows.Assets base.OnAssetLoaded(); } + /// + protected override void DiscardChanges() + { + _asset?.Reload(); + + base.DiscardChanges(); + } + /// public override void OnItemReimported(ContentItem item) { diff --git a/Source/Editor/Windows/Assets/SceneAnimationWindow.cs b/Source/Editor/Windows/Assets/SceneAnimationWindow.cs index de384c60a..4dfa287a9 100644 --- a/Source/Editor/Windows/Assets/SceneAnimationWindow.cs +++ b/Source/Editor/Windows/Assets/SceneAnimationWindow.cs @@ -755,6 +755,7 @@ namespace FlaxEditor.Windows.Assets } _previewPlayerPicker.Parent.Visible = !_previewButton.Checked; _timeline.CanPlayPause = _previewButton.Checked || Editor.IsPlayMode; + _timeline.CanPlayStop = _previewButton.Checked || Editor.IsPlayMode; } private void OnRenderButtonClicked() @@ -927,7 +928,7 @@ namespace FlaxEditor.Windows.Assets UpdateToolstrip(); _timeline.CanPlayPause = _previewButton.Checked; - _timeline.CanPlayStop = false; + _timeline.CanPlayStop = _previewButton.Checked; } /// @@ -982,7 +983,7 @@ namespace FlaxEditor.Windows.Assets // Preview is playing _previewPlayer.Tick(Time.UnscaledDeltaTime); } - else if (Mathf.NearEqual(_previewPlayer.Time, _timeline.CurrentFrame)) + else if (Mathf.NearEqual(_previewPlayer.Time, _timeline.CurrentTime)) { // Preview is paused _previewPlayer.Time = _timeline.CurrentTime; diff --git a/Source/Editor/Windows/DebugLogWindow.cs b/Source/Editor/Windows/DebugLogWindow.cs index 9ae4fad81..b367551d2 100644 --- a/Source/Editor/Windows/DebugLogWindow.cs +++ b/Source/Editor/Windows/DebugLogWindow.cs @@ -77,11 +77,14 @@ namespace FlaxEditor.Windows public const float DefaultHeight = 32.0f; private DebugLogWindow _window; + private RichTextBox _richTextBox; public LogGroup Group; public LogEntryDescription Desc; public SpriteHandle Icon; public int LogCount = 1; + private Color Color => Group == LogGroup.Error ? _window._colorError : (Group == LogGroup.Warning ? _window._colorWarning : _window._colorInfo); + public LogEntry(DebugLogWindow window, ref LogEntryDescription desc) : base(0, 0, 120, DefaultHeight) { @@ -106,6 +109,24 @@ namespace FlaxEditor.Windows Icon = _window._iconError; break; } + + // Use Rich Text Box to display title if it contains any HTML tags + if (desc.Title.Contains('<') && desc.Title.Contains('>')) + { + _richTextBox = new RichTextBox + { + ClipText = false, + HasBorder = false, + BackgroundColor = Color.Transparent, + Text = desc.Title, + }; + if (_window._colorDebugLogText) + { + var style = _richTextBox.TextStyle; + style.Color = Color; + _richTextBox.TextStyle = style; + } + } } /// @@ -116,12 +137,10 @@ namespace FlaxEditor.Windows /// public override void Draw() { - base.Draw(); - - // Cache data var style = Style.Current; var index = IndexInParent; var clientRect = new Rectangle(Float2.Zero, Size); + var color = Color; // Background if (_window._selected == this) @@ -136,8 +155,6 @@ namespace FlaxEditor.Windows else if (index % 2 == 0) Render2D.FillRectangle(clientRect, style.Background * 0.9f); - var color = Group == LogGroup.Error ? _window._colorError : (Group == LogGroup.Warning ? _window._colorWarning : _window._colorInfo); - // Icon Render2D.DrawSprite(Icon, new Rectangle(8, 0, 32, 32), color); @@ -145,14 +162,37 @@ namespace FlaxEditor.Windows var textRect = new Rectangle(43, 2, clientRect.Width - 40, clientRect.Height - 10); Render2D.PushClip(ref clientRect); bool coloredText = _window._colorDebugLogText; - if (LogCount == 1) + if (_richTextBox != null) + { + Render2D.PushTransform(Matrix3x3.Translation2D(textRect.Location)); + _richTextBox.DrawSelf(); + Render2D.PopTransform(); + } + else { Render2D.DrawText(style.FontMedium, Desc.Title, textRect, coloredText ? color : style.Foreground); } - else if (LogCount > 1) + + // Adding log counter for collapsed logs + if (LogCount > 1) { - Render2D.DrawText(style.FontMedium, $"{Desc.Title} ({LogCount})", textRect, coloredText ? color : style.Foreground); + Float2 logCountPos = Float2.Zero; + if (_richTextBox != null) + { + var blocks = _richTextBox.TextBlocks; + if (blocks.Count != 0) + { + var block = blocks[^1]; + logCountPos = new Float2(block.Bounds.Right, block.Bounds.Top); + } + } + else + { + logCountPos.X = style.FontMedium.MeasureText(Desc.Title).X; + } + Render2D.DrawText(style.FontMedium, $" ({LogCount})", color, textRect.Location + logCountPos); } + Render2D.PopClip(); } @@ -296,6 +336,18 @@ namespace FlaxEditor.Windows base.OnMouseLeave(); } + + /// + public override void OnDestroy() + { + if (_richTextBox != null) + { + _richTextBox.OnDestroy(); + _richTextBox = null; + } + + base.OnDestroy(); + } } private readonly SplitPanel _split; diff --git a/Source/Editor/Windows/EditorWindow.cs b/Source/Editor/Windows/EditorWindow.cs index 03d435efe..7a5eb847a 100644 --- a/Source/Editor/Windows/EditorWindow.cs +++ b/Source/Editor/Windows/EditorWindow.cs @@ -53,72 +53,72 @@ namespace FlaxEditor.Windows // Set up editor window shortcuts InputActions.Add(options => options.ContentWindow, () => { - if (InputOptions.WindowShortcutsAvaliable) + if (InputOptions.WindowShortcutsAvailable) Editor.Windows.ContentWin.FocusOrShow(); }); InputActions.Add(options => options.SceneWindow, () => { - if (InputOptions.WindowShortcutsAvaliable) + if (InputOptions.WindowShortcutsAvailable) Editor.Windows.SceneWin.FocusOrShow(); }); InputActions.Add(options => options.ToolboxWindow, () => { - if (InputOptions.WindowShortcutsAvaliable) + if (InputOptions.WindowShortcutsAvailable) Editor.Windows.ToolboxWin.FocusOrShow(); }); InputActions.Add(options => options.PropertiesWindow, () => { - if (InputOptions.WindowShortcutsAvaliable) + if (InputOptions.WindowShortcutsAvailable) Editor.Windows.PropertiesWin.FocusOrShow(); }); InputActions.Add(options => options.GameWindow, () => { - if (InputOptions.WindowShortcutsAvaliable) + if (InputOptions.WindowShortcutsAvailable) Editor.Windows.GameWin.FocusOrShow(); }); InputActions.Add(options => options.EditorWindow, () => { - if (InputOptions.WindowShortcutsAvaliable) + if (InputOptions.WindowShortcutsAvailable) Editor.Windows.EditWin.FocusOrShow(); }); InputActions.Add(options => options.DebugLogWindow, () => { - if (InputOptions.WindowShortcutsAvaliable) + if (InputOptions.WindowShortcutsAvailable) Editor.Windows.DebugLogWin.FocusOrShow(); }); InputActions.Add(options => options.OutputLogWindow, () => { - if (InputOptions.WindowShortcutsAvaliable) + if (InputOptions.WindowShortcutsAvailable) Editor.Windows.OutputLogWin.FocusOrShow(); }); InputActions.Add(options => options.GraphicsQualityWindow, () => { - if (InputOptions.WindowShortcutsAvaliable) + if (InputOptions.WindowShortcutsAvailable) Editor.Windows.GraphicsQualityWin.FocusOrShow(); }); InputActions.Add(options => options.GameCookerWindow, () => { - if (InputOptions.WindowShortcutsAvaliable) + if (InputOptions.WindowShortcutsAvailable) Editor.Windows.GameCookerWin.FocusOrShow(); }); InputActions.Add(options => options.ProfilerWindow, () => { - if (InputOptions.ProfilerShortcutAvaliable) + if (InputOptions.ProfilerShortcutAvailable) Editor.Windows.ProfilerWin.FocusOrShow(); }); InputActions.Add(options => options.ContentFinder, () => { - if (InputOptions.WindowShortcutsAvaliable) + if (InputOptions.WindowShortcutsAvailable) Editor.ContentFinding.ShowSearch(); }); InputActions.Add(options => options.VisualScriptDebuggerWindow, () => { - if (InputOptions.WindowShortcutsAvaliable) + if (InputOptions.WindowShortcutsAvailable) Editor.Windows.VisualScriptDebuggerWin.FocusOrShow(); }); InputActions.Add(options => options.EditorOptionsWindow, () => { - if (InputOptions.WindowShortcutsAvaliable) + if (InputOptions.WindowShortcutsAvailable) Editor.Windows.EditorOptionsWin.FocusOrShow(); }); diff --git a/Source/Engine/CSG/CSGData.cpp b/Source/Engine/CSG/CSGData.cpp index 53f94c175..04a78114e 100644 --- a/Source/Engine/CSG/CSGData.cpp +++ b/Source/Engine/CSG/CSGData.cpp @@ -72,6 +72,8 @@ void RawData::AddSurface(Brush* brush, int32 brushSurfaceIndex, const Guid& surf slot->AddSurface(scaleInLightmap, lightmapUVsBox, firstVertex, vertexCount); // Add surface to brush + if (!brush) + return; auto& brushData = Brushes[brush->GetBrushID()]; if (brushData.Surfaces.Count() != brush->GetSurfacesCount()) brushData.Surfaces.Resize(brush->GetSurfacesCount()); diff --git a/Source/Engine/Content/JsonAsset.cs b/Source/Engine/Content/JsonAsset.cs index 9d11a8c3c..86ed68c1b 100644 --- a/Source/Engine/Content/JsonAsset.cs +++ b/Source/Engine/Content/JsonAsset.cs @@ -93,5 +93,15 @@ namespace FlaxEngine string str = instance != null ? JsonSerializer.Serialize(instance) : null; Data = str; } + +#if FLAX_EDITOR + /// + /// Clears the cached managed instance. The next access will recreate it from the asset data. + /// + internal void ClearInstance() + { + _instance = null; + } +#endif } } diff --git a/Source/Engine/Content/Upgraders/FontAssetUpgrader.h b/Source/Engine/Content/Upgraders/FontAssetUpgrader.h index 4ca6e6d73..b6e8ec12a 100644 --- a/Source/Engine/Content/Upgraders/FontAssetUpgrader.h +++ b/Source/Engine/Content/Upgraders/FontAssetUpgrader.h @@ -19,12 +19,13 @@ public: const Upgrader upgraders[] = { { 3, 4, &Upgrade_3_To_4 }, + { 4, 5, &Upgrade_4_To_5 }, }; setup(upgraders, ARRAY_COUNT(upgraders)); } private: - struct FontOptionsOld + struct FontOptions3 { FontHinting Hinting; FontFlags Flags; @@ -34,8 +35,8 @@ private: { ASSERT(context.Input.SerializedVersion == 3 && context.Output.SerializedVersion == 4); - FontOptionsOld optionsOld; - Platform::MemoryCopy(&optionsOld, context.Input.CustomData.Get(), sizeof(FontOptionsOld)); + FontOptions3 optionsOld; + Platform::MemoryCopy(&optionsOld, context.Input.CustomData.Get(), sizeof(FontOptions3)); FontOptions options; options.Hinting = optionsOld.Hinting; @@ -45,6 +46,30 @@ private: return CopyChunk(context, 0); } + + struct FontOptions4 + { + FontHinting Hinting; + FontFlags Flags; + FontRasterMode RasterMode; + }; + + static bool Upgrade_4_To_5(AssetMigrationContext& context) + { + ASSERT(context.Input.SerializedVersion == 4 && context.Output.SerializedVersion == 5); + + FontOptions4 optionsOld; + Platform::MemoryCopy(&optionsOld, context.Input.CustomData.Get(), sizeof(FontOptions4)); + + FontOptions options; + options.Hinting = optionsOld.Hinting; + options.Flags = optionsOld.Flags; + options.RasterMode = optionsOld.RasterMode; + options.MSDFSize = 32.0f; + context.Output.CustomData.Copy(&options); + + return CopyChunk(context, 0); + } }; #endif diff --git a/Source/Engine/ContentImporters/ImportFont.cpp b/Source/Engine/ContentImporters/ImportFont.cpp index 964f3907e..b01b36627 100644 --- a/Source/Engine/ContentImporters/ImportFont.cpp +++ b/Source/Engine/ContentImporters/ImportFont.cpp @@ -12,13 +12,14 @@ CreateAssetResult ImportFont::Import(CreateAssetContext& context) { // Base - IMPORT_SETUP(FontAsset, 4); + IMPORT_SETUP(FontAsset, 5); // Setup header FontOptions options; options.Hinting = FontHinting::Default; options.Flags = FontFlags::AntiAliasing; options.RasterMode = FontRasterMode::Bitmap; + options.MSDFSize = 32.0f; context.Data.CustomData.Copy(&options); // Open the file diff --git a/Source/Engine/Core/Math/Color.cs b/Source/Engine/Core/Math/Color.cs index dbfd70c7e..be97a7994 100644 --- a/Source/Engine/Core/Math/Color.cs +++ b/Source/Engine/Core/Math/Color.cs @@ -112,6 +112,16 @@ namespace FlaxEngine /// public float ValuesSum => R + G + B + A; + /// + /// Returns true if color is fully transparent (all components are equal zero). + /// + public bool IsTransparent => Mathf.IsZero(R + G + B + A); + + /// + /// Returns true if color has opacity channel in use (different from 1). + /// + public bool HasOpacity => !Mathf.IsOne(A); + /// /// Constructs a new Color with given r,g,b,a component. /// diff --git a/Source/Engine/Level/Actor.h b/Source/Engine/Level/Actor.h index ae6ef4c41..f9ba4fe0e 100644 --- a/Source/Engine/Level/Actor.h +++ b/Source/Engine/Level/Actor.h @@ -783,12 +783,12 @@ public: /// /// Draws debug shapes for the actor and all child scripts. /// - API_FUNCTION() virtual void OnDebugDraw(); + API_FUNCTION(Attributes="NoAnimate") virtual void OnDebugDraw(); /// /// Draws debug shapes for the selected actor and all child scripts. /// - API_FUNCTION() virtual void OnDebugDrawSelected(); + API_FUNCTION(Attributes="NoAnimate") virtual void OnDebugDrawSelected(); #endif public: @@ -1054,64 +1054,64 @@ public: /// Serializes the actor object to the Json string. Serialized are only this actor properties but no child actors nor scripts. Serializes references to the other objects in a proper way using IDs. /// /// The Json container with serialized actor data. - API_FUNCTION() String ToJson(); + API_FUNCTION(Attributes="NoAnimate") String ToJson(); /// /// Deserializes the actor object to the Json string. Deserialized are only this actor properties but no child actors nor scripts. /// /// The serialized actor data (state). - API_FUNCTION() void FromJson(const StringAnsiView& json); + API_FUNCTION(Attributes="NoAnimate") void FromJson(const StringAnsiView& json); /// /// Clones actor including all scripts and any child actors (whole scene tree). Objects are duplicated via serialization (any transient/non-saved state is ignored). /// - API_FUNCTION() Actor* Clone(); + API_FUNCTION(Attributes="NoAnimate") Actor* Clone(); public: /// /// Called when actor gets added to game systems. Occurs on BeginPlay event or when actor gets activated in hierarchy. Use this event to register object to other game system (eg. audio). /// - API_FUNCTION() virtual void OnEnable(); + API_FUNCTION(Attributes="NoAnimate") virtual void OnEnable(); /// /// Called when actor gets removed from game systems. Occurs on EndPlay event or when actor gets inactivated in hierarchy. Use this event to unregister object from other game system (eg. audio). /// - API_FUNCTION() virtual void OnDisable(); + API_FUNCTION(Attributes="NoAnimate") virtual void OnDisable(); /// /// Called when actor parent gets changed. /// - API_FUNCTION() virtual void OnParentChanged(); + API_FUNCTION(Attributes="NoAnimate") virtual void OnParentChanged(); /// /// Called when actor transform gets changed. /// - API_FUNCTION() virtual void OnTransformChanged(); + API_FUNCTION(Attributes="NoAnimate") virtual void OnTransformChanged(); /// /// Called when actor active state gets changed. /// - API_FUNCTION() virtual void OnActiveChanged(); + API_FUNCTION(Attributes="NoAnimate") virtual void OnActiveChanged(); /// /// Called when actor active in tree state gets changed. /// - API_FUNCTION() virtual void OnActiveInTreeChanged(); + API_FUNCTION(Attributes="NoAnimate") virtual void OnActiveInTreeChanged(); /// /// Called when order in parent children array gets changed. /// - API_FUNCTION() virtual void OnOrderInParentChanged(); + API_FUNCTION(Attributes="NoAnimate") virtual void OnOrderInParentChanged(); /// /// Called when actor static flag gets changed. /// - API_FUNCTION() virtual void OnStaticFlagsChanged(); + API_FUNCTION(Attributes="NoAnimate") virtual void OnStaticFlagsChanged(); /// /// Called when layer gets changed. /// - API_FUNCTION() virtual void OnLayerChanged(); + API_FUNCTION(Attributes="NoAnimate") virtual void OnLayerChanged(); /// /// Called when adding object to the game. diff --git a/Source/Engine/Level/Actors/BoxBrush.cpp b/Source/Engine/Level/Actors/BoxBrush.cpp index 06bc047bc..1935a8b51 100644 --- a/Source/Engine/Level/Actors/BoxBrush.cpp +++ b/Source/Engine/Level/Actors/BoxBrush.cpp @@ -50,7 +50,18 @@ Array BoxBrush::GetSurfaces() const void BoxBrush::SetSurfaces(const Array& value) { CHECK(value.Count() == ARRAY_COUNT(Surfaces)); - Platform::MemoryCopy(Surfaces, value.Get(), sizeof(Surfaces)); + for (int32 i = 0; i < ARRAY_COUNT(Surfaces); i++) + { + auto& dst = Surfaces[i]; + const auto& src = value[i]; + dst.Brush = this; + dst.Index = i; + dst.Material = src.Material; + dst.TexCoordScale = src.TexCoordScale; + dst.TexCoordOffset = src.TexCoordOffset; + dst.TexCoordRotation = src.TexCoordRotation; + dst.ScaleInLightmap = src.ScaleInLightmap; + } OnBrushModified(); } @@ -67,10 +78,7 @@ void BoxBrush::SetCenter(const Vector3& value) { if (value == _center) return; - _center = value; - - // Fire events UpdateBounds(); OnBrushModified(); } @@ -79,10 +87,7 @@ void BoxBrush::SetSize(const Vector3& value) { if (value == _size) return; - _size = value; - - // Fire events UpdateBounds(); OnBrushModified(); } @@ -98,7 +103,7 @@ void BoxBrush::GetSurfaces(CSG::Surface surfaces[6]) surfaces[5].Normal = Vector3::Backward; // Calculate final transformation - const auto transform = _transform.LocalToWorld(Transform(_center, Quaternion::Identity, _size)); + const auto transform = _transform.LocalToWorld(Transform(_center, Quaternion::Identity, _size.GetAbsolute())); // Set size and scale surfaces[0].D = surfaces[1].D = transform.Scale.X / 2; @@ -163,6 +168,14 @@ void BoxBrush::GetVertices(int32 surfaceIndex, Array& outputData) const } } +void BoxBrush::UpdateBounds() +{ + OrientedBoundingBox::CreateCentered(_center, _size.GetAbsolute(), _bounds); + _bounds.Transform(_transform); + _bounds.GetBoundingBox(_box); + BoundingSphere::FromBox(_box, _sphere); +} + void BoxBrush::Serialize(SerializeStream& stream, const void* otherObj) { // Base @@ -230,7 +243,8 @@ bool BoxBrush::IntersectsItself(const Ray& ray, Real& distance, Vector3& normal) void BoxBrush::OnDebugDrawSelected() { - DEBUG_DRAW_WIRE_BOX(_bounds, Color::Yellow, 0, false); + DEBUG_DRAW_WIRE_BOX(_bounds, Color::Yellow.AlphaMultiplied(0.2f), 0, false); + DEBUG_DRAW_WIRE_BOX(_bounds, Color::Yellow, 0, true); // Base Actor::OnDebugDrawSelected(); diff --git a/Source/Engine/Level/Actors/BoxBrush.h b/Source/Engine/Level/Actors/BoxBrush.h index 519ca5ad8..17941b16b 100644 --- a/Source/Engine/Level/Actors/BoxBrush.h +++ b/Source/Engine/Level/Actors/BoxBrush.h @@ -188,13 +188,7 @@ public: API_FUNCTION() void GetVertices(int32 surfaceIndex, API_PARAM(Out) Array& outputData) const; private: - FORCE_INLINE void UpdateBounds() - { - OrientedBoundingBox::CreateCentered(_center, _size, _bounds); - _bounds.Transform(_transform); - _bounds.GetBoundingBox(_box); - BoundingSphere::FromBox(_box, _sphere); - } + void UpdateBounds(); public: // [Actor] diff --git a/Source/Engine/Render2D/Font.cpp b/Source/Engine/Render2D/Font.cpp index ea098af2c..80fe784c4 100644 --- a/Source/Engine/Render2D/Font.cpp +++ b/Source/Engine/Render2D/Font.cpp @@ -31,7 +31,6 @@ Font::Font(FontAsset* parentAsset, float size) : ManagedScriptingObject(SpawnParams(Guid::New(), Font::TypeInitializer)) , _asset(parentAsset) , _size(size) - , _characters(512) { _asset->_fonts.Add(this); @@ -55,14 +54,25 @@ Font::~Font() void Font::GetCharacter(Char c, FontCharacterEntry& result, bool enableFallback) { // Try to get the character or cache it if cannot be found - if (!_characters.TryGet(c, result)) + const FontOptions& options = _asset->GetOptions(); + const auto key = Pair(options.RasterMode == FontRasterMode::MSDF ? options.MSDFSize : GetSize(), c); + if (_asset->_characterCache.TryGet(key, result)) + { + // With MSDF font introduced, cached entry may be created by a different font (with same MSDFSize) + // This is to ensure returned entry has a reference to a font whose size matches the font being used to render + result.Font = this; + } + else { // This thread race condition may happen in editor but in game we usually do all stuff with fonts on main thread (chars caching) ScopeLock lock(_asset->Locker); // Handle situation when more than one thread wants to get the same character - if (_characters.TryGet(c, result)) + if (_asset->_characterCache.TryGet(key, result)) + { + result.Font = this; return; + } // Try to use fallback font if character is missing if (enableFallback && !_asset->ContainsChar(c)) @@ -70,13 +80,9 @@ void Font::GetCharacter(Char c, FontCharacterEntry& result, bool enableFallback) for (int32 fallbackIndex = 0; fallbackIndex < FallbackFonts.Count(); fallbackIndex++) { FontAsset* fallbackFont = FallbackFonts.Get()[fallbackIndex].Get(); - if (fallbackFont && _asset->GetOptions().RasterMode == FontRasterMode::MSDF) - { - fallbackFont = fallbackFont->GetMSDF(); - } if (fallbackFont && fallbackFont->ContainsChar(c)) { - fallbackFont->CreateFont(GetSize())->GetCharacter(c, result, enableFallback); + fallbackFont->GetRasterMode(options.RasterMode)->CreateFont(GetSize())->GetCharacter(c, result, enableFallback); return; } } @@ -87,7 +93,7 @@ void Font::GetCharacter(Char c, FontCharacterEntry& result, bool enableFallback) ASSERT(result.Font); // Add to the dictionary - _characters.Add(c, result); + _asset->_characterCache.Add(key, result); } } @@ -134,11 +140,32 @@ void Font::Invalidate() { ScopeLock lock(_asset->Locker); - for (auto i = _characters.Begin(); i.IsNotEnd(); ++i) + // Invalidate cached characters (from atlas) + for (auto i = _asset->_characterCache.Begin(); i.IsNotEnd(); ++i) { - FontManager::Invalidate(i->Value); + if (i->Value.Font == this) + { + FontManager::Invalidate(i->Value); + _asset->_characterCache.Remove(i); + } } - _characters.Clear(); + + // Rebuild font metrics + FlushFaceSize(); + const FT_Face face = _asset->GetFTFace(); + ASSERT(face != nullptr); + _height = Convert26Dot6ToRoundedPixel(FT_MulFix(face->height, face->size->metrics.y_scale)); + _hasKerning = FT_HAS_KERNING(face) != 0; + _ascender = Convert26Dot6ToRoundedPixel(face->size->metrics.ascender); + _descender = Convert26Dot6ToRoundedPixel(face->size->metrics.descender); + _lineGap = _height - _ascender + _descender; + _kerningTable.Clear(); +} + +float Font::GetScale(float layoutScale) const +{ + const FontOptions& options = _asset->GetOptions(); + return layoutScale / FontManager::FontScale * (options.RasterMode == FontRasterMode::MSDF ? _size / options.MSDFSize : 1.0f); } void Font::ProcessText(const StringView& text, Array>& outputLines, const TextLayoutOptions& layout) @@ -151,7 +178,7 @@ void Font::ProcessText(const StringView& text, Array(_height) * layout.BaseLinesGapScale * scale; tmpLine.Location = Float2::Zero; @@ -195,6 +222,7 @@ void Font::ProcessText(const StringView& text, ArrayGetScale(layout.Scale); // Get kerning if (!isWhitespace && previous.IsValid) @@ -206,7 +234,7 @@ void Font::ProcessText(const StringView& text, Array> lines; ProcessText(text, lines, layout); ASSERT(lines.HasItems()); - float scale = layout.Scale / FontManager::FontScale; + const float scale = GetScale(layout.Scale); float baseLinesDistance = static_cast(_height) * layout.BaseLinesGapScale * scale; // Offset position to match lines origin space @@ -378,12 +406,13 @@ int32 Font::HitTestText(const StringView& text, const Float2& location, const Te // Cache current character const Char currentChar = text[currentIndex]; GetCharacter(currentChar, entry); + const float entryScale = entry.Font->GetScale(layout.Scale); const bool isWhitespace = StringUtils::IsWhitespace(currentChar); // Apply kerning if (!isWhitespace && previous.IsValid) { - x += entry.Font->GetKerning(previous.Character, entry.Character); + x += entry.Font->GetKerning(previous.Character, entry.Character) * entryScale; } previous = entry; @@ -402,7 +431,7 @@ int32 Font::HitTestText(const StringView& text, const Float2& location, const Te } // Move - x += entry.AdvanceX * scale; + x += entry.AdvanceX * entryScale; } // Test line end edge @@ -445,7 +474,7 @@ Float2 Font::GetCharPosition(const StringView& text, int32 index, const TextLayo Array> lines; ProcessText(text, lines, layout); ASSERT(lines.HasItems()); - float scale = layout.Scale / FontManager::FontScale; + const float scale = GetScale(layout.Scale); float baseLinesDistance = static_cast(_height) * layout.BaseLinesGapScale * scale; // Find line with that position @@ -466,17 +495,18 @@ Float2 Font::GetCharPosition(const StringView& text, int32 index, const TextLayo // Cache current character const Char currentChar = text[currentIndex]; GetCharacter(currentChar, entry); + const float entryScale = entry.Font->GetScale(layout.Scale); const bool isWhitespace = StringUtils::IsWhitespace(currentChar); // Apply kerning if (!isWhitespace && previous.IsValid) { - charPos.X += entry.Font->GetKerning(previous.Character, entry.Character); + charPos.X += entry.Font->GetKerning(previous.Character, entry.Character) * entryScale; } previous = entry; // Move - charPos.X += entry.AdvanceX * scale; + charPos.X += entry.AdvanceX * entryScale; } // Upper left corner of the character @@ -492,7 +522,9 @@ void Font::FlushFaceSize() const { // Set the character size const FT_Face face = _asset->GetFTFace(); - const FT_Error error = FT_Set_Char_Size(face, 0, ConvertPixelTo26Dot6(_size * FontManager::FontScale), DefaultDPI, DefaultDPI); + const FontOptions& options = _asset->GetOptions(); + float size = options.RasterMode == FontRasterMode::MSDF ? options.MSDFSize : _size; + const FT_Error error = FT_Set_Char_Size(face, 0, ConvertPixelTo26Dot6(size * FontManager::FontScale), DefaultDPI, DefaultDPI); if (error) { LOG_FT_ERROR(error); diff --git a/Source/Engine/Render2D/Font.h b/Source/Engine/Render2D/Font.h index b2365c0f8..8ad3a5c17 100644 --- a/Source/Engine/Render2D/Font.h +++ b/Source/Engine/Render2D/Font.h @@ -10,7 +10,7 @@ #include "TextLayoutOptions.h" class FontAsset; -struct FontTextureAtlasSlot; +struct FontCharacterEntry; // The default DPI that engine is using #define DefaultDPI 96 @@ -119,110 +119,6 @@ struct TIsPODType enum { Value = true }; }; -// Font glyph metrics: -// -// xmin xmax -// | | -// |<-------- width -------->| -// | | -// | +-------------------------+----------------- ymax -// | | ggggggggg ggggg | ^ ^ -// | | g:::::::::ggg::::g | | | -// | | g:::::::::::::::::g | | | -// | | g::::::ggggg::::::gg | | | -// | | g:::::g g:::::g | | | -// offsetX -|-------->| g:::::g g:::::g | offsetY | -// | | g:::::g g:::::g | | | -// | | g::::::g g:::::g | | | -// | | g:::::::ggggg:::::g | | | -// | | g::::::::::::::::g | | height -// | | gg::::::::::::::g | | | -// baseline ---*---------|---- gggggggg::::::g-----*-------- | -// / | | g:::::g | | -// origin | | gggggg g:::::g | | -// | | g:::::gg gg:::::g | | -// | | g::::::ggg:::::::g | | -// | | gg:::::::::::::g | | -// | | ggg::::::ggg | | -// | | gggggg | v -// | +-------------------------+----------------- ymin -// | | -// |------------- advanceX ----------->| - -/// -/// The cached font character entry (read for rendering and further processing). -/// -API_STRUCT(NoDefault) struct FLAXENGINE_API FontCharacterEntry -{ - DECLARE_SCRIPTING_TYPE_MINIMAL(FontCharacterEntry); - - /// - /// The character represented by this entry. - /// - API_FIELD() Char Character; - - /// - /// True if entry is valid, otherwise false. - /// - API_FIELD() bool IsValid = false; - - /// - /// The index to a specific texture in the font cache. - /// - API_FIELD() byte TextureIndex; - - /// - /// The left bearing expressed in integer pixels. - /// - API_FIELD() int16 OffsetX; - - /// - /// The top bearing expressed in integer pixels. - /// - API_FIELD() int16 OffsetY; - - /// - /// The amount to advance in X before drawing the next character in a string. - /// - API_FIELD() int16 AdvanceX; - - /// - /// The distance from baseline to glyph top most point. - /// - API_FIELD() int16 BearingY; - - /// - /// The height in pixels of the glyph. - /// - API_FIELD() int16 Height; - - /// - /// The start location of the character in the texture (in texture coordinates space). - /// - API_FIELD() Float2 UV; - - /// - /// The size the character in the texture (in texture coordinates space). - /// - API_FIELD() Float2 UVSize; - - /// - /// The slot in texture atlas, containing the pixel data of the glyph. - /// - API_FIELD() const FontTextureAtlasSlot* Slot; - - /// - /// The owner font. - /// - API_FIELD() const class Font* Font; -}; - -template<> -struct TIsPODType -{ - enum { Value = true }; -}; - /// /// Represents font object that can be using during text rendering (it uses Font Asset but with pre-cached data for chosen font properties). /// @@ -239,7 +135,6 @@ private: int32 _descender; int32 _lineGap; bool _hasKerning; - Dictionary _characters; mutable Dictionary _kerningTable; public: @@ -327,7 +222,7 @@ public: /// /// The character. /// The output character entry. - /// True if fallback to secondary font when the primary font doesn't contains this character. + /// True if fallback to secondary font when the primary font doesn't contain this character. void GetCharacter(Char c, FontCharacterEntry& result, bool enableFallback = true); /// @@ -345,11 +240,18 @@ public: API_FUNCTION() void CacheText(const StringView& text); /// - /// Invalidates all cached dynamic font atlases using this font. Can be used to reload font characters after changing font asset options. + /// Refresh cached metrics and invalidates all cached characters (in atlases). Can be used after changing font asset options. /// API_FUNCTION() void Invalidate(); public: + /// + /// Gets the scale factor that maps the rasterized font size to the actual rendered size. + /// + /// The layout scale. + /// The scale factor. + float GetScale(float layoutScale) const; + /// /// Processes text to get cached lines for rendering. /// diff --git a/Source/Engine/Render2D/FontAsset.cpp b/Source/Engine/Render2D/FontAsset.cpp index 34ffe7330..1b58d472c 100644 --- a/Source/Engine/Render2D/FontAsset.cpp +++ b/Source/Engine/Render2D/FontAsset.cpp @@ -69,7 +69,7 @@ void FontAsset::unload(bool isReloading) _fontFile.Release(); _virtualBold = nullptr; _virtualItalic = nullptr; - _virtualMSDF = nullptr; + _virtualRasterMode = nullptr; } AssetChunksFlag FontAsset::getChunksToPreload() const @@ -101,6 +101,26 @@ FontFlags FontAsset::GetStyle() const void FontAsset::SetOptions(const FontOptions& value) { _options = value; + Invalidate(); + + if (_virtualBold) + { + auto options = _options; + options.Flags |= FontFlags::Bold; + _virtualBold->SetOptions(options); + } + if (_virtualItalic) + { + auto options = _options; + options.Flags |= FontFlags::Italic; + _virtualItalic->SetOptions(options); + } + if (_virtualRasterMode) + { + auto options = _options; + options.RasterMode = _options.RasterMode == FontRasterMode::MSDF ? FontRasterMode::Bitmap : FontRasterMode::MSDF; + _virtualRasterMode->SetOptions(options); + } } Font* FontAsset::CreateFont(float size) @@ -156,20 +176,20 @@ FontAsset* FontAsset::GetItalic() return _virtualItalic; } -FontAsset* FontAsset::GetMSDF() +FontAsset* FontAsset::GetRasterMode(FontRasterMode rasterMode) { ScopeLock lock(Locker); - if (_options.RasterMode == FontRasterMode::MSDF) + if (_options.RasterMode == rasterMode) return this; - if (!_virtualMSDF) + if (!_virtualRasterMode) { - _virtualMSDF = Content::CreateVirtualAsset(); - _virtualMSDF->Init(_fontFile); + _virtualRasterMode = Content::CreateVirtualAsset(); + _virtualRasterMode->Init(_fontFile); auto options = _options; - options.RasterMode = FontRasterMode::MSDF; - _virtualMSDF->SetOptions(options); + options.RasterMode = rasterMode; + _virtualRasterMode->SetOptions(options); } - return _virtualMSDF; + return _virtualRasterMode; } bool FontAsset::Init(const BytesContainer& fontFile) @@ -214,6 +234,13 @@ bool FontAsset::ContainsChar(Char c) const void FontAsset::Invalidate() { ScopeLock lock(Locker); + + // Invalidate cached characters (from atlas) + for (auto& entry : _characterCache) + FontManager::Invalidate(entry.Value); + _characterCache.Clear(); + + // Refresh cached metrics of all fonts created from this asset for (auto font : _fonts) font->Invalidate(); } diff --git a/Source/Engine/Render2D/FontAsset.cs b/Source/Engine/Render2D/FontAsset.cs index 624daab5b..f49ae4ebb 100644 --- a/Source/Engine/Render2D/FontAsset.cs +++ b/Source/Engine/Render2D/FontAsset.cs @@ -13,7 +13,7 @@ namespace FlaxEngine /// true if this object has the same value as ; otherwise, false public bool Equals(FontOptions other) { - return Hinting == other.Hinting && Flags == other.Flags && RasterMode == other.RasterMode; + return Hinting == other.Hinting && Flags == other.Flags && RasterMode == other.RasterMode && MSDFSize == other.MSDFSize; } /// @@ -25,7 +25,7 @@ namespace FlaxEngine /// public override int GetHashCode() { - return HashCode.Combine((int)Hinting, (int)Flags, (int)RasterMode); + return HashCode.Combine((int)Hinting, (int)Flags, (int)RasterMode, MSDFSize); } /// @@ -36,7 +36,7 @@ namespace FlaxEngine /// true if has the same value as ; otherwise, false. public static bool operator ==(FontOptions left, FontOptions right) { - return left.Hinting == right.Hinting && left.Flags == right.Flags && left.RasterMode == right.RasterMode; + return left.Hinting == right.Hinting && left.Flags == right.Flags && left.RasterMode == right.RasterMode && left.MSDFSize == right.MSDFSize; } /// @@ -47,7 +47,7 @@ namespace FlaxEngine /// true if has a different value than ; otherwise,false. public static bool operator !=(FontOptions left, FontOptions right) { - return left.Hinting != right.Hinting || left.Flags != right.Flags || left.RasterMode != right.RasterMode; + return left.Hinting != right.Hinting || left.Flags != right.Flags || left.RasterMode != right.RasterMode || left.MSDFSize != right.MSDFSize; } } } diff --git a/Source/Engine/Render2D/FontAsset.h b/Source/Engine/Render2D/FontAsset.h index c3aa141a8..0a5c67255 100644 --- a/Source/Engine/Render2D/FontAsset.h +++ b/Source/Engine/Render2D/FontAsset.h @@ -4,6 +4,10 @@ #include "Engine/Content/BinaryAsset.h" #include "Engine/Content/AssetReference.h" +#include "Engine/Core/Collections/Array.h" +#include "Engine/Core/Collections/Dictionary.h" +#include "Engine/Core/Math/Vector2.h" +#include "Engine/Render2D/FontCharacterEntry.h" class Font; class FontManager; @@ -105,6 +109,11 @@ API_STRUCT() struct FontOptions /// The font rasterization mode. /// API_FIELD() FontRasterMode RasterMode; + + /// + /// The font size used when generating MSDF font atlases. + /// + API_FIELD() float MSDFSize; }; /// @@ -112,7 +121,7 @@ API_STRUCT() struct FontOptions /// API_CLASS(NoSpawn) class FLAXENGINE_API FontAsset : public BinaryAsset { - DECLARE_BINARY_ASSET_HEADER(FontAsset, 4); + DECLARE_BINARY_ASSET_HEADER(FontAsset, 5); friend Font; private: @@ -120,9 +129,10 @@ private: FontOptions _options; BytesContainer _fontFile; Array> _fonts; + Dictionary, FontCharacterEntry> _characterCache; AssetReference _virtualBold; AssetReference _virtualItalic; - AssetReference _virtualMSDF; + AssetReference _virtualRasterMode; public: /// @@ -182,10 +192,10 @@ public: API_FUNCTION() FontAsset* GetItalic(); /// - /// Gets the MSDF version of the font. Returns itself or creates a new virtual font asset using this font but rasterized with Multi-channel Signed Distance Field (MSDF). + /// Gets the different rasterization mode of the font. Returns itself or creates a new virtual font asset using this font but rasterized with the specified mode. /// /// The virtual font or this. - API_FUNCTION() FontAsset* GetMSDF(); + API_FUNCTION() FontAsset* GetRasterMode(FontRasterMode rasterMode); /// /// Initializes the font with a custom font file data. diff --git a/Source/Engine/Render2D/FontCharacterEntry.h b/Source/Engine/Render2D/FontCharacterEntry.h new file mode 100644 index 000000000..f73099beb --- /dev/null +++ b/Source/Engine/Render2D/FontCharacterEntry.h @@ -0,0 +1,109 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +#pragma once + +#include "Engine/Core/Math/Vector2.h" + +// Font glyph metrics: +// +// xmin xmax +// | | +// |<-------- width -------->| +// | | +// | +-------------------------+----------------- ymax +// | | ggggggggg ggggg | ^ ^ +// | | g:::::::::ggg::::g | | | +// | | g:::::::::::::::::g | | | +// | | g::::::ggggg::::::gg | | | +// | | g:::::g g:::::g | | | +// offsetX -|-------->| g:::::g g:::::g | offsetY | +// | | g:::::g g:::::g | | | +// | | g::::::g g:::::g | | | +// | | g:::::::ggggg:::::g | | | +// | | g::::::::::::::::g | | height +// | | gg::::::::::::::g | | | +// baseline ---*---------|---- gggggggg::::::g-----*-------- | +// / | | g:::::g | | +// origin | | gggggg g:::::g | | +// | | g:::::gg gg:::::g | | +// | | g::::::ggg:::::::g | | +// | | gg:::::::::::::g | | +// | | ggg::::::ggg | | +// | | gggggg | v +// | +-------------------------+----------------- ymin +// | | +// |------------- advanceX ----------->| + +/// +/// The cached font character entry (read for rendering and further processing). +/// +API_STRUCT(NoDefault) struct FLAXENGINE_API FontCharacterEntry +{ + DECLARE_SCRIPTING_TYPE_MINIMAL(FontCharacterEntry); + + /// + /// The character represented by this entry. + /// + API_FIELD() Char Character; + + /// + /// True if entry is valid, otherwise false. + /// + API_FIELD() bool IsValid = false; + + /// + /// The index to a specific texture in the font cache. + /// + API_FIELD() byte TextureIndex; + + /// + /// The left bearing expressed in integer pixels. + /// + API_FIELD() int16 OffsetX; + + /// + /// The top bearing expressed in integer pixels. + /// + API_FIELD() int16 OffsetY; + + /// + /// The amount to advance in X before drawing the next character in a string. + /// + API_FIELD() int16 AdvanceX; + + /// + /// The distance from baseline to glyph top most point. + /// + API_FIELD() int16 BearingY; + + /// + /// The height in pixels of the glyph. + /// + API_FIELD() int16 Height; + + /// + /// The start location of the character in the texture (in texture coordinates space). + /// + API_FIELD() Float2 UV; + + /// + /// The size the character in the texture (in texture coordinates space). + /// + API_FIELD() Float2 UVSize; + + /// + /// The slot in texture atlas, containing the pixel data of the glyph. + /// + API_FIELD() const struct FontTextureAtlasSlot* Slot; + + /// + /// The owner font. + /// + API_FIELD() const class Font* Font; +}; + +template<> +struct TIsPODType +{ + enum { Value = true }; +}; diff --git a/Source/Engine/Render2D/Render2D.cpp b/Source/Engine/Render2D/Render2D.cpp index 392ec2229..2c540e1aa 100644 --- a/Source/Engine/Render2D/Render2D.cpp +++ b/Source/Engine/Render2D/Render2D.cpp @@ -1203,7 +1203,8 @@ void Render2D::DrawText(Font* font, const StringView& text, const Color& color, Float2 invAtlasSize = Float2::One; FontCharacterEntry previous; int32 kerning; - float scale = 1.0f / FontManager::FontScale; + const FontOptions& options = font->GetAsset()->GetOptions(); + const float scale = font->GetScale(1.0f); const bool enableFallbackFonts = EnumHasAllFlags(Features, RenderingFeatures::FallbackFonts); // Render all characters @@ -1216,7 +1217,7 @@ void Render2D::DrawText(Font* font, const StringView& text, const Color& color, } else { - drawCall.Type = font->GetAsset()->GetOptions().RasterMode == FontRasterMode::MSDF ? DrawCallType::DrawCharMSDF : DrawCallType::DrawChar; + drawCall.Type = options.RasterMode == FontRasterMode::MSDF ? DrawCallType::DrawCharMSDF : DrawCallType::DrawChar; drawCall.AsChar.Mat = nullptr; } Float2 pointer = location; @@ -1231,6 +1232,9 @@ void Render2D::DrawText(Font* font, const StringView& text, const Color& color, // Get character entry font->GetCharacter(currentChar, entry, enableFallbackFonts); + // Fallback fonts may have different MSDFSize, so we need to calculate scale per character + const float entryScale = entry.Font->GetScale(1.0f); + // Check if need to select/change font atlas (since characters even in the same font may be located in different atlases) if (fontAtlas == nullptr || entry.TextureIndex != fontAtlasIndex) { @@ -1262,18 +1266,16 @@ void Render2D::DrawText(Font* font, const StringView& text, const Color& color, { kerning = 0; } - pointer.X += kerning * scale; + pointer.X += kerning * entryScale; previous = entry; // Omit whitespace characters if (!isWhitespace) { // Calculate character size and atlas coordinates - const float x = pointer.X + entry.OffsetX * scale; - const float y = pointer.Y + (font->GetHeight() + font->GetDescender() - entry.OffsetY) * scale; - - Rectangle charRect(x, y, entry.UVSize.X * scale, entry.UVSize.Y * scale); - + const float x = pointer.X + entry.OffsetX * entryScale; + const float y = pointer.Y - entry.OffsetY * entryScale + (font->GetHeight() + font->GetDescender()) * scale; + Rectangle charRect(x, y, entry.UVSize.X * entryScale, entry.UVSize.Y * entryScale); Float2 upperLeftUV = entry.UV * invAtlasSize; Float2 rightBottomUV = (entry.UV + entry.UVSize) * invAtlasSize; @@ -1285,7 +1287,7 @@ void Render2D::DrawText(Font* font, const StringView& text, const Color& color, } // Move - pointer.X += entry.AdvanceX * scale; + pointer.X += entry.AdvanceX * entryScale; } else { @@ -1318,7 +1320,8 @@ void Render2D::DrawText(Font* font, const StringView& text, const Color& color, Float2 invAtlasSize = Float2::One; FontCharacterEntry previous; int32 kerning; - float scale = layout.Scale / FontManager::FontScale; + const FontOptions& options = font->GetAsset()->GetOptions(); + const float scale = font->GetScale(layout.Scale); const bool enableFallbackFonts = EnumHasAllFlags(Features, RenderingFeatures::FallbackFonts); // Process text to get lines @@ -1335,7 +1338,7 @@ void Render2D::DrawText(Font* font, const StringView& text, const Color& color, } else { - drawCall.Type = font->GetAsset()->GetOptions().RasterMode == FontRasterMode::MSDF ? DrawCallType::DrawCharMSDF : DrawCallType::DrawChar; + drawCall.Type = options.RasterMode == FontRasterMode::MSDF ? DrawCallType::DrawCharMSDF : DrawCallType::DrawChar; drawCall.AsChar.Mat = nullptr; } for (int32 lineIndex = 0; lineIndex < Lines.Count(); lineIndex++) @@ -1355,6 +1358,9 @@ void Render2D::DrawText(Font* font, const StringView& text, const Color& color, // Get character entry font->GetCharacter(currentChar, entry, enableFallbackFonts); + // Fallback fonts may have different MSDFSize, so we need to calculate scale per character + const float entryScale = entry.Font->GetScale(layout.Scale); + // Check if need to select/change font atlas (since characters even in the same font may be located in different atlases) if (fontAtlas == nullptr || entry.TextureIndex != fontAtlasIndex) { @@ -1384,19 +1390,17 @@ void Render2D::DrawText(Font* font, const StringView& text, const Color& color, { kerning = 0; } - pointer.X += (float)kerning * scale; + pointer.X += (float)kerning * entryScale; previous = entry; // Omit whitespace characters if (!isWhitespace) { // Calculate character size and atlas coordinates - const float x = pointer.X + entry.OffsetX * scale; - const float y = pointer.Y - entry.OffsetY * scale + Math::Ceil((font->GetHeight() + font->GetDescender()) * scale); - - Rectangle charRect(x, y, entry.UVSize.X * scale, entry.UVSize.Y * scale); + const float x = pointer.X + entry.OffsetX * entryScale; + const float y = pointer.Y - entry.OffsetY * entryScale + Math::Ceil((font->GetHeight() + font->GetDescender()) * scale); + Rectangle charRect(x, y, entry.UVSize.X * entryScale, entry.UVSize.Y * entryScale); charRect.Offset(layout.Bounds.Location); - Float2 upperLeftUV = entry.UV * invAtlasSize; Float2 rightBottomUV = (entry.UV + entry.UVSize) * invAtlasSize; @@ -1408,7 +1412,7 @@ void Render2D::DrawText(Font* font, const StringView& text, const Color& color, } // Move - pointer.X += entry.AdvanceX * scale; + pointer.X += entry.AdvanceX * entryScale; } } } diff --git a/Source/Engine/Tools/TextureTool/TextureTool.stb.cpp b/Source/Engine/Tools/TextureTool/TextureTool.stb.cpp index 27d4fcb78..c9bfde80e 100644 --- a/Source/Engine/Tools/TextureTool/TextureTool.stb.cpp +++ b/Source/Engine/Tools/TextureTool/TextureTool.stb.cpp @@ -575,7 +575,7 @@ bool TextureTool::ImportTextureStb(ImageType type, Span bytes, TextureData } } - // Use two data containers for texture importing for more optimzied performance + // Use two data containers for texture importing for more optimized performance TextureData textureDataTmp; TextureData* textureDataSrc = &textureData; TextureData* textureDataDst = &textureDataTmp; diff --git a/Source/Engine/UI/GUI/Common/Dropdown.cs b/Source/Engine/UI/GUI/Common/Dropdown.cs index b91d70dc9..325a5c34d 100644 --- a/Source/Engine/UI/GUI/Common/Dropdown.cs +++ b/Source/Engine/UI/GUI/Common/Dropdown.cs @@ -595,7 +595,7 @@ namespace FlaxEngine.GUI Size = new Float2(size.X - margin, size.Y), Font = Font, TextColor = TextColor * 0.9f, - TextColorHighlighted = BackgroundColorSelected.Brightness < 0.05f ? Color.Lerp(TextColorHighlighted, Color.White, 0.3f) : TextColorHighlighted, + TextColorHighlighted = (Mathf.IsZero(BackgroundColorSelected.Brightness) || BackgroundColorSelected.IsTransparent) ? Color.Lerp(TextColorHighlighted, Color.White, 0.3f) : TextColorHighlighted, HorizontalAlignment = HorizontalAlignment, VerticalAlignment = VerticalAlignment, Text = _items[i], diff --git a/Source/Engine/UI/GUI/Common/RichTextBox.Parsing.cs b/Source/Engine/UI/GUI/Common/RichTextBox.Parsing.cs index bb6ee22a5..4de19f2f2 100644 --- a/Source/Engine/UI/GUI/Common/RichTextBox.Parsing.cs +++ b/Source/Engine/UI/GUI/Common/RichTextBox.Parsing.cs @@ -225,37 +225,123 @@ namespace FlaxEngine.GUI var lines = font.ProcessText(_text, ref textBlock.Range); if (lines == null || lines.Length == 0) return; + var wrapWidth = _wrapping != TextWrapping.NoWrap ? Width : -1.0f; + for (int i = 0; i < lines.Length; i++) { ref var line = ref lines[i]; - textBlock.Range = new TextRange + var lineRange = new TextRange { StartIndex = start + line.FirstCharIndex, EndIndex = start + line.LastCharIndex + 1, }; + if (i != 0) { context.Caret.X = 0; - OnLineAdded(ref context, textBlock.Range.StartIndex - 1); + OnLineAdded(ref context, lineRange.StartIndex - 1); } - textBlock.Bounds = new Rectangle(context.Caret, line.Size); - textBlock.Bounds.X += line.Location.X; - context.AddTextBlock(ref textBlock); - } - - // Update the caret location - ref var lastLine = ref lines[lines.Length - 1]; - if (lines.Length == 1) - { - context.Caret.X += lastLine.Size.X; - } - else - { - context.Caret.X = lastLine.Size.X; + if (wrapWidth > 0 && context.Caret.X + line.Size.X > wrapWidth) + { + // Line overflows the available width - split it into multiple wrapped lines + AddWrappedTextBlocks(ref context, ref textBlock, font, lineRange, wrapWidth, _wrapping); + } + else + { + // Add block + textBlock.Range = lineRange; + textBlock.Bounds = new Rectangle(context.Caret, line.Size); + textBlock.Bounds.X += line.Location.X; + context.AddTextBlock(ref textBlock); + context.Caret.X += line.Size.X; + } } } + /// + /// Splits the given text range into multiple text blocks (lines) so it fits within the available wrapping width, correctly continuing from the current caret position (eg. when a differently-styled text run follows on the same line). + /// + /// The parsing context. + /// The template text block (style is reused, range and bounds get overriden per produced block). + /// The font used to measure and render the text. + /// The text range to wrap (single logical line - no explicit newlines inside). + /// The maximum available width (in control-space) that a single visual line can use. + /// The wrapping mode - either whole-word (breaks only at whitespace) or per-character (can break in the middle of a word). + private void AddWrappedTextBlocks(ref ParsingContext context, ref TextBlock textBlock, Font font, TextRange range, float wrapWidth, TextWrapping wrapping) + { + int segmentStart = range.StartIndex; + float segmentWidth = 0.0f; + int pos = range.StartIndex; + while (pos < range.EndIndex) + { + // Consume the next "chunk" - a single character for WrapChars, or a whole word (plus trailing whitespace) for WrapWords + int chunkStart = pos; + if (wrapping == TextWrapping.WrapChars) + { + pos++; + } + else + { + while (pos < range.EndIndex && !char.IsWhiteSpace(_text[pos])) + pos++; + while (pos < range.EndIndex && char.IsWhiteSpace(_text[pos])) + pos++; + } + + var chunkRange = new TextRange { StartIndex = chunkStart, EndIndex = pos }; + var chunkWidth = font.MeasureText(_text, ref chunkRange).X; + + if ((segmentWidth > 0.0f || context.Caret.X > 0.0f) && context.Caret.X + segmentWidth + chunkWidth > wrapWidth) + { + // The next chunk no longer fits - emit the accumulated segment (if any) and start a new line + if (segmentStart < chunkStart) + AddWrappedTextBlock(ref context, ref textBlock, font, segmentStart, chunkStart); + context.Caret.X = 0; + OnLineAdded(ref context, chunkStart - 1); + segmentStart = chunkStart; + segmentWidth = 0.0f; + } + segmentWidth += chunkWidth; + + // For Wrap Words mode: A single word wider than the whole available width can't be split further, so force it onto its own line + if (wrapping == TextWrapping.WrapWords && context.Caret.X <= 0.0f && segmentStart == chunkStart && segmentWidth > wrapWidth && pos < range.EndIndex) + { + AddWrappedTextBlock(ref context, ref textBlock, font, segmentStart, pos); + context.Caret.X = 0; + OnLineAdded(ref context, pos - 1); + segmentStart = pos; + segmentWidth = 0.0f; + } + } + if (segmentStart < range.EndIndex) + AddWrappedTextBlock(ref context, ref textBlock, font, segmentStart, range.EndIndex); + } + + /// + /// Adds a single text block to the control, using the current caret position as the origin and moving the caret forward by the width of the text block. + /// + /// The parsing context. + /// The text block to add. + /// The font to use for measurement. + /// The start index of the text range. + /// The end index of the text range. + private void AddWrappedTextBlock(ref ParsingContext context, ref TextBlock textBlock, Font font, int start, int end) + { + var range = new TextRange { StartIndex = start, EndIndex = end }; + var size = font.MeasureText(_text, ref range); + textBlock.Range = range; + textBlock.Bounds = new Rectangle(context.Caret, size); + context.AddTextBlock(ref textBlock); + context.Caret.X += size.X; + } + + /// + /// Called when a new line is added (eg. after a newline character or when the text overflows the available width). + /// It organizes the text blocks within the line and moves the caret to the next line. + /// + /// The parsing context. + /// The index of the last character in the line. private void OnLineAdded(ref ParsingContext context, int lineEnd) { // Calculate size of the line @@ -333,7 +419,7 @@ namespace FlaxEngine.GUI // Move to the next line context.LineStartCharacterIndex = lineEnd + 1; context.LineStartTextBlockIndex = _textBlocks.Count; - context.Caret.Y += lineSize.Y; + context.Caret.Y += lineSize.Y * _baseLinesGapScale; } } } diff --git a/Source/Engine/UI/GUI/Common/RichTextBox.cs b/Source/Engine/UI/GUI/Common/RichTextBox.cs index f7726bf56..314af4ed8 100644 --- a/Source/Engine/UI/GUI/Common/RichTextBox.cs +++ b/Source/Engine/UI/GUI/Common/RichTextBox.cs @@ -1,6 +1,7 @@ // Copyright (c) Wojciech Figat. All rights reserved. using System.Collections.Generic; +using System.ComponentModel; namespace FlaxEngine.GUI { @@ -11,6 +12,8 @@ namespace FlaxEngine.GUI public partial class RichTextBox : RichTextBoxBase { private TextBlockStyle _textStyle; + private TextWrapping _wrapping = TextWrapping.NoWrap; + private float _baseLinesGapScale = 1.0f; /// /// The default text style applied to the whole text. @@ -26,16 +29,50 @@ namespace FlaxEngine.GUI } } + /// + /// Gets or sets the text wrapping within the control bounds. + /// + [DefaultValue(TextWrapping.NoWrap)] + [EditorOrder(30), Tooltip("The text wrapping within the control bounds.")] + public TextWrapping Wrapping + { + get => _wrapping; + set + { + if (_wrapping == value) + return; + _wrapping = value; + UpdateTextBlocks(); + } + } + + /// + /// Gets or sets the gap between lines when wrapping and more than a single line is displayed. + /// + [DefaultValue(1.0f)] + [EditorOrder(40), Tooltip("The gap between lines when wrapping and more than a single line is displayed."), Limit(0f, 100.0f, 0.01f)] + public float BaseLinesGapScale + { + get => _baseLinesGapScale; + set + { + if (Mathf.NearEqual(_baseLinesGapScale, value)) + return; + _baseLinesGapScale = value; + UpdateTextBlocks(); + } + } + /// /// The collection of custom text styles to apply (named). /// - [EditorOrder(30)] + [EditorOrder(50)] public Dictionary Styles = new Dictionary(); /// /// The collection of custom images/sprites that can be inlined in text (named). /// - [EditorOrder(40)] + [EditorOrder(60)] public Dictionary Images = new Dictionary(); /// diff --git a/Source/Engine/UI/GUI/Common/RichTextBoxBase.cs b/Source/Engine/UI/GUI/Common/RichTextBoxBase.cs index a08c2f31d..857e1dccf 100644 --- a/Source/Engine/UI/GUI/Common/RichTextBoxBase.cs +++ b/Source/Engine/UI/GUI/Common/RichTextBoxBase.cs @@ -29,6 +29,11 @@ namespace FlaxEngine.GUI [HideInEditor] public ParseTextBlocksDelegate ParseTextBlocks; + /// + /// Gets the list of parsed text blocks. + /// + public List TextBlocks => _textBlocks; + /// /// Initializes a new instance of the class. /// @@ -95,15 +100,19 @@ namespace FlaxEngine.GUI } // Handle case when index is outside all text ranges - if (index >= 0 && blockCount > 0 && index <= textBlocksSpan[0].Range.StartIndex) + if (index >= 0 && blockCount > 0) { - result = textBlocksSpan[0]; - return true; - } - if (index >= 0 && blockCount > 0 && index >= textBlocksSpan[blockCount - 1].Range.StartIndex) - { - result = textBlocksSpan[blockCount - 1]; - return true; + if (index <= textBlocksSpan[0].Range.StartIndex) + { + result = textBlocksSpan[0]; + return true; + } + + if (index >= textBlocksSpan[blockCount - 1].Range.StartIndex) + { + result = textBlocksSpan[blockCount - 1]; + return true; + } } // If no text block is found diff --git a/Source/Engine/UI/GUI/Panels/ScrollBar.cs b/Source/Engine/UI/GUI/Panels/ScrollBar.cs index 756c698c5..0cf3bda95 100644 --- a/Source/Engine/UI/GUI/Panels/ScrollBar.cs +++ b/Source/Engine/UI/GUI/Panels/ScrollBar.cs @@ -463,6 +463,8 @@ namespace FlaxEngine.GUI // Click change Value = _value + (mousePosition < _thumbCenter ? -1 : 1) * _clickChange; } + + return true; } return base.OnMouseDown(location, button); diff --git a/Source/Engine/UI/TextRender.cpp b/Source/Engine/UI/TextRender.cpp index 123f382b3..80f73ee08 100644 --- a/Source/Engine/UI/TextRender.cpp +++ b/Source/Engine/UI/TextRender.cpp @@ -175,7 +175,7 @@ void TextRender::UpdateLayout() // Pick a font (remove DPI text scale as the text is being placed in the world) auto font = Font->CreateFont(_size); - float scale = _layoutOptions.Scale / FontManager::FontScale; + const float scale = font->GetScale(_layoutOptions.Scale); // Prepare FontTextureAtlas* fontAtlas = nullptr; @@ -220,6 +220,7 @@ void TextRender::UpdateLayout() if (c != '\n') { font->GetCharacter(c, entry); + const float entryScale = entry.Font->GetScale(_layoutOptions.Scale); // Check if need to select/change font atlas (since characters even in the same font may be located in different atlases) if (fontAtlas == nullptr || entry.TextureIndex != drawChunk.FontAtlasIndex) @@ -276,17 +277,17 @@ void TextRender::UpdateLayout() { kerning = 0; } - pointer.X += (float)kerning * scale; + pointer.X += (float)kerning * entryScale; previous = entry; // Omit whitespace characters if (!isWhitespace) { // Calculate character size and atlas coordinates - const float x = pointer.X + (float)entry.OffsetX * scale; - const float y = pointer.Y + (float)(font->GetHeight() + font->GetDescender() - entry.OffsetY) * scale; + const float x = pointer.X + (float)entry.OffsetX * entryScale; + const float y = pointer.Y - (float)entry.OffsetY * entryScale + (float)(font->GetHeight() + font->GetDescender()) * scale; - Rectangle charRect(x, y, entry.UVSize.X * scale, entry.UVSize.Y * scale); + Rectangle charRect(x, y, entry.UVSize.X * entryScale, entry.UVSize.Y * entryScale); charRect.Offset(_layoutOptions.Bounds.Location); Float2 upperLeftUV = entry.UV * invAtlasSize; @@ -329,7 +330,7 @@ void TextRender::UpdateLayout() } // Move - pointer.X += (float)entry.AdvanceX * scale; + pointer.X += (float)entry.AdvanceX * entryScale; } } } diff --git a/Source/ThirdParty/detex/decompress-bptc.cpp b/Source/ThirdParty/detex/decompress-bptc.cpp index 618220561..aac47c56e 100644 --- a/Source/ThirdParty/detex/decompress-bptc.cpp +++ b/Source/ThirdParty/detex/decompress-bptc.cpp @@ -141,9 +141,9 @@ int mode, detexBlock128 * DETEX_RESTRICT block) { // 64-bit word boundary in any mode. uint32_t bits; if (block->index < 64) - bits = block->data0 >> block->index; + bits = (uint32_t)(block->data0 >> block->index); else - bits = block->data1 >> (block->index - 64); + bits = (uint32_t)(block->data1 >> (block->index - 64)); for (int i = 0; i < nu_subsets * 2; i++) { endpoint_array[i * 4 + 0] <<= 1; endpoint_array[i * 4 + 1] <<= 1; diff --git a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs index 3ba5b3d8f..57411d7a2 100644 --- a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs +++ b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs @@ -671,7 +671,11 @@ namespace Flax.Build.Bindings else if (nativeType == "object[]") parameterMarshalType = "MarshalUsing(typeof(FlaxEngine.Interop.SystemObjectArrayMarshaller))"; else if (parameterInfo.Type.Type == "Array" && parameterInfo.Type.GenericArgs.Count > 0 && parameterInfo.Type.GenericArgs[0].Type == "bool") + { parameterMarshalType = $"MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.U1, SizeParamIndex = {(!functionInfo.IsStatic ? 1 : 0) + functionInfo.Parameters.Count + (functionInfo.Glue.CustomParameters.FindIndex(x => x.Name == $"__{parameterInfo.Name}Count"))})"; + if (!parameterInfo.IsOut && !parameterInfo.IsRef) + parameterMarshalType += ", In"; // The usage of 'LibraryImportAttribute' does not follow recommendations. It is recommended to use explicit '[In]' and '[Out]' attributes on array parameters. + } else if (parameterInfo.Type.Type == "Array" || parameterInfo.Type.Type == "Span" || parameterInfo.Type.Type == "DataContainer" || parameterInfo.Type.Type == "BytesContainer" || nativeType == "Array") { parameterMarshalType = $"MarshalUsing(typeof(FlaxEngine.Interop.ArrayMarshaller<,>), CountElementName = \"__{parameterInfo.Name}Count\")";