Skip to content
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
45 changes: 26 additions & 19 deletions .config/dotnet-tools.json
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
{
"version": 1,
"isRoot": true,
"tools": {
"tcli": {
"version": "0.2.4",
"commands": [
"tcli"
],
"rollForward": false
},
"husky": {
"version": "0.8.0",
"commands": [
"husky"
],
"rollForward": false
}
}
{
"version": 1,
"isRoot": true,
"tools": {
"tcli": {
"version": "0.2.4",
"commands": [
"tcli"
],
"rollForward": false
},
"husky": {
"version": "0.8.0",
"commands": [
"husky"
],
"rollForward": false
},
"csharpier": {
"version": "1.2.6",
"commands": [
"csharpier"
],
"rollForward": false
}
}
}
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@
It should follow the format major.minor.patch (semantic versioning). If you publish your mod
as a library to NuGet, this version will also be used as the package version.
-->
<Version>0.2.0</Version>
<Version>0.3.0</Version>
</PropertyGroup>
</Project>
27 changes: 13 additions & 14 deletions Elements/AbstractGroup.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Silksong.ModMenu.Internal;
Expand All @@ -13,7 +12,7 @@ namespace Silksong.ModMenu.Elements;
/// </summary>
public abstract class AbstractGroup : INavigableMenuEntity
{
private readonly VisibilityManager visibility = new();
private readonly VisibilityManager visibility = new(false);

/// <inheritdoc/>
public VisibilityManager Visibility => visibility;
Expand All @@ -27,14 +26,9 @@ public abstract class AbstractGroup : INavigableMenuEntity
public IEnumerable<MenuElement> AllElements() => AllEntities().SelectMany(e => e.AllElements());

/// <summary>
/// Record `entity` as a child of this group. The group is responsible for storing it in whatever data structure makes sense for it.
/// Register `entity` as a child of this group.
/// </summary>
protected void ParentEntity(IMenuEntity entity)
{
entity.SetMenuParent(this);
if (gameObjectParent != null)
entity.SetGameObjectParent(gameObjectParent);
}
protected void AddChild(IMenuEntity entity) => entity.SetParents(this, gameObjectParent);

/// <summary>
/// Enumerate all navigables which should be directly connected in `direction`.
Expand Down Expand Up @@ -77,17 +71,22 @@ public abstract bool GetSelectable(
/// <inheritdoc/>
public virtual void SetGameObjectParent(GameObject parent)
{
if (gameObjectParent != null)
throw new ArgumentException("GameObjectParent already set");
ClearGameObjectParent();

gameObjectParent = parent;
foreach (var entity in AllEntities())
entity.SetGameObjectParent(gameObjectParent);
}

/// <inheritdoc/>
public virtual void SetMenuParent(IMenuEntity parent) =>
visibility.SetParent(parent.Visibility);
public virtual void ClearGameObjectParent()
{
if (gameObjectParent == null)
return;

foreach (var entity in AllEntities())
entity.ClearGameObjectParent();
}

/// <inheritdoc/>
public virtual void SetNeighbor(NavigationDirection direction, Selectable selectable)
Expand Down
18 changes: 16 additions & 2 deletions Elements/FreeGroup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ public class FreeGroup : AbstractGroup
/// </summary>
public void Add(IMenuEntity entity, Vector2 offset)
{
entities.Add(entity, offset);
ParentEntity(entity);
entities[entity] = offset;
AddChild(entity);
}

/// <summary>
Expand All @@ -39,6 +39,20 @@ public void Update(IMenuEntity entity, Vector2 offset)
entities[entity] = offset;
}

/// <summary>
/// Remove the specified entity from this group.
/// </summary>
public bool Remove(IMenuEntity entity)
{
if (entities.Remove(entity))
{
entity.ClearParents();
return true;
}

return false;
}

// Sort low values first.
private static float SortKey(NavigationDirection direction, Vector2 pos) =>
direction switch
Expand Down
130 changes: 89 additions & 41 deletions Elements/GridGroup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ namespace Silksong.ModMenu.Elements;
/// </summary>
public class GridGroup(int columns) : AbstractGroup
{
private readonly List<IMenuEntity?[]> entitiesByRow = [];
private readonly List<IMenuEntity?[]> rows = [];
private readonly Dictionary<IMenuEntity, GridCell> index = [];

/// <summary>
/// The number of columns in this grid.
Expand All @@ -24,7 +25,7 @@ public class GridGroup(int columns) : AbstractGroup
/// <summary>
/// The number of rows in this grid.
/// </summary>
public int Rows => entitiesByRow.Count;
public int Rows => rows.Count;

/// <summary>
/// Spacing between different columns.
Expand All @@ -46,11 +47,11 @@ public class GridGroup(int columns) : AbstractGroup
/// </summary>
public void Add(IMenuEntity entity)
{
while (IsFull(nextEmpty))
nextEmpty = nextEmpty.Next(this);
while (IsFull(nextEmptyCell))
nextEmptyCell = nextEmptyCell.Next(this);

AddAt(nextEmpty.Row, nextEmpty.Column, entity);
nextEmpty = nextEmpty.Next(this);
AddAt(nextEmptyCell.Row, nextEmptyCell.Column, entity);
nextEmptyCell = nextEmptyCell.Next(this);
}

/// <summary>
Expand All @@ -65,16 +66,52 @@ public void AddAt(int row, int column, IMenuEntity entity)
throw new ArgumentException($"{nameof(row)}: {row} (Must be >= 0)");
if (column < 0 || column >= Columns)
throw new ArgumentException($"{nameof(column)}: {column} (Must be in [0, {Columns}))");
if (IsFull(new(row, column)))
throw new ArgumentException($"Cell({row}, {column}) is already filled.");

while (entitiesByRow.Count <= row)
entitiesByRow.Add(new IMenuEntity?[Columns]);
entitiesByRow[row][column] = entity;
GridCell cell = new(row, column);
if (index.TryGetValue(entity, out var prevCell))
{
if (cell == prevCell)
return; // Nothing to do.
else
throw new ArgumentException($"Entity already present at ({row}, {column})");
}

ParentEntity(entity);
if (IsFull(cell))
throw new ArgumentException($"({row}, {column}) is already filled.");

while (rows.Count <= row)
rows.Add(new IMenuEntity?[Columns]);
rows[row][column] = entity;
index[entity] = cell;
AddChild(entity);
}

/// <summary>
/// Remove the specified entity from the grid.
/// </summary>
public bool Remove(IMenuEntity entity)
{
if (!index.TryGetValue(entity, out var cell))
return false;

index.Remove(entity);
rows[cell.Row][cell.Column] = null;
nextEmptyCell = cell.CompareTo(nextEmptyCell) <= 0 ? cell : nextEmptyCell;
entity.ClearParents();

// Truncate empty rows.
if (cell.Row == rows.Count - 1)
for (int row = cell.Row; row >= 0 && rows[row].All(e => e == null); row--)
rows.RemoveAt(row);
return true;
}

/// <summary>
/// Remove the entity at the specified cell.
/// </summary>
public bool RemoveAt(int row, int column) =>
TryGetValue(new(row, column), out var entity) && Remove(entity);

/// <inheritdoc/>
public override bool GetSelectable(
NavigationDirection direction,
Expand Down Expand Up @@ -114,24 +151,19 @@ public override void UpdateLayout(Vector2 localAnchorPos)
ClearNeighbors();

// Update positions.
for (int row = 0; row < entitiesByRow.Count; row++)
foreach (var e in index)
{
for (int column = 0; column < Columns; column++)
{
var entity = entitiesByRow[row][column];
if (entity == null)
continue;

Vector2 pos = localAnchorPos;
pos.y -= VerticalSpacing * row;
pos.x += HorizontalSpacing * (column - (Columns - 1) / 2f);
entity.UpdateLayout(pos);
}
var (entity, cell) = (e.Key, e.Value);

Vector2 pos = localAnchorPos;
pos.y -= VerticalSpacing * cell.Row;
pos.x += HorizontalSpacing * (cell.Column - (Columns - 1) / 2f);
entity.UpdateLayout(pos);
}

// Update navigation.
INavigable?[]? prevRow = null;
foreach (var row in entitiesByRow)
foreach (var row in rows)
{
INavigable?[] nextRow =
[
Expand Down Expand Up @@ -205,46 +237,62 @@ static bool ClosestColumn(

/// <inheritdoc/>
protected override IEnumerable<IMenuEntity> AllEntities() =>
entitiesByRow.SelectMany(row => row.WhereNonNull());
rows.SelectMany(row => row.WhereNonNull());

private ListView<ListView<IMenuEntity?>> GetColumns() =>
new(column => new(row => entitiesByRow[row][column], entitiesByRow.Count), Columns);
new(column => new(row => rows[row][column], rows.Count), Columns);

/// <inheritdoc/>
protected override IEnumerable<INavigable> GetNavigables(NavigationDirection direction) =>
direction switch
{
// All elements of first row with stuff in it.
NavigationDirection.Up => entitiesByRow
.Where(row => row.Any(e => e is INavigable && e.VisibleSelf))
NavigationDirection.Up => rows.Where(row =>
row.Any(e => e is INavigable && e.VisibleSelf)
)
.FirstOrDefault()
?.OfType<INavigable>()
?? [],
?? [],
// Leftmost element of every row.
NavigationDirection.Left => entitiesByRow
.SelectMany(row => row.Where(e => e is INavigable && e.VisibleSelf).Take(1))
NavigationDirection.Left => rows.SelectMany(row =>
row.Where(e => e is INavigable && e.VisibleSelf).Take(1)
)
.OfType<INavigable>(),
// Rightmost element of every row.
NavigationDirection.Right => entitiesByRow
.SelectMany(row => row.Where(e => e is INavigable && e.VisibleSelf).TakeLast(1))
NavigationDirection.Right => rows.SelectMany(row =>
row.Where(e => e is INavigable && e.VisibleSelf).TakeLast(1)
)
.OfType<INavigable>(),
// All elements of last row with stuff in it.
NavigationDirection.Down => entitiesByRow
.Where(row => row.Any(e => e is INavigable && e.VisibleSelf))
NavigationDirection.Down => rows.Where(row =>
row.Any(e => e is INavigable && e.VisibleSelf)
)
.LastOrDefault()
?.OfType<INavigable>()
?? [],
?? [],
_ => throw new ArgumentException($"{direction}"),
};

private record GridCell(int Row, int Column)
private record GridCell(int Row, int Column) : IComparable<GridCell>
{
internal GridCell Next(GridGroup parent) =>
Column == parent.Columns - 1 ? new(Row + 1, 0) : new(Row, Column + 1);

public int CompareTo(GridCell other) =>
Row == other.Row ? Column.CompareTo(other.Column) : Row.CompareTo(other.Row);
}

private GridCell nextEmpty = new(0, 0);
private GridCell nextEmptyCell = new(0, 0);

private bool TryGetValue(GridCell cell, [MaybeNullWhen(false)] out IMenuEntity entity)
{
entity = default;
if (cell.Row < 0 || cell.Row >= rows.Count || cell.Column < 0 || cell.Column >= Columns)
return false;

entity = rows[cell.Row][cell.Column];
return entity != null;
}

private bool IsFull(GridCell cell) =>
cell.Row < Rows && entitiesByRow[cell.Row][cell.Column] != null;
private bool IsFull(GridCell cell) => TryGetValue(cell, out _);
}
8 changes: 4 additions & 4 deletions Elements/IMenuEntity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@ public interface IMenuEntity
void UpdateLayout(Vector2 localAnchorPos);

/// <summary>
/// Set the visibility parent of this entity. Can only be done once.
/// Sets the GameObject container for this entity, from which all positions are relative.
/// </summary>
void SetMenuParent(IMenuEntity parent);
void SetGameObjectParent(GameObject container);

/// <summary>
/// Sets the GameObject container for this entity, from which all positions are relative. Can only be done once.
/// Make this entity parent-less, which in most cases also renders it invisible.
/// </summary>
void SetGameObjectParent(GameObject container);
void ClearGameObjectParent();
}
Loading