From 7a391e6253662920029d4fca4bc216dd911e66ca Mon Sep 17 00:00:00 2001 From: Keda Date: Sun, 2 Nov 2025 00:26:11 +0100 Subject: [PATCH] =?UTF-8?q?"Am=C3=A9lioration=20de=20l'UI=20AutoDetect=20S?= =?UTF-8?q?yncshell=20:=20ajout=20de=20param=C3=A8tres=20r=C3=A9currents,?= =?UTF-8?q?=20plages=20horaires=20et=20gestion=20des=20fuseaux=20horaires.?= =?UTF-8?q?=20Refactorisation=20des=20m=C3=A9thodes=20de=20dessin=20et=20i?= =?UTF-8?q?ntroduction=20de=20boutons=20centr=C3=A9s=20pour=20une=20meille?= =?UTF-8?q?ure=20ergonomie.=20Mise=20=C3=A0=20jour=20des=20fichiers=20de?= =?UTF-8?q?=20configuration=20et=20du=20projet=20avec=20des=20optimisation?= =?UTF-8?q?s=20diverses."?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 + MareAPI | 2 +- MareSynchronos/MareSynchronos.csproj | 2 + .../AutoDetect/SyncshellDiscoveryService.cs | 12 ++ MareSynchronos/UI/Components/DrawGroupPair.cs | 21 ++- MareSynchronos/UI/Components/DrawPairBase.cs | 18 +- MareSynchronos/UI/Components/DrawUserPair.cs | 26 ++- MareSynchronos/UI/SyncshellAdminUI.cs | 154 +++++++++++++++++- MareSynchronos/UI/UISharedService.cs | 94 +++++++++++ 9 files changed, 312 insertions(+), 20 deletions(-) diff --git a/.gitignore b/.gitignore index 0afd316..e3bdbdc 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore .idea +qodana.yaml # User-specific files *.rsuser *.suo @@ -13,6 +14,8 @@ MareSynchronos/.DS_Store *.zip UmbraServer_extracted/ +NuGet.config +Directory.Build.props # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs diff --git a/MareAPI b/MareAPI index deb911c..f75f16f 160000 --- a/MareAPI +++ b/MareAPI @@ -1 +1 @@ -Subproject commit deb911cb0a0e0abb2bfe4df9c243e09a70db4000 +Subproject commit f75f16fb13637ba3e2b7cfc4c79de252f4d4eea6 diff --git a/MareSynchronos/MareSynchronos.csproj b/MareSynchronos/MareSynchronos.csproj index 0b7986d..5c1f6c2 100644 --- a/MareSynchronos/MareSynchronos.csproj +++ b/MareSynchronos/MareSynchronos.csproj @@ -50,6 +50,8 @@ build$([System.DateTime]::UtcNow.ToString("yyyy-MM-ddTHH:mm:ss:fffZ")) enable + true + $(NoWarn);NU1900 diff --git a/MareSynchronos/Services/AutoDetect/SyncshellDiscoveryService.cs b/MareSynchronos/Services/AutoDetect/SyncshellDiscoveryService.cs index 67394b7..89d4392 100644 --- a/MareSynchronos/Services/AutoDetect/SyncshellDiscoveryService.cs +++ b/MareSynchronos/Services/AutoDetect/SyncshellDiscoveryService.cs @@ -73,6 +73,13 @@ public sealed class SyncshellDiscoveryService : IHostedService, IMediatorSubscri } public async Task SetVisibilityAsync(string gid, bool visible, CancellationToken ct) + { + return await SetVisibilityAsync(gid, visible, null, null, null, null, null, ct).ConfigureAwait(false); + } + + public async Task SetVisibilityAsync(string gid, bool visible, int? displayDurationHours, + int[]? activeWeekdays, TimeSpan? timeStartLocal, TimeSpan? timeEndLocal, string? timeZone, + CancellationToken ct) { try { @@ -80,6 +87,11 @@ public sealed class SyncshellDiscoveryService : IHostedService, IMediatorSubscri { GID = gid, AutoDetectVisible = visible, + DisplayDurationHours = displayDurationHours, + ActiveWeekdays = activeWeekdays, + TimeStartLocal = timeStartLocal.HasValue ? new DateTime(timeStartLocal.Value.Ticks).ToString("HH:mm") : null, + TimeEndLocal = timeEndLocal.HasValue ? new DateTime(timeEndLocal.Value.Ticks).ToString("HH:mm") : null, + TimeZone = timeZone, }; var success = await _apiController.SyncshellDiscoverySetVisibility(request).ConfigureAwait(false); if (!success) return false; diff --git a/MareSynchronos/UI/Components/DrawGroupPair.cs b/MareSynchronos/UI/Components/DrawGroupPair.cs index ec15593..bbf1bb7 100644 --- a/MareSynchronos/UI/Components/DrawGroupPair.cs +++ b/MareSynchronos/UI/Components/DrawGroupPair.cs @@ -79,6 +79,8 @@ public class DrawGroupPair : DrawPairBase width += _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Plus).X + spacing; } + width += spacing * 1.2f; + return width; } @@ -215,6 +217,7 @@ public class DrawGroupPair : DrawPairBase var pauseIcon = _fullInfoDto.GroupUserPermissions.IsPaused() ? FontAwesomeIcon.Play : FontAwesomeIcon.Pause; var pauseButtonWidth = _uiSharedService.GetIconButtonSize(pauseIcon).X; var barButtonWidth = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Bars).X; + var rightEdgeGap = spacing * 1.2f; float totalWidth = 0f; void Accumulate(bool condition, float width) @@ -242,7 +245,7 @@ public class DrawGroupPair : DrawPairBase float cardPaddingX = UiSharedService.GetCardContentPaddingX(); float rightMargin = cardPaddingX + 6f * ImGuiHelpers.GlobalScale; float baseX = MathF.Max(ImGui.GetCursorPosX(), - ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - rightMargin - totalWidth); + ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - rightMargin - rightEdgeGap - totalWidth); float currentX = baseX; ImGui.SameLine(); @@ -266,6 +269,16 @@ public class DrawGroupPair : DrawPairBase if (showInfo && infoIconWidth > 0f) { + bool centerWarning = permIcon == FontAwesomeIcon.ExclamationTriangle && showPause && showBars && !showShared && !showPlus; + if (centerWarning) + { + float barsClusterWidth = showBars ? (barButtonWidth + spacing * 0.5f) : 0f; + float leftAreaWidth = MathF.Max(totalWidth - pauseButtonWidth - barsClusterWidth, 0f); + float warningX = baseX + MathF.Max((leftAreaWidth - infoIconWidth) / 2f, 0f); + currentX = warningX; + ImGui.SetCursorPosX(currentX); + } + ImGui.SetCursorPosY(textPosY); if (individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled) { @@ -359,7 +372,7 @@ public class DrawGroupPair : DrawPairBase { ImGui.SetCursorPosY(originalY); - if (_uiSharedService.IconButton(FontAwesomeIcon.Plus)) + if (_uiSharedService.IconPlusButtonCentered()) { var targetUid = _pair.UserData.UID; if (!string.IsNullOrEmpty(targetUid)) @@ -376,7 +389,7 @@ public class DrawGroupPair : DrawPairBase { float gapToBars = showBars ? spacing * 0.5f : spacing; ImGui.SetCursorPosY(originalY); - if (_uiSharedService.IconButton(pauseIcon)) + if (pauseIcon == FontAwesomeIcon.Pause ? _uiSharedService.IconPauseButtonCentered() : _uiSharedService.IconButtonCentered(pauseIcon)) { var newPermissions = _fullInfoDto.GroupUserPermissions ^ GroupUserPermissions.Paused; _fullInfoDto.GroupUserPermissions = newPermissions; @@ -391,7 +404,7 @@ public class DrawGroupPair : DrawPairBase if (showBars) { ImGui.SetCursorPosY(originalY); - if (_uiSharedService.IconButton(FontAwesomeIcon.Bars)) + if (_uiSharedService.IconButtonCentered(FontAwesomeIcon.Bars)) { ImGui.OpenPopup("Syncshell Flyout Menu"); } diff --git a/MareSynchronos/UI/Components/DrawPairBase.cs b/MareSynchronos/UI/Components/DrawPairBase.cs index cc6c88d..a82aebe 100644 --- a/MareSynchronos/UI/Components/DrawPairBase.cs +++ b/MareSynchronos/UI/Components/DrawPairBase.cs @@ -42,8 +42,8 @@ public abstract class DrawPairBase var menuButtonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Bars); float pauseClusterWidth = Math.Max(pauseButtonSize.X, playButtonSize.X); - float pauseClusterHeight = Math.Max(pauseButtonSize.Y, playButtonSize.Y); - float reservedSpacing = style.ItemSpacing.X * 2.4f; + float pauseClusterHeight = Math.Max(Math.Max(pauseButtonSize.Y, playButtonSize.Y), ImGui.GetFrameHeight()); + float reservedSpacing = style.ItemSpacing.X * 1.6f; float rightButtonWidth = menuButtonSize.X + pauseClusterWidth + @@ -84,11 +84,15 @@ public abstract class DrawPairBase ImGui.SetCursorPos(new Vector2(rowStartCursor.X + padding.X, iconTop)); DrawLeftSide(iconTop, iconTop); - ImGui.SameLine(); - ImGui.SetCursorPosY(textTop); - var posX = ImGui.GetCursorPosX(); + + float leftReserved = GetLeftSideReservedWidth(); + float nameStartX = rowStartCursor.X + padding.X + leftReserved; + var rightSide = DrawRightSide(buttonTop, buttonTop); - DrawName(textTop + padding.Y * 0.15f, posX, rightSide); + + ImGui.SameLine(nameStartX); + ImGui.SetCursorPosY(textTop); + DrawName(textTop + padding.Y * 0.15f, nameStartX, rightSide); ImGui.SetCursorPos(new Vector2(rowStartCursor.X, rowStartCursor.Y + totalHeight)); ImGui.SetCursorPosX(rowStartCursor.X); @@ -100,6 +104,8 @@ public abstract class DrawPairBase protected virtual float GetRightSideExtraWidth() => 0f; + protected virtual float GetLeftSideReservedWidth() => UiSharedService.GetIconSize(FontAwesomeIcon.Moon).X * 2f + ImGui.GetStyle().ItemSpacing.X * 1.5f; + private void DrawName(float originalY, float leftSide, float rightSide) { _displayHandler.DrawPairText(_id, _pair, leftSide, originalY, () => rightSide - leftSide); diff --git a/MareSynchronos/UI/Components/DrawUserPair.cs b/MareSynchronos/UI/Components/DrawUserPair.cs index 6de3e2b..86f62c5 100644 --- a/MareSynchronos/UI/Components/DrawUserPair.cs +++ b/MareSynchronos/UI/Components/DrawUserPair.cs @@ -60,8 +60,28 @@ public class DrawUserPair : DrawPairBase width += _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Running).X + spacingX * 0.5f; } + width += spacingX * 1.2f; return width; } + + protected override float GetLeftSideReservedWidth() + { + var style = ImGui.GetStyle(); + float spacing = style.ItemSpacing.X; + float iconW = UiSharedService.GetIconSize(FontAwesomeIcon.Moon).X; + + int icons = 1; + if (!(_pair.UserPair!.OwnPermissions.IsPaired() && _pair.UserPair!.OtherPermissions.IsPaired())) + icons++; + else if (_pair.UserPair!.OwnPermissions.IsPaused() || _pair.UserPair!.OtherPermissions.IsPaused()) + icons++; + if (_pair.IsOnline && _pair.IsVisible) + icons++; + + float iconsTotal = icons * iconW + Math.Max(0, icons - 1) * spacing; + float cushion = spacing * 0.6f; + return iconsTotal + cushion; + } protected override void DrawLeftSide(float textPosY, float originalY) { @@ -133,7 +153,8 @@ public class DrawUserPair : DrawPairBase var entryUID = _pair.UserData.AliasOrUID; var spacingX = ImGui.GetStyle().ItemSpacing.X; var edgePadding = UiSharedService.GetCardContentPaddingX() + 6f * ImGuiHelpers.GlobalScale; - var windowEndX = ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - edgePadding; + var rightEdgeGap = spacingX * 1.2f; + var windowEndX = ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - edgePadding - rightEdgeGap; var rightSidePos = windowEndX - barButtonSize.X; // Flyout Menu @@ -150,13 +171,12 @@ public class DrawUserPair : DrawPairBase ImGui.EndPopup(); } - // Pause (mutual pairs only) if (_pair.UserPair!.OwnPermissions.IsPaired() && _pair.UserPair!.OtherPermissions.IsPaired()) { rightSidePos -= pauseIconSize.X + spacingX; ImGui.SameLine(rightSidePos); ImGui.SetCursorPosY(originalY); - if (_uiSharedService.IconButton(pauseIcon)) + if (pauseIcon == FontAwesomeIcon.Pause ? _uiSharedService.IconPauseButtonCentered() : _uiSharedService.IconButtonCentered(pauseIcon)) { var perm = _pair.UserPair!.OwnPermissions; perm.SetPaused(!perm.IsPaused()); diff --git a/MareSynchronos/UI/SyncshellAdminUI.cs b/MareSynchronos/UI/SyncshellAdminUI.cs index 58c0559..0365d5a 100644 --- a/MareSynchronos/UI/SyncshellAdminUI.cs +++ b/MareSynchronos/UI/SyncshellAdminUI.cs @@ -41,6 +41,16 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase private bool _autoDetectVisible; private bool _autoDetectPasswordDisabled; private string? _autoDetectMessage; + + private bool _autoDetectDesiredVisibility; + private int _adDurationHours = 2; + private bool _adRecurring = false; + private readonly bool[] _adWeekdays = new bool[7]; + private int _adStartHour = 21; + private int _adStartMinute = 0; + private int _adEndHour = 23; + private int _adEndMinute = 0; + private const string AutoDetectTimeZone = "Europe/Paris"; public SyncshellAdminUI(ILogger logger, MareMediator mediator, ApiController apiController, UiSharedService uiSharedService, PairManager pairManager, SyncshellDiscoveryService syncshellDiscoveryService, @@ -58,6 +68,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase _multiInvites = 30; _pwChangeSuccess = true; _autoDetectVisible = groupFullInfo.AutoDetectVisible; + _autoDetectDesiredVisibility = _autoDetectVisible; _autoDetectPasswordDisabled = groupFullInfo.PasswordTemporarilyDisabled; Mediator.Subscribe(this, OnSyncshellAutoDetectStateChanged); IsOpen = true; @@ -89,6 +100,9 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase ImGui.Separator(); var perm = GroupFullInfo.GroupPermissions; + using var tabColor = ImRaii.PushColor(ImGuiCol.Tab, UiSharedService.AccentColor); + using var tabHoverColor = ImRaii.PushColor(ImGuiCol.TabHovered, UiSharedService.AccentHoverColor); + using var tabActiveColor = ImRaii.PushColor(ImGuiCol.TabActive, UiSharedService.AccentActiveColor); using var tabbar = ImRaii.TabBar("syncshell_tab_" + GroupFullInfo.GID); if (tabbar) @@ -498,26 +512,93 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase UiSharedService.ColorTextWrapped(_autoDetectMessage!, ImGuiColors.DalamudYellow); } - bool desiredVisibility = _autoDetectVisible; using (ImRaii.Disabled(_autoDetectToggleInFlight || _autoDetectStateLoading)) { - if (ImGui.Checkbox("Afficher cette Syncshell dans l'AutoDetect", ref desiredVisibility)) + if (ImGui.Checkbox("Afficher cette Syncshell dans l'AutoDetect", ref _autoDetectDesiredVisibility)) { - _ = ToggleAutoDetectAsync(desiredVisibility); + // Only change local desired state; sending is done via the validate button } } _uiSharedService.DrawHelpText("Quand cette option est activée, le mot de passe devient inactif tant que la visibilité est maintenue."); + if (_autoDetectDesiredVisibility) + { + ImGuiHelpers.ScaledDummy(4); + ImGui.TextUnformatted("Options d'affichage AutoDetect"); + ImGui.Separator(); + + // Recurring toggle first + ImGui.Checkbox("Affichage récurrent", ref _adRecurring); + _uiSharedService.DrawHelpText("Si activé, vous pouvez choisir les jours et une plage horaire récurrents. Si désactivé, seule la durée sera prise en compte."); + + // Duration in hours (only when NOT recurring) + if (!_adRecurring) + { + ImGuiHelpers.ScaledDummy(4); + int duration = _adDurationHours; + ImGui.PushItemWidth(120 * ImGuiHelpers.GlobalScale); + if (ImGui.InputInt("Durée (heures)", ref duration)) + { + _adDurationHours = Math.Clamp(duration, 1, 240); + } + ImGui.PopItemWidth(); + _uiSharedService.DrawHelpText("Combien de temps la Syncshell doit rester visible, en heures."); + } + + ImGuiHelpers.ScaledDummy(4); + if (_adRecurring) + { + ImGuiHelpers.ScaledDummy(4); + ImGui.TextUnformatted("Jours de la semaine actifs :"); + string[] daysFr = new[] { "Lun", "Mar", "Mer", "Jeu", "Ven", "Sam", "Dim" }; + for (int i = 0; i < 7; i++) + { + ImGui.SameLine(i == 0 ? 0 : 0); + bool v = _adWeekdays[i]; + if (ImGui.Checkbox($"##adwd{i}", ref v)) _adWeekdays[i] = v; + ImGui.SameLine(); + ImGui.TextUnformatted(daysFr[i]); + if (i < 6) ImGui.SameLine(); + } + ImGui.NewLine(); + _uiSharedService.DrawHelpText("Sélectionnez les jours où l'affichage est autorisé (ex: jeudi et dimanche)."); + + ImGuiHelpers.ScaledDummy(4); + ImGui.TextUnformatted("Plage horaire (heure locale Europe/Paris) :"); + ImGui.PushItemWidth(60 * ImGuiHelpers.GlobalScale); + ImGui.InputInt("Début heure", ref _adStartHour); ImGui.SameLine(); + ImGui.InputInt("min", ref _adStartMinute); + _adStartHour = Math.Clamp(_adStartHour, 0, 23); + _adStartMinute = Math.Clamp(_adStartMinute, 0, 59); + ImGui.SameLine(); + ImGui.TextUnformatted("→"); ImGui.SameLine(); + ImGui.InputInt("Fin heure", ref _adEndHour); ImGui.SameLine(); + ImGui.InputInt("min ", ref _adEndMinute); + _adEndHour = Math.Clamp(_adEndHour, 0, 23); + _adEndMinute = Math.Clamp(_adEndMinute, 0, 59); + ImGui.PopItemWidth(); + _uiSharedService.DrawHelpText("Exemple : de 21h00 à 23h00. Le fuseau utilisé est Europe/Paris (avec changements été/hiver)."); + } + } + if (_autoDetectPasswordDisabled && _autoDetectVisible) { UiSharedService.ColorTextWrapped("Le mot de passe est actuellement désactivé pendant la visibilité AutoDetect.", ImGuiColors.DalamudYellow); } ImGuiHelpers.ScaledDummy(6); - if (ImGui.Button("Recharger l'état")) + using (ImRaii.Disabled(_autoDetectToggleInFlight || _autoDetectStateLoading)) { - _autoDetectStateLoading = true; - _ = EnsureAutoDetectStateAsync(true); + if (ImGui.Button("Valider et envoyer")) + { + _ = SubmitAutoDetectAsync(); + } + ImGui.SameLine(); + if (ImGui.Button("Recharger l'état")) + { + _autoDetectStateLoading = true; + _ = EnsureAutoDetectStateAsync(true); + } } } @@ -580,6 +661,67 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase } } + private async Task SubmitAutoDetectAsync() + { + if (_autoDetectToggleInFlight) + { + return; + } + + _autoDetectToggleInFlight = true; + _autoDetectMessage = null; + + try + { + // Duration always used when visible + int? duration = _autoDetectDesiredVisibility ? _adDurationHours : null; + + // Scheduling fields only if recurring is enabled + int[]? weekdaysArr = null; + TimeSpan? start = null; + TimeSpan? end = null; + string? tz = null; + if (_autoDetectDesiredVisibility && _adRecurring) + { + List weekdays = new(); + for (int i = 0; i < 7; i++) if (_adWeekdays[i]) weekdays.Add(i); + weekdaysArr = weekdays.Count > 0 ? weekdays.ToArray() : Array.Empty(); + start = new TimeSpan(_adStartHour, _adStartMinute, 0); + end = new TimeSpan(_adEndHour, _adEndMinute, 0); + tz = AutoDetectTimeZone; + } + + var ok = await _syncshellDiscoveryService.SetVisibilityAsync( + GroupFullInfo.GID, + _autoDetectDesiredVisibility, + duration, + weekdaysArr, + start, + end, + tz, + CancellationToken.None).ConfigureAwait(false); + + if (!ok) + { + _autoDetectMessage = "Impossible d'envoyer les paramètres AutoDetect."; + return; + } + + await EnsureAutoDetectStateAsync(true).ConfigureAwait(false); + _autoDetectMessage = _autoDetectDesiredVisibility + ? "Paramètres AutoDetect envoyés. La Syncshell sera visible selon le planning défini." + : "La Syncshell n'est plus visible dans AutoDetect."; + } + catch (Exception ex) + { + _autoDetectMessage = $"Erreur lors de l'envoi des paramètres AutoDetect : {ex.Message}"; + } + finally + { + _autoDetectToggleInFlight = false; + } + } + private void ApplyAutoDetectState(bool visible, bool passwordDisabled, bool fromServer) { _autoDetectVisible = visible; diff --git a/MareSynchronos/UI/UISharedService.cs b/MareSynchronos/UI/UISharedService.cs index d2a7456..8ecbc77 100644 --- a/MareSynchronos/UI/UISharedService.cs +++ b/MareSynchronos/UI/UISharedService.cs @@ -541,6 +541,100 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase return result; } + + public bool IconButtonCentered(FontAwesomeIcon icon, float? height = null, float xOffset = 0f, float yOffset = 0f, bool square = false) + { + string text = icon.ToIconString(); + + ImGui.PushID($"centered-{text}"); + Vector2 glyphSize; + using (IconFont.Push()) + glyphSize = ImGui.CalcTextSize(text); + ImDrawListPtr drawList = ImGui.GetWindowDrawList(); + Vector2 cursorScreenPos = ImGui.GetCursorScreenPos(); + float frameHeight = height ?? ImGui.GetFrameHeight(); + float buttonWidth = square ? frameHeight : glyphSize.X + ImGui.GetStyle().FramePadding.X * 2f; + using var hoverColor = ImRaii.PushColor(ImGuiCol.ButtonHovered, AccentHoverColor); + using var activeColor = ImRaii.PushColor(ImGuiCol.ButtonActive, AccentActiveColor); + bool clicked = ImGui.Button(string.Empty, new Vector2(buttonWidth, frameHeight)); + Vector2 pos = new Vector2( + cursorScreenPos.X + (buttonWidth - glyphSize.X) / 2f + xOffset, + cursorScreenPos.Y + frameHeight / 2f - glyphSize.Y / 2f + yOffset); + using (IconFont.Push()) + drawList.AddText(pos, ImGui.GetColorU32(ImGuiCol.Text), text); + ImGui.PopID(); + + return clicked; + } + public bool IconPauseButtonCentered(float? height = null) + { + ImGui.PushID("centered-pause-custom"); + Vector2 glyphSize; + using (IconFont.Push()) + glyphSize = ImGui.CalcTextSize(FontAwesomeIcon.Pause.ToIconString()); + float frameHeight = height ?? ImGui.GetFrameHeight(); + float buttonWidth = glyphSize.X + ImGui.GetStyle().FramePadding.X * 2f; + + using var hoverColor = ImRaii.PushColor(ImGuiCol.ButtonHovered, AccentHoverColor); + using var activeColor = ImRaii.PushColor(ImGuiCol.ButtonActive, AccentActiveColor); + + var drawList = ImGui.GetWindowDrawList(); + var buttonTopLeft = ImGui.GetCursorScreenPos(); + bool clicked = ImGui.Button(string.Empty, new Vector2(buttonWidth, frameHeight)); + + var textColor = ImGui.GetColorU32(ImGuiCol.Text); + + float h = frameHeight * 0.55f; // bar height + float w = MathF.Max(1f, frameHeight * 0.16f); // bar width + float gap = MathF.Max(1f, w * 0.9f); // gap between bars + float total = 2f * w + gap; + + float startX = buttonTopLeft.X + (buttonWidth - total) / 2f; + float startY = buttonTopLeft.Y + (frameHeight - h) / 2f; + float rounding = w * 0.35f; + + drawList.AddRectFilled(new Vector2(startX, startY), new Vector2(startX + w, startY + h), textColor, rounding); + float rightX = startX + w + gap; + drawList.AddRectFilled(new Vector2(rightX, startY), new Vector2(rightX + w, startY + h), textColor, rounding); + + ImGui.PopID(); + return clicked; + } + + public bool IconPlusButtonCentered(float? height = null) + { + ImGui.PushID("centered-plus-custom"); + Vector2 glyphSize; + using (IconFont.Push()) + glyphSize = ImGui.CalcTextSize(FontAwesomeIcon.Plus.ToIconString()); + float frameHeight = height ?? ImGui.GetFrameHeight(); + float buttonWidth = glyphSize.X + ImGui.GetStyle().FramePadding.X * 2f; + + using var hoverColor = ImRaii.PushColor(ImGuiCol.ButtonHovered, AccentHoverColor); + using var activeColor = ImRaii.PushColor(ImGuiCol.ButtonActive, AccentActiveColor); + + var drawList = ImGui.GetWindowDrawList(); + var buttonTopLeft = ImGui.GetCursorScreenPos(); + bool clicked = ImGui.Button(string.Empty, new Vector2(buttonWidth, frameHeight)); + + var color = ImGui.GetColorU32(ImGuiCol.Text); + + float armThickness = MathF.Max(1f, frameHeight * 0.14f); + float crossSize = frameHeight * 0.55f; // total length of vertical/horizontal arms + float startX = buttonTopLeft.X + (buttonWidth - crossSize) / 2f; + float startY = buttonTopLeft.Y + (frameHeight - crossSize) / 2f; + float endX = startX + crossSize; + float endY = startY + crossSize; + float r = armThickness * 0.35f; + + float hY1 = startY + (crossSize - armThickness) / 2f; + drawList.AddRectFilled(new Vector2(startX, hY1), new Vector2(endX, hY1 + armThickness), color, r); + float vX1 = startX + (crossSize - armThickness) / 2f; + drawList.AddRectFilled(new Vector2(vX1, startY), new Vector2(vX1 + armThickness, endY), color, r); + + ImGui.PopID(); + return clicked; + } private bool IconTextButtonInternal(FontAwesomeIcon icon, string text, Vector4? defaultColor = null, float? width = null, bool useAccentHover = true) {