using System; using System.Collections.Generic; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Dalamud.Game.ClientState.Objects.Types; using HSUI.Config; using HSUI.Enums; using HSUI.Helpers; using Dalamud.Bindings.ImGui; using Dalamud.Interface.Utility; using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI.Misc; using FFXIVClientStructs.FFXIV.Component.GUI; using static FFXIVClientStructs.FFXIV.Client.Game.ActionManager; using LuminaAction = Lumina.Excel.Sheets.Action; namespace HSUI.Interface.GeneralElements { /// Draws one controller cross hotbar (8 slots in cross layout). Visible only when controller hotbars are enabled and trigger state matches. public class CrossBarHud : DraggableHudElement, IHudElementWithActor, IHudElementWithVisibilityConfig { private ControllerBarConfig Config => (ControllerBarConfig)_config; private bool _wasDragging; private uint _dragId; private RaptureHotbarModule.HotbarSlotType _dragSlotType; private int _pendingSlotIconIndex = -1; private uint _pendingSlotIconId; private int _pendingSlotIconFramesLeft; private int _lastFrameDroppedOnSlot = -1; private bool _suppressSlotClicksAfterDragRelease; public IGameObject? Actor { get; set; } public VisibilityConfig VisibilityConfig => Config.VisibilityConfig; public CrossBarHud(ControllerBarConfig config, string displayName) : base(config, displayName) { } protected override (List, List) ChildrenPositionsAndSizes() { var size = BarSize(); return (new List { Config.Position }, new List { size }); } private Vector2 BarSize() { // Two crosses side by side: each cross is 3 cells (slot+pad); gap between crosses float cellW = Config.SlotSize.X + Config.SlotPadding; float cellH = Config.SlotSize.Y + Config.SlotPadding; float gap = Config.SlotPadding * 2; float w = 3 * cellW + gap + 3 * cellW; float h = 3 * cellH; return new Vector2(w, h); } /// Cross layout like the game: left cross (slots 0-3) = top/left/right/bottom around center; right cross (slots 4-7) same. private Vector2 GetSlotPositionCross(int slotIndex, Vector2 slotSize, int pad) { float cellW = slotSize.X + pad; float cellH = slotSize.Y + pad; float leftCrossWidth = 3 * cellW; float gap = pad * 2f; if (slotIndex < 4) { // Left cross: 3x3 grid, slots at (1,0), (0,1), (2,1), (1,2) int col = slotIndex switch { 0 => 1, 1 => 0, 2 => 2, _ => 1 }; int row = slotIndex switch { 0 => 0, 1 => 1, 2 => 1, _ => 2 }; return new Vector2(col * cellW, row * cellH); } else { // Right cross: same 3x3 pattern, offset by left cross + gap int i = slotIndex - 4; int col = i switch { 0 => 1, 1 => 0, 2 => 2, _ => 1 }; int row = i switch { 0 => 0, 1 => 1, 2 => 1, _ => 2 }; return new Vector2(leftCrossWidth + gap + col * cellW, row * cellH); } } private const float DropMargin = 24f; /// Hit test: returns slot index 0-7 if pos is over a cross slot, else -1. private int GetSlotIndexAtPositionCross(Vector2 topLeft, Vector2 slotSize, int pad, Vector2 pos) { Vector2 barSize = new Vector2( 3 * (slotSize.X + pad) + pad * 2f + 3 * (slotSize.X + pad), 3 * (slotSize.Y + pad)); if (pos.X < topLeft.X || pos.X >= topLeft.X + barSize.X || pos.Y < topLeft.Y || pos.Y >= topLeft.Y + barSize.Y) return -1; int best = -1; float bestDistSq = float.MaxValue; for (int i = 0; i < ControllerBarConfig.CrossSlotCount; i++) { var slotOffset = GetSlotPositionCross(i, slotSize, pad); float cx = topLeft.X + slotOffset.X + slotSize.X * 0.5f; float cy = topLeft.Y + slotOffset.Y + slotSize.Y * 0.5f; float dx = pos.X - cx, dy = pos.Y - cy; float slotHalfW = slotSize.X * 0.5f + DropMargin; float slotHalfH = slotSize.Y * 0.5f + DropMargin; if (Math.Abs(dx) <= slotHalfW && Math.Abs(dy) <= slotHalfH) { float distSq = dx * dx + dy * dy; if (distSq < bestDistSq) { bestDistSq = distSq; best = i; } } } return best; } private unsafe void HandleDragDropCross(Vector2 topLeft, Vector2 barSize, Vector2 slotSize, int pad) { if (_pendingSlotIconIndex >= 0 && _pendingSlotIconFramesLeft > 0) { _pendingSlotIconFramesLeft--; if (_pendingSlotIconFramesLeft <= 0) _pendingSlotIconIndex = -1; } else if (_pendingSlotIconIndex >= 0) _pendingSlotIconIndex = -1; var stage = AtkStage.Instance(); if (stage == null) return; var dm = (AtkDragDropManager*)Unsafe.AsPointer(ref stage->DragDropManager); bool dragging = dm->IsDragging; if (dragging) { if (ActionBarsHud.GetHotbarsConfig()?.GeneralOptions?.EnableDragDropFromGame != false && ActionBarsHud.TryGetDragPayload(out var slotType, out var id)) { _dragSlotType = slotType; _dragId = id; _wasDragging = true; var mp = ImGui.GetMousePos(); int idx = GetSlotIndexAtPositionCross(topLeft, slotSize, pad, mp); if (idx >= 0) InputsHelper.SuppressExecuteSlotByIdAfterDrop((uint)(ActionBarsManager.ScratchBarIndex - 1), (uint)ActionBarsManager.ScratchSlotIndex, 500); if (ImGui.IsMouseReleased(ImGuiMouseButton.Left)) { int releaseIdx = GetSlotIndexAtPositionCross(topLeft, slotSize, pad, mp); if (releaseIdx >= 0 && slotType != RaptureHotbarModule.HotbarSlotType.Empty && (id != 0 || slotType == RaptureHotbarModule.HotbarSlotType.Macro || slotType == RaptureHotbarModule.HotbarSlotType.GearSet)) { if (!ImGui.GetIO().KeyShift) { try { ActionBarsManager.Instance?.PlacePayloadOnControllerSlot(Config.HotbarIndex, releaseIdx, slotType, id); uint iconId = ActionBarsHud.GetIconIdForPayload(slotType, id); if (iconId != 0) { _pendingSlotIconIndex = releaseIdx; _pendingSlotIconId = iconId; _pendingSlotIconFramesLeft = 300; } _lastFrameDroppedOnSlot = releaseIdx; InputsHelper.SuppressExecuteSlotByIdAfterDrop((uint)(ActionBarsManager.ScratchBarIndex - 1), (uint)ActionBarsManager.ScratchSlotIndex, 300); if (slotType == RaptureHotbarModule.HotbarSlotType.Action || slotType == RaptureHotbarModule.HotbarSlotType.GeneralAction || slotType == RaptureHotbarModule.HotbarSlotType.CraftAction || slotType == RaptureHotbarModule.HotbarSlotType.PetAction) InputsHelper.SuppressUseActionAfterDrop(id, 300); } catch (Exception ex) { Plugin.Logger.Warning($"[HSUI CrossBar DragDrop] Drop failed: {ex.Message}"); } } try { dm->CancelDragDrop(true, true); } catch { } } else { try { dm->CancelDragDrop(true, true); } catch { } } _wasDragging = false; } } else { _dragSlotType = RaptureHotbarModule.HotbarSlotType.Empty; _dragId = 0; _wasDragging = true; if (ImGui.IsMouseReleased(ImGuiMouseButton.Left)) { try { dm->CancelDragDrop(true, true); } catch { } _wasDragging = false; } } return; } if (_wasDragging) { _suppressSlotClicksAfterDragRelease = true; var mp = ImGui.GetMousePos(); int idx = GetSlotIndexAtPositionCross(topLeft, slotSize, pad, mp); if (idx >= 0) { var slotType = _dragSlotType; var id = _dragId; bool hasValidPayload = slotType != RaptureHotbarModule.HotbarSlotType.Empty && (id != 0 || slotType == RaptureHotbarModule.HotbarSlotType.Macro || slotType == RaptureHotbarModule.HotbarSlotType.GearSet); if (hasValidPayload) { if (ImGui.GetIO().KeyShift) { try { dm->CancelDragDrop(true, true); } catch { } ActionBarsManager.Instance?.ClearControllerSlot(Config.HotbarIndex, idx); _lastFrameDroppedOnSlot = idx; _wasDragging = false; return; } try { ActionBarsManager.Instance?.PlacePayloadOnControllerSlot(Config.HotbarIndex, idx, slotType, id); try { dm->CancelDragDrop(true, true); } catch { } uint iconId = ActionBarsHud.GetIconIdForPayload(slotType, id); if (iconId != 0) { _pendingSlotIconIndex = idx; _pendingSlotIconId = iconId; _pendingSlotIconFramesLeft = 300; } _lastFrameDroppedOnSlot = idx; InputsHelper.SuppressExecuteSlotByIdAfterDrop((uint)(ActionBarsManager.ScratchBarIndex - 1), (uint)ActionBarsManager.ScratchSlotIndex, 300); if (slotType == RaptureHotbarModule.HotbarSlotType.Action || slotType == RaptureHotbarModule.HotbarSlotType.GeneralAction || slotType == RaptureHotbarModule.HotbarSlotType.CraftAction || slotType == RaptureHotbarModule.HotbarSlotType.PetAction) InputsHelper.SuppressUseActionAfterDrop(id, 300); } catch (Exception ex) { Plugin.Logger.Warning($"[HSUI CrossBar DragDrop] Drop failed: {ex.Message}"); } } } _wasDragging = false; } } public override void DrawChildren(Vector2 origin) { if (!ControllerHotbarsConfig.GetEffectiveControllerHotbarsEnabled() || Actor == null) return; if (ControllerHotbarHelper.GetCurrentTrigger() != Config.Trigger) return; if (ActionBarsManager.Instance == null) return; _lastFrameDroppedOnSlot = -1; _suppressSlotClicksAfterDragRelease = false; var barSize = BarSize(); var topLeft = Utils.GetAnchoredPosition(origin + Config.Position, barSize, Config.Anchor); var slotSize = Config.SlotSize; int pad = Config.SlotPadding; bool showCd = Config.ShowCooldownOverlay; bool showCdNumbers = Config.ShowCooldownNumbers; bool showBorder = Config.ShowBorder; AddDrawAction(Config.StrataLevel, () => { HandleDragDropCross(topLeft, barSize, slotSize, pad); var slots = ActionBarsManager.Instance.GetControllerSlotData(Config.HotbarIndex, ControllerBarConfig.CrossSlotCount); void DrawBarContent(ImDrawListPtr drawList) { using (FontsManager.Instance.PushFont(FontsConfig.DefaultSmallFontKey)) { for (int i = 0; i < slots.Count; i++) { var slot = slots[i]; uint displayIconId = slot.IconId; bool isEmpty = slot.IsEmpty; bool usePending = _pendingSlotIconIndex == i && _pendingSlotIconId != 0; if (usePending) { displayIconId = _pendingSlotIconId; isEmpty = false; } var slotOffset = GetSlotPositionCross(i, slotSize, pad); var pos = topLeft + slotOffset; var size = new Vector2(slotSize.X, slotSize.Y); bool hovered = !isEmpty && ImGui.IsMouseHoveringRect(pos, pos + size); if (isEmpty) { drawList.AddRectFilled(pos, pos + size, 0x22000000); if (showBorder) drawList.AddRect(pos, pos + size, 0xFF444444); } else { uint color = hovered ? 0xFFFFFFFF : (slot.IsUsable ? 0xFFFFFFFF : 0xAA888888); bool isComboNext = Config.ShowComboHighlight && IsComboNextAction(slot); DrawHelper.DrawIcon(displayIconId, pos, size, showBorder && !hovered && !isComboNext, color, drawList); if (hovered && showBorder) drawList.AddRect(pos, pos + size, 0xFF88CCFF, 0, ImDrawFlags.None, 2); else if (isComboNext) { var ch = Config.ComboHighlightConfig; DrawHelper.DrawComboHighlightRect(drawList, pos, pos + size, ch.Color.Base, ch.Thickness, ch.ShowGlow, ch.LineStyle); } if (showCd && slot.CooldownPercent > 0 && _pendingSlotIconIndex != i) { // Skip cooldown overlay when slot overlaps game UI (e.g. dialogue box) // to avoid hotbar cooldowns showing through during "Show HUD during dialogue" if (!ClipRectsHelper.Instance.SlotOverlapsGameAddon(pos, size)) { float total = 100f; float elapsed = total - slot.CooldownPercent; DrawHelper.DrawIconCooldown(pos, size, elapsed, total, drawList); if (showCdNumbers && slot.CooldownSecondsLeft > 0) { string cdText = slot.CooldownSecondsLeft.ToString(); Vector2 textSize = ImGui.CalcTextSize(cdText); Vector2 textPos = pos + (size - textSize) * 0.5f; DrawHelper.DrawOutlinedText(cdText, textPos, 0xFFFFFFFF, 0xFF000000, drawList, 1); } } } if (Config.ShowKeypressFlash) { float flashAlpha = InputsHelper.GetKeypressFlashAlpha(Config.HotbarIndex, i, 0.2); if (flashAlpha > 0.001f) { byte a = (byte)(flashAlpha * 0.5f * 255); drawList.AddRectFilled(pos, pos + size, (uint)(a << 24 | 0xFFFFFF)); } } } if (Config.ShowSlotNumbers && !slot.IsEmpty) { string label = !string.IsNullOrWhiteSpace(slot.KeybindHint) ? slot.KeybindHint : ActionBarsManager.GetDefaultKeybindLabel(Config.HotbarIndex, i); Vector2 labelSize = ImGui.CalcTextSize(label); Vector2 labelPos = new Vector2(pos.X + size.X - labelSize.X - 2, pos.Y + size.Y - labelSize.Y - 2); DrawHelper.DrawOutlinedText(label, labelPos, 0xFFFFFFFF, 0xFF000000, drawList, 1); } if (Config.ShowChargeCount && !slot.IsEmpty && slot.MaxCharges > 1) { string chargeText = slot.CurrentCharges.ToString(); Vector2 chargeSize = ImGui.CalcTextSize(chargeText); Vector2 chargePos = new Vector2(pos.X + 2, pos.Y + 2); DrawHelper.DrawOutlinedText(chargeText, chargePos, 0xFFFFFFFF, 0xFF000000, drawList, 1); } } } } DrawHelper.DrawInWindow(ID + "_crossbar", topLeft, barSize, false, DrawBarContent); }); // Overlay for click, drag-drop and tooltip AddDrawAction(StrataLevel.HIGHEST, () => { var slots = ActionBarsManager.Instance.GetControllerSlotData(Config.HotbarIndex, ControllerBarConfig.CrossSlotCount); var flags = ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoBringToFrontOnFocus | ImGuiWindowFlags.NoNav; string overlayId = $"HSUI_CrossBar{Config.HotbarIndex}_input"; ImGui.SetCursorScreenPos(topLeft); if (!ImGui.BeginChild(overlayId, barSize, false, flags)) { ImGui.EndChild(); return; } int frame = ImGui.GetFrameCount(); if (frame != ActionBarsHud._lastOverlayFrame) { ActionBarsHud._lastOverlayFrame = frame; ActionBarsHud._anyHotbarAcceptedDrop = false; if (ActionBarsHud._pendingReleaseOutsideSlotId >= 0 && ActionBarsHud.GetHotbarsConfig()?.GeneralOptions?.EnableReleaseOutsideToClear != false) { var (bar, slot) = ActionBarsHud.FromSlotId(ActionBarsHud._pendingReleaseOutsideSlotId); if (bar >= 1 && bar <= 8 && slot >= 0 && slot < ControllerBarConfig.CrossSlotCount) ActionBarsManager.Instance?.ClearControllerSlot(bar, slot); ActionBarsHud._pendingReleaseOutsideSlotId = -1; } } bool acceptedDrop = false; for (int i = 0; i < ControllerBarConfig.CrossSlotCount; i++) { var slotOffset = GetSlotPositionCross(i, slotSize, pad); var slotPos = topLeft + slotOffset; ImGui.SetCursorScreenPos(slotPos); bool clicked = ImGui.InvisibleButton($"##{overlayId}_slot_{i}", slotSize); bool isHovered = ImGui.IsItemHovered(); bool isActive = ImGui.IsItemActive(); bool shiftHeld = ImGui.GetIO().KeyShift; int targetSlotId = ActionBarsHud.ToSlotId(Config.HotbarIndex, i); if (ActionBarsHud.GetHotbarsConfig()?.GeneralOptions?.EnableShiftDragToRearrange != false && ImGui.BeginDragDropTarget()) { var payload = ImGui.AcceptDragDropPayload(ActionBarsHud.SlotPayloadType); if (payload != ImGuiPayloadPtr.Null && payload.IsDataType(ActionBarsHud.SlotPayloadType) && ActionBarsHud.TryGetSlotPayload(payload, out var src)) { acceptedDrop = true; ActionBarsHud._anyHotbarAcceptedDrop = true; ActionBarsHud._imGuiDragSourceSlotId = -1; ActionBarsHud._pendingReleaseOutsideSlotId = -1; ActionBarsHud._lastLoggedPickupSlotId = -1; if (src.SlotId != targetSlotId) { var (srcBar, srcSlot) = ActionBarsHud.FromSlotId(src.SlotId); if (srcBar >= 1 && srcSlot >= 0 && srcBar <= 8 && srcSlot < ControllerBarConfig.CrossSlotCount) ActionBarsManager.Instance?.SwapControllerSlots(srcBar, srcSlot, Config.HotbarIndex, i); } } ImGui.EndDragDropTarget(); } if (ActionBarsHud.GetHotbarsConfig()?.GeneralOptions?.EnableShiftDragToRearrange != false && !ActionBarsHud.IsGameDragging() && shiftHeld && i < slots.Count && !slots[i].IsEmpty && isActive && ImGui.IsMouseDragging(ImGuiMouseButton.Left)) { if (ImGui.BeginDragDropSource(ImGuiDragDropFlags.None)) { ActionBarsHud._imGuiDragSourceSlotId = targetSlotId; ActionBarsHud.SetSlotPayload(new ActionBarsHud.SlotDragPayload { SlotId = targetSlotId }); var icon = TexturesHelper.GetTextureFromIconId(slots[i].IconId); if (icon != null) ImGui.Image(icon.Handle, new Vector2(32, 32)); ImGui.EndDragDropSource(); } } if (clicked && !acceptedDrop && !ImGui.IsMouseDragging(ImGuiMouseButton.Left) && i < slots.Count && !slots[i].IsEmpty) { if (_suppressSlotClicksAfterDragRelease || i == _lastFrameDroppedOnSlot) { _suppressSlotClicksAfterDragRelease = false; _lastFrameDroppedOnSlot = -1; } else if (shiftHeld) { ActionBarsManager.Instance?.ClearControllerSlot(Config.HotbarIndex, i); } else { ActionBarsManager.Instance?.ExecuteControllerSlot(Config.HotbarIndex, i); } } if (Config.ShowTooltips && isHovered && i < slots.Count) { var slot = slots[i]; if (!slot.IsEmpty) { var (title, text) = ActionBarsHud.GetSlotTooltipPublic(slot); if (!string.IsNullOrEmpty(title) || !string.IsNullOrEmpty(text)) { string body = string.IsNullOrEmpty(text) ? title : text; var tooltipConfig = ConfigurationManager.Instance?.GetConfigObject(); var formatted = tooltipConfig != null ? TooltipsHelper.BuildFormattedActionTooltipBody(body, tooltipConfig) : null; TooltipsHelper.Instance.ShowTooltipOnCursor(body, title, slot.ActionId, "", slot.IconId > 0 ? slot.IconId : null, TooltipIdKind.Action, formatted); } } } } if (ActionBarsHud.GetHotbarsConfig()?.GeneralOptions?.EnableReleaseOutsideToClear != false && ImGui.GetIO().KeyShift && ActionBarsHud._imGuiDragSourceSlotId >= 0 && ImGui.IsMouseReleased(ImGuiMouseButton.Left) && !ActionBarsHud._anyHotbarAcceptedDrop) { ActionBarsHud._pendingReleaseOutsideSlotId = ActionBarsHud._imGuiDragSourceSlotId; ActionBarsHud._imGuiDragSourceSlotId = -1; ActionBarsHud._lastLoggedPickupSlotId = -1; } ImGui.EndChild(); }); } private static bool IsComboNextAction(ActionBarsManager.SlotInfo slot) { if (slot.IsEmpty || slot.ActionId == 0) return false; try { var slotType = slot.SlotType; var actionType = slotType switch { RaptureHotbarModule.HotbarSlotType.Action => ActionType.Action, RaptureHotbarModule.HotbarSlotType.GeneralAction => ActionType.GeneralAction, RaptureHotbarModule.HotbarSlotType.CraftAction => ActionType.CraftAction, RaptureHotbarModule.HotbarSlotType.PetAction => ActionType.PetAction, _ => (ActionType?)null }; if (!actionType.HasValue) return false; return SpellHelper.Instance.IsActionHighlighted(slot.ActionId, actionType.Value); } catch { return false; } } } }