Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Workspace Testing rework #809

Merged
merged 10 commits into from
Dec 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 72 additions & 51 deletions src/NexusMods.App.UI/WorkspaceSystem/GridUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,46 @@ namespace NexusMods.App.UI.WorkspaceSystem;

internal static class GridUtils
{
/// <summary>
/// Checks whether the given state is a perfect grid.
/// </summary>
/// <remarks>
/// A perfect has no gaps, and no panel is out-of-bounds.
/// </remarks>
/// <exception cref="Exception">Thrown when the grid is not perfect.</exception>
internal static bool IsPerfectGrid(WorkspaceGridState gridState)
{
var totalArea = 0.0;

foreach (var panelState in gridState)
{
var (id, rect) = panelState;
if (rect.Left < 0.0 || rect.Right > 1.0 || rect.Top < 0.0 || rect.Bottom > 1.0)
{
throw new Exception($"Panel {panelState} is out of bounds");
}

totalArea += rect.Height * rect.Width;

foreach (var other in gridState)
{
if (id == other.Id) continue;

if (rect.Intersects(other.Rect))
{
throw new Exception($"{panelState.ToString()} intersects with {other.ToString()}");
}
}
}

if (!totalArea.IsCloseTo(1.0))
{
throw new Exception($"Area of {totalArea} doesn't match 1.0");
}

return true;
}

/// <summary>
/// Returns all possible new states.
/// </summary>
Expand Down Expand Up @@ -146,60 +186,39 @@ private static bool CanAddRow(
return currentRows < maxRows;
}

internal static IReadOnlyDictionary<PanelId, Rect> GetStateWithoutPanel(
ImmutableDictionary<PanelId, Rect> currentState,
PanelId panelToRemove,
bool isHorizontal = true)
internal static WorkspaceGridState GetStateWithoutPanel(
WorkspaceGridState gridState,
PanelId panelToRemove)
{
if (currentState.Count == 1) return ImmutableDictionary<PanelId, Rect>.Empty;
if (gridState.Count == 1) return WorkspaceGridState.Empty(gridState.IsHorizontal);

var res = currentState.Remove(panelToRemove);
if (res.Count == 1)
{
return new Dictionary<PanelId, Rect>
{
{ res.First().Key, MathUtils.One }
};
}
var res = gridState.Remove(gridState[panelToRemove]);
if (res.Count == 1) return WorkspaceGridState.Empty(gridState.IsHorizontal).Add(new PanelGridState(res[0].Id, MathUtils.One));

var currentRect = currentState[panelToRemove];
var panelState = gridState[panelToRemove];
var currentRect = panelState.Rect;

Span<PanelId> sameColumn = stackalloc PanelId[currentState.Count];
Span<PanelId> sameColumn = stackalloc PanelId[gridState.Count];
var sameColumnCount = 0;

Span<PanelId> sameRow = stackalloc PanelId[currentState.Count];
Span<PanelId> sameRow = stackalloc PanelId[gridState.Count];
var sameRowCount = 0;

foreach (var kv in res)
using (var enumerator = res.EnumerateAdjacentPanels(panelState, includeAnchor: true))
{
var (id, rect) = kv;

// same column
// | a | x | | b | x |
// | b | x | | a | x |
if (rect.Left.IsGreaterThanOrCloseTo(currentRect.Left) && rect.Right.IsLessThanOrCloseTo(currentRect.Right))
{
if (rect.Top.IsCloseTo(currentRect.Bottom) || rect.Bottom.IsCloseTo(currentRect.Top))
{
sameColumn[sameColumnCount++] = id;
}
}

// same row
// | a | b | | b | a | | a | b |
// | x | x | | x | x | | a | c |
if (rect.Top.IsGreaterThanOrCloseTo(currentRect.Top) && rect.Bottom.IsLessThanOrCloseTo(currentRect.Bottom))
while (enumerator.MoveNext())
{
if (rect.Left.IsCloseTo(currentRect.Right) || rect.Right.IsCloseTo(currentRect.Left))
{
sameRow[sameRowCount++] = id;
}
var adjacent = enumerator.Current;
if ((adjacent.Kind & WorkspaceGridState.AdjacencyKind.SameColumn) == WorkspaceGridState.AdjacencyKind.SameColumn)
sameColumn[sameColumnCount++] = adjacent.Panel.Id;
if ((adjacent.Kind & WorkspaceGridState.AdjacencyKind.SameRow) == WorkspaceGridState.AdjacencyKind.SameRow)
sameRow[sameRowCount++] = adjacent.Panel.Id;
}
}

Debug.Assert(sameColumnCount > 0 || sameRowCount > 0);

if (isHorizontal)
if (gridState.IsHorizontal)
{
// prefer columns over rows when horizontal
if (sameColumnCount > 0)
Expand All @@ -225,54 +244,56 @@ internal static IReadOnlyDictionary<PanelId, Rect> GetStateWithoutPanel(
return res;
}

private static ImmutableDictionary<PanelId, Rect> JoinSameColumn(
ImmutableDictionary<PanelId, Rect> res,
private static WorkspaceGridState JoinSameColumn(
WorkspaceGridState res,
Rect currentRect,
Span<PanelId> sameColumn,
int sameColumnCount)
{
var updates = GC.AllocateUninitializedArray<KeyValuePair<PanelId, Rect>>(sameColumnCount);
var updates = GC.AllocateUninitializedArray<PanelGridState>(sameColumnCount);

for (var i = 0; i < sameColumnCount; i++)
{
var id = sameColumn[i];
var rect = res[id];
var panel = res[id];
var rect = panel.Rect;

var x = rect.X;
var width = rect.Width;

var y = Math.Min(rect.Y, currentRect.Y);
var height = rect.Height + currentRect.Height;

updates[i] = new KeyValuePair<PanelId, Rect>(id, new Rect(x, y, width, height));
updates[i] = new PanelGridState(id, new Rect(x, y, width, height));
}

return res.SetItems(updates);
return res.UnionById(updates);
}

private static ImmutableDictionary<PanelId, Rect> JoinSameRow(
ImmutableDictionary<PanelId, Rect> res,
private static WorkspaceGridState JoinSameRow(
WorkspaceGridState res,
Rect currentRect,
Span<PanelId> sameRow,
int sameRowCount)
{
var updates = GC.AllocateUninitializedArray<KeyValuePair<PanelId, Rect>>(sameRowCount);
var updates = GC.AllocateUninitializedArray<PanelGridState>(sameRowCount);

for (var i = 0; i < sameRowCount; i++)
{
var id = sameRow[i];
var rect = res[id];
var panel = res[id];
var rect = panel.Rect;

var y = rect.Y;
var height = rect.Height;

var x = Math.Min(rect.X, currentRect.X);
var width = rect.Width + currentRect.Width;

updates[i] = new KeyValuePair<PanelId, Rect>(id, new Rect(x, y, width, height));
updates[i] = new PanelGridState(id, new Rect(x, y, width, height));
}

return res.SetItems(updates);
return res.UnionById(updates);
}

internal static IReadOnlyList<ResizerInfo> GetResizers(
Expand Down
24 changes: 24 additions & 0 deletions src/NexusMods.App.UI/WorkspaceSystem/PanelGridState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using Avalonia;

namespace NexusMods.App.UI.WorkspaceSystem;

public record struct PanelGridState(PanelId Id, Rect Rect);

public class PanelGridStateComparer : IComparer<PanelGridState>
{
public static readonly IComparer<PanelGridState> Instance = new PanelGridStateComparer();

public int Compare(PanelGridState x, PanelGridState y)
{
var a = x.Rect;
var b = y.Rect;

var xComparison = a.X.CompareTo(b.X);
if (xComparison != 0) return xComparison;

var yComparison = a.Y.CompareTo(b.Y);
if (yComparison != 0) return yComparison;

return x.Id.CompareTo(y.Id);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -294,16 +294,16 @@ public IPanelViewModel AddPanel(IReadOnlyDictionary<PanelId, Rect> state)

public void ClosePanel(PanelId panelToClose)
{
var currentState = _panels.ToImmutableDictionary(panel => panel.Id, panel => panel.LogicalBounds);
var newState = GridUtils.GetStateWithoutPanel(currentState, panelToClose, isHorizontal: IsHorizontal);
var currentState = WorkspaceGridState.From(_panels, IsHorizontal);
var newState = GridUtils.GetStateWithoutPanel(currentState, panelToClose);

_panelSource.Edit(updater =>
{
updater.Remove(panelToClose);

foreach (var kv in newState)
foreach (var panelState in newState)
{
var (panelId, logicalBounds) = kv;
var (panelId, logicalBounds) = panelState;
{
var existingPanel = updater.Lookup(panelId);
Debug.Assert(existingPanel.HasValue);
Expand Down
Loading
Loading