diff --git a/GlitchyEditor/src/EditWindows/ContentBrowserWindow.bf b/GlitchyEditor/src/EditWindows/ContentBrowserWindow.bf index 5eb3192..ceaae3a 100644 --- a/GlitchyEditor/src/EditWindows/ContentBrowserWindow.bf +++ b/GlitchyEditor/src/EditWindows/ContentBrowserWindow.bf @@ -59,9 +59,77 @@ namespace GlitchyEditor.EditWindows public const String s_WindowTitle = "Content Browser"; - private append String _currentDirectory = .(); + class History + { + AssetHierarchy _hierarchy; - //private append String _selectedFile = .(); + public AssetHierarchy AssetHierarchy + { + get => _hierarchy; + set => _hierarchy = value; + } + + private append List _history = .() ~ ClearAndDeleteItems!(_); + private int _currentIndex = -1; + + public StringView CurrentDirectoryPath => _currentIndex >= 0 ? _history[_currentIndex] : ""; + + public void Navigate(StringView nextPath) + { + _currentIndex++; + _history.Insert(_currentIndex, new String(nextPath)); + + TrimHistory(); + } + + private void TrimHistory() + { + while (_history.Count > (_currentIndex + 1)) + { + delete _history.PopBack(); + } + } + + public void Replace(StringView nextPath) + { + if (_currentIndex == -1) + { + _currentIndex = 0; + _history.AddFront(new String(nextPath)); + } + else + { + _history[_currentIndex].Set(nextPath); + } + + TrimHistory(); + } + + public bool Back() + { + if (_currentIndex > 0) + { + _currentIndex--; + return true; + } + return false; + } + + public bool Forward() + { + if ((_currentIndex + 1) < _history.Count) + { + _currentIndex++; + return true; + } + + return false; + } + } + + private append History _directoryHistory = .(); + + public StringView CurrentDirectory => _directoryHistory.CurrentDirectoryPath; private append List _selectedFiles = .() ~ ClearAndDeleteItems!(_); @@ -136,21 +204,48 @@ namespace GlitchyEditor.EditWindows protected override void InternalShow() { _manager.Update(); + + // Show History + if (ImGui.Begin("Debug History")) + { + for (String path in _directoryHistory.[Friend]_history) + { + if (_directoryHistory.CurrentDirectoryPath === StringView(path)) + { + ImGui.Text("==>"); + ImGui.SameLine(); + } + ImGui.Text(path); + } + + //if (!_selectionRectStart.X.IsNaN) + { + ImGui.TextUnformatted(_selectionRect.ToString(.. scope .())); + } + + ImGui.End(); + } // Make sure we are in an existing directory. - if (!_manager.AssetHierarchy.FileExists(_currentDirectory)) + if (!_manager.AssetHierarchy.FileExists(CurrentDirectory)) { - _currentDirectory.Set(_manager.AssetDirectory); + _directoryHistory.Replace(_manager.AssetDirectory); } + + let originalWindowPadding = ImGui.GetStyle().WindowPadding; + ImGui.PushStyleVar(.WindowPadding, float2(0,0)); + defer ImGui.PopStyleVar(); + + if(!ImGui.Begin(s_WindowTitle, &_open, .None)) + { + ImGui.End(); + return; + } + + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + originalWindowPadding.y); // Update selection modifies { - if(!ImGui.Begin(s_WindowTitle, &_open, .None)) - { - ImGui.End(); - return; - } - _addToSelection = Input.IsKeyPressed(.Control); if (Input.IsKeyPressed(.Shift)) @@ -163,7 +258,7 @@ namespace GlitchyEditor.EditWindows } } - // Remove paths that no longer exist. + // Remove paths from selection that no longer exist. for (String path in _selectedFiles) { Result> node = _manager.AssetHierarchy.GetNodeFromPath(path); @@ -223,9 +318,12 @@ namespace GlitchyEditor.EditWindows if (ImGui.BeginChild("Files")) { - TreeNode currentDirectoryNode = TrySilent!(_manager.AssetHierarchy.GetNodeFromPath(_currentDirectory)); + Result> currentDirectoryNode = _manager.AssetHierarchy.GetNodeFromPath(_directoryHistory.CurrentDirectoryPath); - DrawCurrentDirectory(currentDirectoryNode); + if (currentDirectoryNode case .Ok(TreeNode currentDirectory)) + { + DrawCurrentDirectory(currentDirectoryNode); + } // Context menu when clicking on the background. if (ImGui.BeginPopupContextWindow()) @@ -235,8 +333,16 @@ namespace GlitchyEditor.EditWindows } ImGui.EndChild(); - - FileDropTarget(currentDirectoryNode, .External); + + if (ImGui.IsItemClicked(.Left | .Right)) + { + _selectedFiles.ClearAndDeleteItems(); + } + + if (currentDirectoryNode case .Ok(TreeNode currentDirectory)) + { + FileDropTarget(currentDirectory, .External); + } } ImGui.EndTable(); @@ -258,14 +364,14 @@ namespace GlitchyEditor.EditWindows Range } - private void SelectFile(StringView fullFilePath, bool clearOldSelection, SelectionMode mode) + private void SelectFile(StringView fullFilePath, bool clearOldSelection, SelectionMode mode, bool toggle = true) { if (clearOldSelection) { _selectedFiles.ClearAndDeleteItems(); } - if (mode == .SingleFile) + if (mode == .SingleFile && toggle) { if (IsFileSelected(fullFilePath)) { @@ -277,6 +383,13 @@ namespace GlitchyEditor.EditWindows _selectedFiles.Add(new String(fullFilePath)); } } + else if (mode == .SingleFile && !toggle) + { + if (!IsFileSelected(fullFilePath)) + { + _selectedFiles.Add(new String(fullFilePath)); + } + } else if (mode == .Range) { if (IsFileSelected(fullFilePath)) @@ -295,6 +408,12 @@ namespace GlitchyEditor.EditWindows OnFileSelected(this, fullFilePath); } + private void DeselectFile(StringView fullFilePath) + { + String oldPath = TrySilent!(_selectedFiles.GetAndRemoveAlt(fullFilePath)); + delete oldPath; + } + private void DrawSearchBar() { ImGui.TextUnformatted("Search:"); @@ -309,7 +428,7 @@ namespace GlitchyEditor.EditWindows { if (ImGui.MenuItem("Open in file browser...")) { - if (Path.OpenFolder(_currentDirectory) case .Err) + if (Path.OpenFolder(CurrentDirectory) case .Err) Log.EngineLogger.Error("Failed to open directory in file browser."); } @@ -319,14 +438,14 @@ namespace GlitchyEditor.EditWindows { if (ImGui.MenuItem("Full path")) { - ImGui.SetClipboardText(_currentDirectory); + ImGui.SetClipboardText(CurrentDirectory.ToScopeCStr!()); } ImGui.AttachTooltip("Copies the full file path of the current folder."); if (ImGui.MenuItem("Asset identifier")) { - Result> assetNode = _manager.AssetHierarchy.GetNodeFromPath(_currentDirectory); + Result> assetNode = _manager.AssetHierarchy.GetNodeFromPath(CurrentDirectory); if (assetNode case .Ok(let treeNode)) { @@ -404,14 +523,14 @@ namespace GlitchyEditor.EditWindows /// Creates a new asset with the given creator private void CreateAsset() { - if (!Directory.Exists(_currentDirectory)) + if (!Directory.Exists(CurrentDirectory)) { - Log.EngineLogger.Error($"Directory {_currentDirectory} doesn't exist."); + Log.EngineLogger.Error($"Directory {CurrentDirectory} doesn't exist."); return; } String currentFile = scope String(); - Path.Combine(currentFile, _currentDirectory, StringView(&_newFileName)); + Path.Combine(currentFile, CurrentDirectory, StringView(&_newFileName)); // Add file extension, if necessary if (!currentFile.EndsWith(_newFileCreator.FileExtension)) @@ -459,7 +578,7 @@ namespace GlitchyEditor.EditWindows if(tree.Children.Where(scope (node) => node->IsDirectory).Count() == 0) flags |= .Leaf; - if (tree->Path == _currentDirectory) + if (tree->Path == CurrentDirectory) flags |= .Selected; // TODO: this kinda works, but the user should be able to close the directory @@ -475,7 +594,7 @@ namespace GlitchyEditor.EditWindows if (!ImGui.IsItemToggledOpen() && ImGui.IsItemClicked(.Left)) { - _currentDirectory.Set(tree->Path); + _directoryHistory.Navigate(tree->Path); } if(isOpen) @@ -493,21 +612,40 @@ namespace GlitchyEditor.EditWindows const float2 padding = .(24, 24); + //private Rectangle _selectionRect = .(float.NaN, -1); + private float2 _selectionRectStart = float.NaN; + private float2 _selectionRectEnd = float.NaN; + private float4 _selectionRect; + /// Renders the contents of _currentDirectory. Returns the node of the current directory, or null if the browser isn't in a directory. private void DrawCurrentDirectory(TreeNode currentDirectoryNode) { var currentDirectoryNode; - if (Input.IsKeyPressed(.Alt) && Input.IsKeyPressing(.Up)) + if (Input.IsKeyPressed(.Alt)) { - if (currentDirectoryNode.Parent != _manager.AssetHierarchy.RootNode) + if (Input.IsKeyPressing(.Up)) { - _currentDirectory.Set(currentDirectoryNode.Parent->Path); + if (currentDirectoryNode.Parent != _manager.AssetHierarchy.RootNode) + { + _directoryHistory.Navigate(currentDirectoryNode.Parent->Path); + + _selectedFiles.ClearAndDeleteItems(); + currentDirectoryNode = currentDirectoryNode.Parent; + } + } - _selectedFiles.ClearAndDeleteItems(); - currentDirectoryNode = currentDirectoryNode.Parent; + if (Input.IsKeyPressing(.Left)) + { + _directoryHistory.Back(); + } + if (Input .IsKeyPressing(.Right)) + { + _directoryHistory.Forward(); } } + ImGui.BeginRectangleSelection(ref _selectionRect, ImGui.IsMouseDown(.Left) || ImGui.IsMouseDown(.Right)); + List> directoryEntries = null; ImGui.Style* style = ImGui.GetStyle(); @@ -661,7 +799,7 @@ namespace GlitchyEditor.EditWindows { EditorLayer.SetDropEffect(.Copy); - if (dropTarget->Path == _currentDirectory) + if (dropTarget->Path == CurrentDirectory) { ImGui.SetTooltip($"Copy here ({dropTarget->Name})"); } @@ -712,6 +850,18 @@ namespace GlitchyEditor.EditWindows ImGui.ImageButton("FileImage", image, (.)IconSize); + if (ImGui.IsRectangleSelecting(ref _selectionRect)) + { + if (ImGui.IsInRectangleSelection(ref _selectionRect)) + { + SelectFile(entry->Path, false, .SingleFile, false); + } + else + { + DeselectFile(entry->Path); + } + } + ImGui.PopStyleColor(); if (ImGui.BeginDragDropSource()) @@ -816,7 +966,7 @@ namespace GlitchyEditor.EditWindows ImGui.EndPopup(); } - + ImGui.EndChild(); ImGui.AttachTooltip(entry->Name); @@ -1027,7 +1177,7 @@ namespace GlitchyEditor.EditWindows { if (entry->IsDirectory) { - _currentDirectory.Set(entry->Path); + _directoryHistory.Navigate(entry->Path); } else { diff --git a/GlitchyEngine/src/ImGui/ImGuiExtension.bf b/GlitchyEngine/src/ImGui/ImGuiExtension.bf index 472b9d8..1bf9df3 100644 --- a/GlitchyEngine/src/ImGui/ImGuiExtension.bf +++ b/GlitchyEngine/src/ImGui/ImGuiExtension.bf @@ -38,6 +38,95 @@ namespace ImGui public static explicit operator float4(Vec4 v) => .(v.x, v.y, v.z, v.w); public static explicit operator Vec4(float4 v) => .(v.X, v.Y, v.Z, v.W); } + + extension Color + { + public static explicit operator ColorRGBA(Color c) => .(c.Value.x, c.Value.y, c.Value.z, c.Value.w); + public static explicit operator Color(ColorRGBA c) => .(c.R, c.G, c.B, c.A); + } + + enum RectangleSelectionFlags + { + None, + NoRender + } + + public static bool BeginRectangleSelection(ref float4 selectionRectangle, bool isMouseDown, RectangleSelectionFlags flags = .None) + { + /*let storage = ImGui.GetStateStorage(); + ref float selectionRectX = ref *storage.GetFloatRef(ImGui.GetID("SelectionRect.X"), float.NaN);*/ + + float2 minRegion = (.)ImGui.GetCursorPos() + (.)ImGui.GetWindowPos(); + float2 maxRegion = minRegion + (float2)ImGui.GetContentRegionAvail(); + + ImGui.DrawRect((.)minRegion, (.)maxRegion, ImGui.Color(0,1f,0)); + + bool selectionValid() => !any(isnan(selectionRectangle)); + + if (isMouseDown) + { + if (!selectionValid() && IsWindowHovered()) + { + selectionRectangle.XY = (float2)ImGui.GetMousePos(); + } + + selectionRectangle.ZW = (.)ImGui.GetMousePos(); + } + else + { + selectionRectangle.XYZW = float.NaN; + return false; + } + + selectionRectangle.XY = clamp(selectionRectangle.XY, minRegion, maxRegion); + selectionRectangle.ZW = clamp(selectionRectangle.ZW, minRegion, maxRegion); + + if (!flags.HasFlag(.NoRender) && selectionValid()) + { + let drawList = ImGui.GetForegroundDrawList(); + drawList.AddRect((.)selectionRectangle.XY, (.)selectionRectangle.ZW, ImGui.GetColorU32(ImGui.Color(0, 130, 216, 255).Value)); + drawList.AddRectFilled((.)selectionRectangle.XY, (.)selectionRectangle.ZW, ImGui.GetColorU32(ImGui.Color(0, 130, 216, 50).Value)); + } + + return true; + } + + /// Returns true if the user is currently doing a rectangle selection + public static bool IsRectangleSelecting(ref float4 selectionRectangle) + { + return !any(isnan(selectionRectangle)); + } + + /// Returns true if the current item is intersecting the selection rectangle + public static bool IsInRectangleSelection(ref float4 selectionRectangle) + { + if (any(isnan(selectionRectangle))) + { + return false; + } + + ImGui.DebugDrawItemRect(); + + float2 minRect = (.)ImGui.GetItemRectMin(); + float2 maxRect = (.)ImGui.GetItemRectMax(); + + float2 topLeft = min(minRect, maxRect); + float2 bottomRight = max(minRect, maxRect); + + Rectangle itemRectangle = .(topLeft, 0); + itemRectangle.BottomRight = bottomRight; + + let drawList = ImGui.GetForegroundDrawList(); + drawList.AddRect((.)itemRectangle.TopLeft, (.)itemRectangle.BottomRight, ImGui.GetColorU32(ImGui.Color(0, 130, 216, 255).Value)); + + float2 selectionTopLeft = min(selectionRectangle.XY, selectionRectangle.ZW); + float2 selectionTottomRight = max(selectionRectangle.XY, selectionRectangle.ZW); + + Rectangle rect = .(selectionTopLeft, 0); + rect.BottomRight = selectionTottomRight; + + return rect.Intersects(itemRectangle); + } public static Payload? AcceptDragDropPayload(char8* type, DragDropFlags flags = .None) where T : struct { diff --git a/GlitchyEngine/src/Math/Rectangle.bf b/GlitchyEngine/src/Math/Rectangle.bf new file mode 100644 index 0000000..0a0a6ef --- /dev/null +++ b/GlitchyEngine/src/Math/Rectangle.bf @@ -0,0 +1,110 @@ +using Bon; +using System; + +namespace GlitchyEngine.Math; + +enum IntersectionMode +{ + Disjoint, + Intersects, + Contains +} + +[BonTarget, CRepr] +struct Rectangle +{ + private float2 _topLeft; + private float2 _size; + + public float2 TopLeft + { + get => _topLeft; + set mut => _topLeft = value; + } + + public float2 Size + { + get => _size; + set mut => _size = value; + } + + public float2 BottomRight + { + get => _topLeft + _size; + set mut + { + _size = value - _topLeft; + + if (_size.X < 0) + { + _size.X = -_size.X; + _topLeft.X = value.X; + } + + if (_size.Y < 0) + { + _size.Y = -_size.Y; + _topLeft.Y = value.Y; + } + } + } + + public float Left + { + get => _topLeft.X; + set mut => _topLeft.X = value; + } + + public float Top + { + get => _topLeft.Y; + set mut => _topLeft.Y = value; + } + + public float Right + { + get => _topLeft.X + _size.X; + set mut => _size.X = value - _topLeft.X; + } + + public float Bottom + { + get => _topLeft.Y + _size.Y; + set mut => _size.Y = value - _topLeft.Y; + } + + public this(float2 topLeft, float2 size) + { + _topLeft = topLeft; + _size = size; + } + + public bool Contains(float2 point) + { + return all(_topLeft <= point) && all(BottomRight >= point); + } + + public IntersectionMode Collision(Rectangle other) + { + if (any(other.TopLeft >= this.BottomRight) || any(other.BottomRight <= this.TopLeft)) + { + return .Disjoint; + } + + if (all(other.TopLeft >= this.TopLeft) && all(other.BottomRight <= this.BottomRight)) + { + return .Contains; + } + + return .Intersects; + } + + public bool Intersects(Rectangle other) + { + return !(other.Left > Right + || other.Top > Bottom + || other.Right < Left + || other.Bottom < Top + ); + } +} \ No newline at end of file