Skip to content

Commit dc696bd

Browse files
authored
Merge pull request #93 from ELDment/beta-1
feat: Enhance menu system with new features and improved usability
2 parents d8e7368 + e832468 commit dc696bd

File tree

20 files changed

+1744
-1051
lines changed

20 files changed

+1744
-1051
lines changed

managed/src/SwiftlyS2.Core/Modules/Menus/MenuAPI.cs

Lines changed: 43 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ namespace SwiftlyS2.Core.Menus;
99

1010
internal sealed class MenuAPI : IMenuAPI, IDisposable
1111
{
12-
private IMenuAPI? parent;
12+
private (IMenuAPI? ParentMenu, IMenuOption? TriggerOption) parent;
1313

1414
/// <summary>
1515
/// The menu manager that this menu belongs to.
@@ -42,19 +42,19 @@ internal sealed class MenuAPI : IMenuAPI, IDisposable
4242
public IMenuBuilderAPI? Builder { get; init; }
4343

4444
/// <summary>
45-
/// The parent menu in a hierarchical menu structure, or null if this is a top-level menu.
45+
/// The parent hierarchy information in a hierarchical menu structure.
4646
/// </summary>
47-
public IMenuAPI? Parent {
47+
public (IMenuAPI? ParentMenu, IMenuOption? TriggerOption) Parent {
4848
get => parent;
4949
internal set {
5050
if (parent == value)
5151
{
5252
return;
5353
}
5454

55-
if (value == null || value == this)
55+
if (value.ParentMenu == this)
5656
{
57-
Spectre.Console.AnsiConsole.WriteException(new ArgumentException($"Parent cannot be null or self.", nameof(value)));
57+
Spectre.Console.AnsiConsole.WriteException(new ArgumentException($"Parent cannot be self.", nameof(value)));
5858
}
5959
else
6060
{
@@ -318,41 +318,49 @@ MenuOptionScrollStyle.LinearScroll when clampedDesiredIndex < maxVisibleItems -
318318

319319
private string BuildMenuHtml( IPlayer player, IReadOnlyList<IMenuOption> visibleOptions, int arrowPosition, int selectedIndex, int maxOptions, int maxVisibleItems )
320320
{
321-
var titleSection = Configuration.HideTitle
322-
? string.Empty
323-
: string.Concat(
324-
$"<font class='fontSize-m' color='#FFFFFF'>{Configuration.Title}</font>",
325-
maxOptions > maxVisibleItems
326-
? $"<font class='fontSize-s' color='#FFFFFF'> [{selectedIndex + 1}/{maxOptions}]</font><br><font class='fontSize-s' color='#FFFFFF'>──────────────────────────</font><br>"
327-
: "<br><font class='fontSize-s' color='#FFFFFF'>──────────────────────────</font><br>"
328-
);
329-
330-
var menuItems = visibleOptions.Select(( option, index ) =>
331-
{
332-
var prefix = index == arrowPosition
333-
? $"<font color='#FFFFFF' class='fontSize-sm'>{core.MenusAPI.Configuration.NavigationPrefix} </font>"
334-
: "\u00A0\u00A0\u00A0 ";
335-
return $"{prefix}{option.GetDisplayText(player, 0)}";
336-
});
321+
var guideLineColor = Configuration.VisualGuideLineColor ?? "#FFFFFF";
322+
var navigationColor = Configuration.NavigationMarkerColor ?? "#FFFFFF";
323+
var footerColor = Configuration.FooterColor ?? "#FF0000";
324+
var guideLine = $"<font class='fontSize-s' color='{guideLineColor}'>──────────────────────────</font>";
325+
326+
var titleSection = Configuration.HideTitle ? string.Empty : string.Concat(
327+
$"<font class='fontSize-m' color='#FFFFFF'>{Configuration.Title}</font>",
328+
maxOptions > maxVisibleItems
329+
? string.Concat($"<font class='fontSize-s' color='#FFFFFF'> [{selectedIndex + 1}/{maxOptions}]</font><br>", guideLine, "<br>")
330+
: string.Concat("<br>", guideLine, "<br>")
331+
);
337332

338-
var footerSection = Configuration.HideFooter ? string.Empty : new Func<string>(() =>
339-
{
340-
var isWasd = core.MenusAPI.Configuration.InputMode == "wasd";
341-
var moveKey = isWasd ? "W/S" : $"{KeybindOverrides.Move?.ToString() ?? core.MenusAPI.Configuration.ButtonsScroll.ToUpper()}/{KeybindOverrides.MoveBack?.ToString() ?? core.MenusAPI.Configuration.ButtonsScrollBack.ToUpper()}";
342-
var useKey = isWasd ? "D" : (KeybindOverrides.Select?.ToString() ?? core.MenusAPI.Configuration.ButtonsUse).ToUpper();
343-
var exitKey = isWasd ? "A" : (KeybindOverrides.Exit?.ToString() ?? core.MenusAPI.Configuration.ButtonsExit).ToUpper();
344-
return string.Concat(
345-
$"<br>",
346-
$"<font class='fontSize-s' color='#FFFFFF'>──────────────────────────</font>",
347-
$"<br>",
348-
$"<font class='fontSize-s' color='#FFFFFF'><font color='#FF0000'>Move:</font> {moveKey} | <font color='#FF0000'>Use:</font> {useKey} | <font color='#FF0000'>Exit:</font> {exitKey}</font>"
349-
);
350-
})();
333+
var menuItems = string.Join("<br>", visibleOptions.Select(( option, index ) => string.Concat(
334+
index == arrowPosition
335+
? $"<font color='{navigationColor}' class='fontSize-sm'>{core.MenusAPI.Configuration.NavigationPrefix} </font>"
336+
: "\u00A0\u00A0\u00A0 ",
337+
option.GetDisplayText(player, 0)
338+
)));
339+
340+
var footerSection = Configuration.HideFooter ? string.Empty :
341+
core.MenusAPI.Configuration.InputMode switch {
342+
"wasd" => string.Concat(
343+
"<br>", guideLine, "<br>",
344+
"<font class='fontSize-s' color='#FFFFFF'>",
345+
$"<font color='{footerColor}'>Move:</font> W/S | ",
346+
$"<font color='{footerColor}'>Use:</font> D | ",
347+
$"<font color='{footerColor}'>Exit:</font> A",
348+
"</font>"
349+
),
350+
_ => string.Concat(
351+
"<br>", guideLine, "<br>",
352+
"<font class='fontSize-s' color='#FFFFFF'>",
353+
$"<font color='{footerColor}'>Move:</font> {KeybindOverrides.Move?.ToString() ?? core.MenusAPI.Configuration.ButtonsScroll.ToUpper()}/{KeybindOverrides.MoveBack?.ToString() ?? core.MenusAPI.Configuration.ButtonsScrollBack.ToUpper()} | ",
354+
$"<font color='{footerColor}'>Use:</font> {KeybindOverrides.Select?.ToString() ?? core.MenusAPI.Configuration.ButtonsUse.ToUpper()} | ",
355+
$"<font color='{footerColor}'>Exit:</font> {KeybindOverrides.Exit?.ToString() ?? core.MenusAPI.Configuration.ButtonsExit.ToUpper()}",
356+
"</font>"
357+
)
358+
};
351359

352360
return string.Concat(
353361
titleSection,
354362
"<font color='#FFFFFF' class='fontSize-sm'>",
355-
string.Join("<br>", menuItems),
363+
menuItems,
356364
"</font>",
357365
footerSection
358366
);

managed/src/SwiftlyS2.Core/Modules/Menus/MenuBuilderAPI.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ public IMenuBuilderAPI SetExitButton( KeyBind keyBind )
8787

8888
public IMenuAPI Build()
8989
{
90-
var menu = new MenuAPI(core, configuration, keybindOverrides, this/*, parent*/, optionScrollStyle/*, optionTextStyle*/) { Parent = parent };
90+
var menu = new MenuAPI(core, configuration, keybindOverrides, this/*, parent*/, optionScrollStyle/*, optionTextStyle*/) { Parent = (parent, null) };
9191
options.ForEach(option => menu.AddOption(option));
9292
return menu;
9393
}

managed/src/SwiftlyS2.Core/Modules/Menus/MenuDesignAPI.cs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,78 @@ public IMenuBuilderAPI SetGlobalScrollStyle( MenuOptionScrollStyle style )
5959
return builder;
6060
}
6161

62+
public IMenuBuilderAPI SetNavigationMarkerColor( string? hexColor = null )
63+
{
64+
configuration.NavigationMarkerColor = hexColor;
65+
return builder;
66+
}
67+
68+
public IMenuBuilderAPI SetNavigationMarkerColor( Shared.Natives.Color color )
69+
{
70+
configuration.NavigationMarkerColor = color.ToHex();
71+
return builder;
72+
}
73+
74+
public IMenuBuilderAPI SetNavigationMarkerColor( System.Drawing.Color color )
75+
{
76+
configuration.NavigationMarkerColor = $"#{color.R:X2}{color.G:X2}{color.B:X2}";
77+
return builder;
78+
}
79+
80+
public IMenuBuilderAPI SetMenuFooterColor( string? hexColor = null )
81+
{
82+
configuration.FooterColor = hexColor;
83+
return builder;
84+
}
85+
86+
public IMenuBuilderAPI SetMenuFooterColor( Shared.Natives.Color color )
87+
{
88+
configuration.FooterColor = color.ToHex();
89+
return builder;
90+
}
91+
92+
public IMenuBuilderAPI SetMenuFooterColor( System.Drawing.Color color )
93+
{
94+
configuration.FooterColor = $"#{color.R:X2}{color.G:X2}{color.B:X2}";
95+
return builder;
96+
}
97+
98+
public IMenuBuilderAPI SetVisualGuideLineColor( string? hexColor = null )
99+
{
100+
configuration.VisualGuideLineColor = hexColor;
101+
return builder;
102+
}
103+
104+
public IMenuBuilderAPI SetVisualGuideLineColor( Shared.Natives.Color color )
105+
{
106+
configuration.VisualGuideLineColor = color.ToHex();
107+
return builder;
108+
}
109+
110+
public IMenuBuilderAPI SetVisualGuideLineColor( System.Drawing.Color color )
111+
{
112+
configuration.VisualGuideLineColor = $"#{color.R:X2}{color.G:X2}{color.B:X2}";
113+
return builder;
114+
}
115+
116+
public IMenuBuilderAPI SetDisabledColor( string? hexColor = null )
117+
{
118+
configuration.DisabledColor = hexColor;
119+
return builder;
120+
}
121+
122+
public IMenuBuilderAPI SetDisabledColor( Shared.Natives.Color color )
123+
{
124+
configuration.DisabledColor = color.ToHex();
125+
return builder;
126+
}
127+
128+
public IMenuBuilderAPI SetDisabledColor( System.Drawing.Color color )
129+
{
130+
configuration.DisabledColor = $"#{color.R:X2}{color.G:X2}{color.B:X2}";
131+
return builder;
132+
}
133+
62134
// public IMenuBuilderAPI SetGlobalOptionTextStyle( MenuOptionTextStyle style )
63135
// {
64136
// setTextStyle(style);

managed/src/SwiftlyS2.Core/Modules/Menus/MenuManagerAPI.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ private void KeyStateChange( IOnClientKeyStateChangedEvent @event )
168168
else if (useKey.HasFlag(@event.Key.ToKeyBind()))
169169
{
170170
var option = menu.GetCurrentOption(player);
171-
if (option != null && option.Enabled && option.GetEnabled(player))
171+
if (option != null && option.Enabled && option.GetEnabled(player) && option.IsClickTaskCompleted(player))
172172
{
173173
_ = Task.Run(async () => await option.OnClickAsync(player));
174174

@@ -218,7 +218,7 @@ private void KeyStateChange( IOnClientKeyStateChangedEvent @event )
218218
else if (KeyBind.D.HasFlag(@event.Key.ToKeyBind()))
219219
{
220220
var option = menu.GetCurrentOption(player);
221-
if (option != null && option.Enabled && option.GetEnabled(player))
221+
if (option != null && option.Enabled && option.GetEnabled(player) && option.IsClickTaskCompleted(player))
222222
{
223223
_ = Task.Run(async () => await option.OnClickAsync(player));
224224

@@ -283,7 +283,7 @@ public IMenuAPI CreateMenu( MenuConfiguration configuration, MenuKeybindOverride
283283
}
284284
}
285285

286-
return new MenuAPI(Core, configuration, keybindOverrides, null/*, parent*/, optionScrollStyle/*, optionTextStyle*/) { Parent = parent };
286+
return new MenuAPI(Core, configuration, keybindOverrides, null/*, parent*/, optionScrollStyle/*, optionTextStyle*/) { Parent = (parent, null) };
287287
}
288288

289289
public IMenuAPI? GetCurrentMenu( IPlayer player )
@@ -326,9 +326,9 @@ public void CloseMenuForPlayer( IPlayer player, IMenuAPI menu )
326326
menu.HideForPlayer(player);
327327
MenuClosed?.Invoke(this, new MenuManagerEventArgs { Player = player, Menu = menu });
328328

329-
if (menu.Parent != null)
329+
if (menu.Parent.ParentMenu != null)
330330
{
331-
OpenMenuForPlayer(player, menu.Parent);
331+
OpenMenuForPlayer(player, menu.Parent.ParentMenu);
332332
}
333333
}
334334
}
@@ -342,7 +342,7 @@ public void CloseAllMenus()
342342
{
343343
currentMenu.HideForPlayer(kvp.Key);
344344
MenuClosed?.Invoke(this, new MenuManagerEventArgs { Player = kvp.Key, Menu = currentMenu });
345-
currentMenu = currentMenu.Parent;
345+
currentMenu = currentMenu.Parent.ParentMenu;
346346
}
347347
_ = openMenus.TryRemove(kvp.Key, out _);
348348
});

managed/src/SwiftlyS2.Core/Modules/Menus/OptionsBase/ButtonMenuOption.cs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,21 @@ namespace SwiftlyS2.Core.Menus.OptionsBase;
55
/// </summary>
66
public sealed class ButtonMenuOption : MenuOptionBase
77
{
8+
/// <summary>
9+
/// Creates an instance of <see cref="ButtonMenuOption"/> with dynamic text updating capabilities.
10+
/// </summary>
11+
/// <param name="updateIntervalMs">The interval in milliseconds between text updates. Defaults to 120ms.</param>
12+
/// <param name="pauseIntervalMs">The pause duration in milliseconds before starting the next text update cycle. Defaults to 1000ms.</param>
13+
/// <remarks>
14+
/// When using this constructor, the <see cref="MenuOptionBase.Text"/> property must be manually set to specify the initial text.
15+
/// </remarks>
16+
public ButtonMenuOption(
17+
int updateIntervalMs = 120,
18+
int pauseIntervalMs = 1000 ) : base(updateIntervalMs, pauseIntervalMs)
19+
{
20+
PlaySound = true;
21+
}
22+
823
/// <summary>
924
/// Creates an instance of <see cref="ButtonMenuOption"/> with dynamic text updating capabilities.
1025
/// </summary>
@@ -14,9 +29,8 @@ public sealed class ButtonMenuOption : MenuOptionBase
1429
public ButtonMenuOption(
1530
string text,
1631
int updateIntervalMs = 120,
17-
int pauseIntervalMs = 1000 ) : base(updateIntervalMs, pauseIntervalMs)
32+
int pauseIntervalMs = 1000 ) : this(updateIntervalMs, pauseIntervalMs)
1833
{
1934
Text = text;
20-
PlaySound = true;
2135
}
2236
}

managed/src/SwiftlyS2.Core/Modules/Menus/OptionsBase/ChoiceMenuOption.cs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,19 @@ public sealed class ChoiceMenuOption : MenuOptionBase
2626
/// <summary>
2727
/// Creates an instance of <see cref="ChoiceMenuOption"/> with a list of choices.
2828
/// </summary>
29-
/// <param name="text">The text content to display.</param>
3029
/// <param name="choices">The list of available choices.</param>
3130
/// <param name="defaultChoice">The default choice to select. If null or not found, defaults to first choice.</param>
3231
/// <param name="updateIntervalMs">The interval in milliseconds between text updates. Defaults to 120ms.</param>
3332
/// <param name="pauseIntervalMs">The pause duration in milliseconds before starting the next text update cycle. Defaults to 1000ms.</param>
33+
/// <remarks>
34+
/// When using this constructor, the <see cref="MenuOptionBase.Text"/> property must be manually set to specify the initial text.
35+
/// </remarks>
3436
public ChoiceMenuOption(
35-
string text,
3637
IEnumerable<string> choices,
3738
string? defaultChoice = null,
3839
int updateIntervalMs = 120,
3940
int pauseIntervalMs = 1000 ) : base(updateIntervalMs, pauseIntervalMs)
4041
{
41-
Text = text;
4242
PlaySound = true;
4343
this.choices = choices.ToList();
4444

@@ -54,6 +54,24 @@ public ChoiceMenuOption(
5454
Click += OnChoiceClick;
5555
}
5656

57+
/// <summary>
58+
/// Creates an instance of <see cref="ChoiceMenuOption"/> with a list of choices.
59+
/// </summary>
60+
/// <param name="text">The text content to display.</param>
61+
/// <param name="choices">The list of available choices.</param>
62+
/// <param name="defaultChoice">The default choice to select. If null or not found, defaults to first choice.</param>
63+
/// <param name="updateIntervalMs">The interval in milliseconds between text updates. Defaults to 120ms.</param>
64+
/// <param name="pauseIntervalMs">The pause duration in milliseconds before starting the next text update cycle. Defaults to 1000ms.</param>
65+
public ChoiceMenuOption(
66+
string text,
67+
IEnumerable<string> choices,
68+
string? defaultChoice = null,
69+
int updateIntervalMs = 120,
70+
int pauseIntervalMs = 1000 ) : this(choices, defaultChoice, updateIntervalMs, pauseIntervalMs)
71+
{
72+
Text = text;
73+
}
74+
5775
public override string GetDisplayText( IPlayer player, int displayLine = 0 )
5876
{
5977
var text = base.GetDisplayText(player, displayLine);

0 commit comments

Comments
 (0)