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
78 changes: 43 additions & 35 deletions managed/src/SwiftlyS2.Core/Modules/Menus/MenuAPI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace SwiftlyS2.Core.Menus;

internal sealed class MenuAPI : IMenuAPI, IDisposable
{
private IMenuAPI? parent;
private (IMenuAPI? ParentMenu, IMenuOption? TriggerOption) parent;

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

/// <summary>
/// The parent menu in a hierarchical menu structure, or null if this is a top-level menu.
/// The parent hierarchy information in a hierarchical menu structure.
/// </summary>
public IMenuAPI? Parent {
public (IMenuAPI? ParentMenu, IMenuOption? TriggerOption) Parent {
get => parent;
internal set {
if (parent == value)
{
return;
}

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

private string BuildMenuHtml( IPlayer player, IReadOnlyList<IMenuOption> visibleOptions, int arrowPosition, int selectedIndex, int maxOptions, int maxVisibleItems )
{
var titleSection = Configuration.HideTitle
? string.Empty
: string.Concat(
$"<font class='fontSize-m' color='#FFFFFF'>{Configuration.Title}</font>",
maxOptions > maxVisibleItems
? $"<font class='fontSize-s' color='#FFFFFF'> [{selectedIndex + 1}/{maxOptions}]</font><br><font class='fontSize-s' color='#FFFFFF'>──────────────────────────</font><br>"
: "<br><font class='fontSize-s' color='#FFFFFF'>──────────────────────────</font><br>"
);

var menuItems = visibleOptions.Select(( option, index ) =>
{
var prefix = index == arrowPosition
? $"<font color='#FFFFFF' class='fontSize-sm'>{core.MenusAPI.Configuration.NavigationPrefix} </font>"
: "\u00A0\u00A0\u00A0 ";
return $"{prefix}{option.GetDisplayText(player, 0)}";
});
var guideLineColor = Configuration.VisualGuideLineColor ?? "#FFFFFF";
var navigationColor = Configuration.NavigationMarkerColor ?? "#FFFFFF";
var footerColor = Configuration.FooterColor ?? "#FF0000";
var guideLine = $"<font class='fontSize-s' color='{guideLineColor}'>──────────────────────────</font>";

var titleSection = Configuration.HideTitle ? string.Empty : string.Concat(
$"<font class='fontSize-m' color='#FFFFFF'>{Configuration.Title}</font>",
maxOptions > maxVisibleItems
? string.Concat($"<font class='fontSize-s' color='#FFFFFF'> [{selectedIndex + 1}/{maxOptions}]</font><br>", guideLine, "<br>")
: string.Concat("<br>", guideLine, "<br>")
);

var footerSection = Configuration.HideFooter ? string.Empty : new Func<string>(() =>
{
var isWasd = core.MenusAPI.Configuration.InputMode == "wasd";
var moveKey = isWasd ? "W/S" : $"{KeybindOverrides.Move?.ToString() ?? core.MenusAPI.Configuration.ButtonsScroll.ToUpper()}/{KeybindOverrides.MoveBack?.ToString() ?? core.MenusAPI.Configuration.ButtonsScrollBack.ToUpper()}";
var useKey = isWasd ? "D" : (KeybindOverrides.Select?.ToString() ?? core.MenusAPI.Configuration.ButtonsUse).ToUpper();
var exitKey = isWasd ? "A" : (KeybindOverrides.Exit?.ToString() ?? core.MenusAPI.Configuration.ButtonsExit).ToUpper();
return string.Concat(
$"<br>",
$"<font class='fontSize-s' color='#FFFFFF'>──────────────────────────</font>",
$"<br>",
$"<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>"
);
})();
var menuItems = string.Join("<br>", visibleOptions.Select(( option, index ) => string.Concat(
index == arrowPosition
? $"<font color='{navigationColor}' class='fontSize-sm'>{core.MenusAPI.Configuration.NavigationPrefix} </font>"
: "\u00A0\u00A0\u00A0 ",
option.GetDisplayText(player, 0)
)));

var footerSection = Configuration.HideFooter ? string.Empty :
core.MenusAPI.Configuration.InputMode switch {
"wasd" => string.Concat(
"<br>", guideLine, "<br>",
"<font class='fontSize-s' color='#FFFFFF'>",
$"<font color='{footerColor}'>Move:</font> W/S | ",
$"<font color='{footerColor}'>Use:</font> D | ",
$"<font color='{footerColor}'>Exit:</font> A",
"</font>"
),
_ => string.Concat(
"<br>", guideLine, "<br>",
"<font class='fontSize-s' color='#FFFFFF'>",
$"<font color='{footerColor}'>Move:</font> {KeybindOverrides.Move?.ToString() ?? core.MenusAPI.Configuration.ButtonsScroll.ToUpper()}/{KeybindOverrides.MoveBack?.ToString() ?? core.MenusAPI.Configuration.ButtonsScrollBack.ToUpper()} | ",
$"<font color='{footerColor}'>Use:</font> {KeybindOverrides.Select?.ToString() ?? core.MenusAPI.Configuration.ButtonsUse.ToUpper()} | ",
$"<font color='{footerColor}'>Exit:</font> {KeybindOverrides.Exit?.ToString() ?? core.MenusAPI.Configuration.ButtonsExit.ToUpper()}",
"</font>"
)
};

return string.Concat(
titleSection,
"<font color='#FFFFFF' class='fontSize-sm'>",
string.Join("<br>", menuItems),
menuItems,
"</font>",
footerSection
);
Expand Down
2 changes: 1 addition & 1 deletion managed/src/SwiftlyS2.Core/Modules/Menus/MenuBuilderAPI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ public IMenuBuilderAPI SetExitButton( KeyBind keyBind )

public IMenuAPI Build()
{
var menu = new MenuAPI(core, configuration, keybindOverrides, this/*, parent*/, optionScrollStyle/*, optionTextStyle*/) { Parent = parent };
var menu = new MenuAPI(core, configuration, keybindOverrides, this/*, parent*/, optionScrollStyle/*, optionTextStyle*/) { Parent = (parent, null) };
options.ForEach(option => menu.AddOption(option));
return menu;
}
Expand Down
72 changes: 72 additions & 0 deletions managed/src/SwiftlyS2.Core/Modules/Menus/MenuDesignAPI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,78 @@ public IMenuBuilderAPI SetGlobalScrollStyle( MenuOptionScrollStyle style )
return builder;
}

public IMenuBuilderAPI SetNavigationMarkerColor( string? hexColor = null )
{
configuration.NavigationMarkerColor = hexColor;
return builder;
}

public IMenuBuilderAPI SetNavigationMarkerColor( Shared.Natives.Color color )
{
configuration.NavigationMarkerColor = color.ToHex();
return builder;
}

public IMenuBuilderAPI SetNavigationMarkerColor( System.Drawing.Color color )
{
configuration.NavigationMarkerColor = $"#{color.R:X2}{color.G:X2}{color.B:X2}";
return builder;
}

public IMenuBuilderAPI SetMenuFooterColor( string? hexColor = null )
{
configuration.FooterColor = hexColor;
return builder;
}

public IMenuBuilderAPI SetMenuFooterColor( Shared.Natives.Color color )
{
configuration.FooterColor = color.ToHex();
return builder;
}

public IMenuBuilderAPI SetMenuFooterColor( System.Drawing.Color color )
{
configuration.FooterColor = $"#{color.R:X2}{color.G:X2}{color.B:X2}";
return builder;
}

public IMenuBuilderAPI SetVisualGuideLineColor( string? hexColor = null )
{
configuration.VisualGuideLineColor = hexColor;
return builder;
}

public IMenuBuilderAPI SetVisualGuideLineColor( Shared.Natives.Color color )
{
configuration.VisualGuideLineColor = color.ToHex();
return builder;
}

public IMenuBuilderAPI SetVisualGuideLineColor( System.Drawing.Color color )
{
configuration.VisualGuideLineColor = $"#{color.R:X2}{color.G:X2}{color.B:X2}";
return builder;
}

public IMenuBuilderAPI SetDisabledColor( string? hexColor = null )
{
configuration.DisabledColor = hexColor;
return builder;
}

public IMenuBuilderAPI SetDisabledColor( Shared.Natives.Color color )
{
configuration.DisabledColor = color.ToHex();
return builder;
}

public IMenuBuilderAPI SetDisabledColor( System.Drawing.Color color )
{
configuration.DisabledColor = $"#{color.R:X2}{color.G:X2}{color.B:X2}";
return builder;
}

// public IMenuBuilderAPI SetGlobalOptionTextStyle( MenuOptionTextStyle style )
// {
// setTextStyle(style);
Expand Down
12 changes: 6 additions & 6 deletions managed/src/SwiftlyS2.Core/Modules/Menus/MenuManagerAPI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ private void KeyStateChange( IOnClientKeyStateChangedEvent @event )
else if (useKey.HasFlag(@event.Key.ToKeyBind()))
{
var option = menu.GetCurrentOption(player);
if (option != null && option.Enabled && option.GetEnabled(player))
if (option != null && option.Enabled && option.GetEnabled(player) && option.IsClickTaskCompleted(player))
{
_ = Task.Run(async () => await option.OnClickAsync(player));

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

Expand Down Expand Up @@ -283,7 +283,7 @@ public IMenuAPI CreateMenu( MenuConfiguration configuration, MenuKeybindOverride
}
}

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

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

if (menu.Parent != null)
if (menu.Parent.ParentMenu != null)
{
OpenMenuForPlayer(player, menu.Parent);
OpenMenuForPlayer(player, menu.Parent.ParentMenu);
}
}
}
Expand All @@ -342,7 +342,7 @@ public void CloseAllMenus()
{
currentMenu.HideForPlayer(kvp.Key);
MenuClosed?.Invoke(this, new MenuManagerEventArgs { Player = kvp.Key, Menu = currentMenu });
currentMenu = currentMenu.Parent;
currentMenu = currentMenu.Parent.ParentMenu;
}
_ = openMenus.TryRemove(kvp.Key, out _);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,21 @@ namespace SwiftlyS2.Core.Menus.OptionsBase;
/// </summary>
public sealed class ButtonMenuOption : MenuOptionBase
{
/// <summary>
/// Creates an instance of <see cref="ButtonMenuOption"/> with dynamic text updating capabilities.
/// </summary>
/// <param name="updateIntervalMs">The interval in milliseconds between text updates. Defaults to 120ms.</param>
/// <param name="pauseIntervalMs">The pause duration in milliseconds before starting the next text update cycle. Defaults to 1000ms.</param>
/// <remarks>
/// When using this constructor, the <see cref="MenuOptionBase.Text"/> property must be manually set to specify the initial text.
/// </remarks>
public ButtonMenuOption(
int updateIntervalMs = 120,
int pauseIntervalMs = 1000 ) : base(updateIntervalMs, pauseIntervalMs)
{
PlaySound = true;
}

/// <summary>
/// Creates an instance of <see cref="ButtonMenuOption"/> with dynamic text updating capabilities.
/// </summary>
Expand All @@ -14,9 +29,8 @@ public sealed class ButtonMenuOption : MenuOptionBase
public ButtonMenuOption(
string text,
int updateIntervalMs = 120,
int pauseIntervalMs = 1000 ) : base(updateIntervalMs, pauseIntervalMs)
int pauseIntervalMs = 1000 ) : this(updateIntervalMs, pauseIntervalMs)
{
Text = text;
PlaySound = true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,19 @@ public sealed class ChoiceMenuOption : MenuOptionBase
/// <summary>
/// Creates an instance of <see cref="ChoiceMenuOption"/> with a list of choices.
/// </summary>
/// <param name="text">The text content to display.</param>
/// <param name="choices">The list of available choices.</param>
/// <param name="defaultChoice">The default choice to select. If null or not found, defaults to first choice.</param>
/// <param name="updateIntervalMs">The interval in milliseconds between text updates. Defaults to 120ms.</param>
/// <param name="pauseIntervalMs">The pause duration in milliseconds before starting the next text update cycle. Defaults to 1000ms.</param>
/// <remarks>
/// When using this constructor, the <see cref="MenuOptionBase.Text"/> property must be manually set to specify the initial text.
/// </remarks>
public ChoiceMenuOption(
string text,
IEnumerable<string> choices,
string? defaultChoice = null,
int updateIntervalMs = 120,
int pauseIntervalMs = 1000 ) : base(updateIntervalMs, pauseIntervalMs)
{
Text = text;
PlaySound = true;
this.choices = choices.ToList();

Expand All @@ -54,6 +54,24 @@ public ChoiceMenuOption(
Click += OnChoiceClick;
}

/// <summary>
/// Creates an instance of <see cref="ChoiceMenuOption"/> with a list of choices.
/// </summary>
/// <param name="text">The text content to display.</param>
/// <param name="choices">The list of available choices.</param>
/// <param name="defaultChoice">The default choice to select. If null or not found, defaults to first choice.</param>
/// <param name="updateIntervalMs">The interval in milliseconds between text updates. Defaults to 120ms.</param>
/// <param name="pauseIntervalMs">The pause duration in milliseconds before starting the next text update cycle. Defaults to 1000ms.</param>
public ChoiceMenuOption(
string text,
IEnumerable<string> choices,
string? defaultChoice = null,
int updateIntervalMs = 120,
int pauseIntervalMs = 1000 ) : this(choices, defaultChoice, updateIntervalMs, pauseIntervalMs)
{
Text = text;
}

public override string GetDisplayText( IPlayer player, int displayLine = 0 )
{
var text = base.GetDisplayText(player, displayLine);
Expand Down
Loading
Loading