using System.Numerics; using System.Text; using ChatTwo.Code; using ChatTwo.GameFunctions.Types; using ChatTwo.Resources; using Dalamud.Game.ClientState.Keys; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Interface; using Dalamud.Interface.Components; using Dalamud.Interface.FontIdentifier; using Dalamud.Interface.ImGuiFontChooserDialog; using Dalamud.Interface.Style; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Bindings.ImGui; namespace ChatTwo.Util; internal static class ImGuiUtil { private static Plugin Plugin = null!; public static void Initialize(Plugin plugin) { Plugin = plugin; } private static readonly ImGuiMouseButton[] Buttons = [ ImGuiMouseButton.Left, ImGuiMouseButton.Middle, ImGuiMouseButton.Right ]; private static Payload? Hovered; private static Payload? LastLink; private static readonly List<(Vector2, Vector2)> PayloadBounds = []; internal static void PostPayload(Chunk chunk, PayloadHandler? handler) { var payload = chunk.Link; if (payload != null && ImGui.IsItemHovered()) { Hovered = payload; ImGui.SetMouseCursor(ImGuiMouseCursor.Hand); handler?.Hover(payload); } else if (!ReferenceEquals(Hovered, payload)) { Hovered = null; } if (handler == null) return; foreach (var button in Buttons) if (ImGui.IsItemClicked(button)) handler.Click(chunk, payload, button); } internal static unsafe void WrapText(string csText, Chunk chunk, PayloadHandler? handler, Vector4 defaultText, float lineWidth) { void Text(byte* text, byte* textEnd) { var oldPos = ImGui.GetCursorScreenPos(); ImGuiNative.TextUnformatted(text, textEnd); PostPayload(chunk, handler); if (!ReferenceEquals(LastLink, chunk.Link)) PayloadBounds.Clear(); LastLink = chunk.Link; if (Hovered != null && ReferenceEquals(Hovered, chunk.Link)) { defaultText.W = 0.25f; var actualCol = ColourUtil.Vector4ToAbgr(defaultText); ImGui.GetWindowDrawList().AddRectFilled(oldPos, oldPos + ImGui.GetItemRectSize(), actualCol); foreach (var (start, size) in PayloadBounds) ImGui.GetWindowDrawList().AddRectFilled(start, start + size, actualCol); PayloadBounds.Clear(); } if (Hovered == null && chunk.Link != null) PayloadBounds.Add((oldPos, ImGui.GetItemRectSize())); } if (csText.Length == 0) return; foreach (var part in csText.Split(["\r\n", "\r", "\n"], StringSplitOptions.None)) { var bytes = Encoding.UTF8.GetBytes(part); fixed (byte* rawText = bytes) { var text = rawText; var textEnd = text + bytes.Length; // empty string if (text == null) { ImGui.TextUnformatted(""); continue; } var widthLeft = ImGui.GetContentRegionAvail().X; var endPrevLine = ImGuiNative.CalcWordWrapPositionA(ImGui.GetFont().Handle, ImGuiHelpers.GlobalScale, text, textEnd, widthLeft); if (endPrevLine == null) continue; var firstSpace = FindFirstSpace(text, textEnd); var properBreak = firstSpace <= endPrevLine; if (properBreak) { Text(text, endPrevLine); } else { if (lineWidth == 0f) { ImGui.TextUnformatted(""); } else { // check if the next bit is longer than the entire line width var wrapPos = ImGuiNative.CalcWordWrapPositionA(ImGui.GetFont().Handle, ImGuiHelpers.GlobalScale, text, firstSpace, lineWidth); // only go to next line is it's going to wrap at the space if (wrapPos >= firstSpace) ImGui.TextUnformatted(""); } } widthLeft = ImGui.GetContentRegionAvail().X; while (endPrevLine < textEnd) { if (properBreak) text = endPrevLine; // skip a space at start of line if (*text == ' ') ++text; var newEnd = ImGuiNative.CalcWordWrapPositionA(ImGui.GetFont().Handle, ImGuiHelpers.GlobalScale, text, textEnd, widthLeft); if (properBreak && newEnd == endPrevLine) break; endPrevLine = newEnd; if (endPrevLine == null) { ImGui.TextUnformatted(""); ImGui.TextUnformatted(""); break; } Text(text, endPrevLine); if (!properBreak) { properBreak = true; widthLeft = ImGui.GetContentRegionAvail().X; } } } } } private static unsafe byte* FindFirstSpace(byte* text, byte* textEnd) { for (var i = text; i < textEnd; i++) if (char.IsWhiteSpace((char) *i)) return i; return textEnd; } internal static bool IconButton(FontAwesomeIcon icon, string? id = null, string? tooltip = null, int width = 0) { var label = icon.ToIconString(); if (id != null) label += $"##{id}"; bool ret; using (Plugin.FontManager.FontAwesome.Push()) { var size = Vector2.Zero; if (width > 0) size.X = width - 2 * ImGui.GetStyle().CellPadding.X; ret = ImGui.Button(label, size); } if (!string.IsNullOrEmpty(tooltip) && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) Tooltip(tooltip); return ret; } internal static bool OptionCheckbox(ref bool value, string label, string? description = null) { var ret = ImGui.Checkbox(label, ref value); if (!string.IsNullOrEmpty(description)) HelpText(description); return ret; } internal static void HelpText(string text) { using (ImRaii.TextWrapPos(0.0f)) using (ImRaii.PushColor(ImGuiCol.Text, ImGui.GetStyle().Colors[(int) ImGuiCol.TextDisabled])) ImGui.TextUnformatted(text); } // Hellion Chat — compact help affordance: a dimmed "(?)" glyph rendered // on the same line as the previous item, with the long-form description // tucked into a hover tooltip. Lets us keep the settings panes scannable // instead of stacking a wall of HelpText paragraphs under every option. internal static void HelpMarker(string description) { ImGui.SameLine(); using (ImRaii.PushColor(ImGuiCol.Text, ImGui.GetStyle().Colors[(int) ImGuiCol.TextDisabled])) ImGui.TextUnformatted("(?)"); // AllowWhenDisabled — ohne das Flag liefert IsItemHovered bei // ausgegrauten Settings false, der User könnte nicht mehr lesen // warum eine Option nicht aktiv ist. Genau dann braucht er den // Hover-Tooltip aber am dringendsten. if (!ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) return; using var tooltip = ImRaii.Tooltip(); using (ImRaii.TextWrapPos(35.0f * ImGui.GetFontSize())) ImGui.TextUnformatted(description); } internal static void WarningText(string text, bool wrap = true) { var style = StyleModel.GetConfiguredStyle() ?? StyleModel.GetFromCurrent(); var dalamudOrange = style.BuiltInColors?.DalamudOrange; using (ImRaii.TextWrapPos(wrap ? 0.0f : ImGui.GetFontSize() * 35.0f)) using (ImRaii.PushColor(ImGuiCol.Text, dalamudOrange ?? Vector4.Zero, dalamudOrange != null)) ImGui.TextUnformatted(text); } internal static ImRaii.ComboDisposable BeginComboVertical(string label, string previewValue, ImGuiComboFlags flags = ImGuiComboFlags.None) { ImGui.TextUnformatted(label); ImGui.SetNextItemWidth(-1); return ImRaii.Combo($"##{label}", previewValue, flags); } internal static bool DragFloatVertical(string label, ref float value, float vSpeed = 1.0f, float vMin = float.MinValue, float vMax = float.MaxValue, string? format = null, ImGuiSliderFlags flags = ImGuiSliderFlags.None) { ImGui.TextUnformatted(label); ImGui.SetNextItemWidth(-1); return ImGui.DragFloat($"##{label}", ref value, vSpeed, vMin, vMax, format, flags); } internal static bool DragFloatVertical(string label, string description, ref float value, float vSpeed = 1.0f, float vMin = float.MinValue, float vMax = float.MaxValue, string? format = null, ImGuiSliderFlags flags = ImGuiSliderFlags.None) { ImGui.TextUnformatted(label); ImGui.SetNextItemWidth(-1); var r = ImGui.DragFloat($"##{label}", ref value, vSpeed, vMin, vMax, format, flags); HelpText(description); return r; } internal static bool InputIntVertical(string label, string description, ref int value, int step = 1, int stepFast = 100, ImGuiInputTextFlags flags = ImGuiInputTextFlags.None) { ImGui.TextUnformatted(label); ImGui.SetNextItemWidth(-1); var r = ImGui.InputInt($"##{label}", ref value, step, stepFast, flags: flags); HelpText(description); return r; } internal static void Tooltip(string tooltip) { using (ImRaii.Tooltip()) using (ImRaii.TextWrapPos(ImGui.GetFontSize() * 35.0f)) ImGui.TextUnformatted(tooltip); } public static SingleFontChooserDialog? FontChooser(string label, SingleFontSpec font, bool checkbox, ref bool checkboxValue, Predicate? exclusion = null, string? preview = null) { using var id = ImRaii.PushId(label); ImGui.TextUnformatted(label); if (checkbox) { ImGui.Checkbox("##enabled", ref checkboxValue); ImGui.SameLine(); } var fontFamily = font.FontId.Family.EnglishName; var fontStyle = font.FontId.EnglishName; fontStyle = fontStyle.Equals(fontFamily) ? "" : $" - {fontStyle}"; var buttonText = $"{fontFamily}{fontStyle} ({font.SizePt}pt)"; if (!ImGui.Button($"{buttonText}##{label}")) return null; var chooser = SingleFontChooserDialog.CreateAuto((UiBuilder) Plugin.Interface.UiBuilder); chooser.SelectedFont = font; if (exclusion is not null) chooser.FontFamilyExcludeFilter = exclusion; if (preview is not null) chooser.PreviewText = preview; return chooser; } public static void FontSizeCombo(string label, ref float currentSize) { ImGui.TextUnformatted(label); ImGui.SetNextItemWidth(-1); using var combo = ImRaii.Combo($"##{label}", $"{currentSize:###.##}pt"); if (!combo.Success) return; foreach (var size in FontManager.AxisFontSizeList) if (ImGui.Selectable($"{size:###.##}pt", currentSize.Equals(size))) currentSize = size; } public static bool Button(string id, FontAwesomeIcon icon, bool disabled) { using (ImRaii.Disabled(disabled)) return ImGuiComponents.IconButton(id, icon); } internal static bool CtrlShiftButton(string label, string tooltip = "") { var ctrlShiftHeld = ImGui.GetIO() is { KeyCtrl: true, KeyShift: true }; bool ret; using (ImRaii.Disabled(!ctrlShiftHeld)) ret = ImGui.Button(label) && ctrlShiftHeld; if (tooltip.Length != 0 && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) Tooltip(tooltip); return ret; } internal static void KeybindInput(string id, ref ConfigKeyBind? keybind) { var idUint = ImGui.GetID(id); using var pushedId = ImRaii.PushId(id); if (ImGui.GetStateStorage().GetBool(idUint)) { var io = ImGui.GetIO(); var currentMods = ModifierFlag.None; var modString = ""; if (io.KeyCtrl) { currentMods |= ModifierFlag.Ctrl; modString += Language.Keybind_Modifier_Ctrl + " + "; } if (io.KeyShift) { currentMods |= ModifierFlag.Shift; modString += Language.Keybind_Modifier_Shift + " + "; } if (io.KeyAlt) { currentMods |= ModifierFlag.Alt; modString += Language.Keybind_Modifier_Alt + " + "; } var text = $"{modString}... ({Language.Keybind_EscToClear})"; using (ImRaii.PushColor(ImGuiCol.TextSelectedBg, Vector4.Zero)) { ImGui.SetKeyboardFocusHere(); ImGui.InputText(id + "##keybind", ref text, 0, ImGuiInputTextFlags.ReadOnly); } if (ImGui.IsKeyPressed(ImGuiKey.Escape)) { keybind = null; ImGui.GetStateStorage().SetBool(idUint, false); return; } foreach (var vk in Enum.GetValues()) { if (vk is VirtualKey.NO_KEY or VirtualKey.CONTROL or VirtualKey.LCONTROL or VirtualKey.RCONTROL or VirtualKey.SHIFT or VirtualKey.LSHIFT or VirtualKey.RSHIFT or VirtualKey.MENU or VirtualKey.LMENU or VirtualKey.RMENU) continue; if (!vk.TryToImGui(out var imKey) || !ImGui.IsKeyPressed(imKey)) continue; keybind = new ConfigKeyBind { Modifier = currentMods, Key = vk }; ImGui.GetStateStorage().SetBool(idUint, false); return; } } else { var text = $"({Language.Keybind_None})"; if (keybind != null) text = keybind.ToString(); if (ImGui.Button(text, new Vector2(-1, 0))) ImGui.GetStateStorage().SetBool(idUint, true); } } public static void DrawArrows(ref int selected, int min, int max, float spacing, int id = 0, string? tooltipLeft = null, string? tooltipRight = null) { // Prevents changing values from triggering EndDisable var isMin = selected == min; var isMax = selected == max; ImGui.SameLine(0, spacing); using (ImRaii.Disabled(isMin)) { if (IconButton(FontAwesomeIcon.ArrowLeft, id.ToString())) selected--; } if (tooltipLeft != null && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) ImGui.SetTooltip(tooltipLeft); ImGui.SameLine(0, spacing); using (ImRaii.Disabled(isMax)) { if (IconButton(FontAwesomeIcon.ArrowRight, id+1.ToString())) selected++; } if (tooltipRight != null && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) ImGui.SetTooltip(tooltipRight); } public static void WrappedTextWithColor(Vector4 color, string text) { using (ImRaii.PushColor(ImGuiCol.Text, color)) ImGui.TextWrapped(text); } public static void CenterText(string text, float indent = 0.0f) { indent *= ImGuiHelpers.GlobalScale; ImGui.SameLine(((ImGui.GetContentRegionAvail().X - ImGui.CalcTextSize(text).X) * 0.5f) + indent); ImGui.TextUnformatted(text); } internal static bool TryToImGui(this VirtualKey key, out ImGuiKey result) { result = key switch { VirtualKey.NO_KEY => ImGuiKey.None, VirtualKey.BACK => ImGuiKey.Backspace, VirtualKey.TAB => ImGuiKey.Tab, VirtualKey.RETURN => ImGuiKey.Enter, VirtualKey.SHIFT => ImGuiKey.ModShift, VirtualKey.CONTROL => ImGuiKey.ModCtrl, VirtualKey.MENU => ImGuiKey.ModAlt, VirtualKey.PAUSE => ImGuiKey.Pause, VirtualKey.CAPITAL => ImGuiKey.CapsLock, VirtualKey.ESCAPE => ImGuiKey.Escape, VirtualKey.SPACE => ImGuiKey.Space, VirtualKey.PRIOR => ImGuiKey.PageUp, VirtualKey.NEXT => ImGuiKey.PageDown, VirtualKey.END => ImGuiKey.End, VirtualKey.HOME => ImGuiKey.Home, VirtualKey.LEFT => ImGuiKey.LeftArrow, VirtualKey.UP => ImGuiKey.UpArrow, VirtualKey.RIGHT => ImGuiKey.RightArrow, VirtualKey.DOWN => ImGuiKey.DownArrow, VirtualKey.SNAPSHOT => ImGuiKey.PrintScreen, VirtualKey.INSERT => ImGuiKey.Insert, VirtualKey.DELETE => ImGuiKey.Delete, VirtualKey.KEY_0 => ImGuiKey.Key0, VirtualKey.KEY_1 => ImGuiKey.Key1, VirtualKey.KEY_2 => ImGuiKey.Key2, VirtualKey.KEY_3 => ImGuiKey.Key3, VirtualKey.KEY_4 => ImGuiKey.Key4, VirtualKey.KEY_5 => ImGuiKey.Key5, VirtualKey.KEY_6 => ImGuiKey.Key6, VirtualKey.KEY_7 => ImGuiKey.Key7, VirtualKey.KEY_8 => ImGuiKey.Key8, VirtualKey.KEY_9 => ImGuiKey.Key9, VirtualKey.A => ImGuiKey.A, VirtualKey.B => ImGuiKey.B, VirtualKey.C => ImGuiKey.C, VirtualKey.D => ImGuiKey.D, VirtualKey.E => ImGuiKey.E, VirtualKey.F => ImGuiKey.F, VirtualKey.G => ImGuiKey.G, VirtualKey.H => ImGuiKey.H, VirtualKey.I => ImGuiKey.I, VirtualKey.J => ImGuiKey.J, VirtualKey.K => ImGuiKey.K, VirtualKey.L => ImGuiKey.L, VirtualKey.M => ImGuiKey.M, VirtualKey.N => ImGuiKey.N, VirtualKey.O => ImGuiKey.O, VirtualKey.P => ImGuiKey.P, VirtualKey.Q => ImGuiKey.Q, VirtualKey.R => ImGuiKey.R, VirtualKey.S => ImGuiKey.S, VirtualKey.T => ImGuiKey.T, VirtualKey.U => ImGuiKey.U, VirtualKey.V => ImGuiKey.V, VirtualKey.W => ImGuiKey.W, VirtualKey.X => ImGuiKey.X, VirtualKey.Y => ImGuiKey.Y, VirtualKey.Z => ImGuiKey.Z, VirtualKey.LWIN => ImGuiKey.LeftSuper, VirtualKey.RWIN => ImGuiKey.RightSuper, VirtualKey.NUMPAD0 => ImGuiKey.Keypad0, VirtualKey.NUMPAD1 => ImGuiKey.Keypad1, VirtualKey.NUMPAD2 => ImGuiKey.Keypad2, VirtualKey.NUMPAD3 => ImGuiKey.Keypad3, VirtualKey.NUMPAD4 => ImGuiKey.Keypad4, VirtualKey.NUMPAD5 => ImGuiKey.Keypad5, VirtualKey.NUMPAD6 => ImGuiKey.Keypad6, VirtualKey.NUMPAD7 => ImGuiKey.Keypad7, VirtualKey.NUMPAD8 => ImGuiKey.Keypad8, VirtualKey.NUMPAD9 => ImGuiKey.Keypad9, VirtualKey.MULTIPLY => ImGuiKey.KeypadMultiply, VirtualKey.ADD => ImGuiKey.KeypadAdd, VirtualKey.SUBTRACT => ImGuiKey.KeypadSubtract, VirtualKey.DECIMAL => ImGuiKey.KeypadDecimal, VirtualKey.DIVIDE => ImGuiKey.KeypadDivide, VirtualKey.F1 => ImGuiKey.F1, VirtualKey.F2 => ImGuiKey.F2, VirtualKey.F3 => ImGuiKey.F3, VirtualKey.F4 => ImGuiKey.F4, VirtualKey.F5 => ImGuiKey.F5, VirtualKey.F6 => ImGuiKey.F6, VirtualKey.F7 => ImGuiKey.F7, VirtualKey.F8 => ImGuiKey.F8, VirtualKey.F9 => ImGuiKey.F9, VirtualKey.F10 => ImGuiKey.F10, VirtualKey.F11 => ImGuiKey.F11, VirtualKey.F12 => ImGuiKey.F12, VirtualKey.NUMLOCK => ImGuiKey.NumLock, VirtualKey.SCROLL => ImGuiKey.ScrollLock, VirtualKey.OEM_NEC_EQUAL => ImGuiKey.KeypadEqual, VirtualKey.LSHIFT => ImGuiKey.LeftShift, VirtualKey.RSHIFT => ImGuiKey.RightShift, VirtualKey.LCONTROL => ImGuiKey.LeftCtrl, VirtualKey.RCONTROL => ImGuiKey.RightCtrl, VirtualKey.LMENU => ImGuiKey.LeftAlt, VirtualKey.RMENU => ImGuiKey.RightAlt, VirtualKey.OEM_1 => ImGuiKey.Semicolon, VirtualKey.OEM_PLUS => ImGuiKey.Equal, VirtualKey.OEM_COMMA => ImGuiKey.Comma, VirtualKey.OEM_MINUS => ImGuiKey.Minus, VirtualKey.OEM_PERIOD => ImGuiKey.Period, VirtualKey.OEM_2 => ImGuiKey.Slash, VirtualKey.OEM_3 => ImGuiKey.GraveAccent, VirtualKey.OEM_4 => ImGuiKey.LeftBracket, VirtualKey.OEM_5 => ImGuiKey.Backslash, VirtualKey.OEM_6 => ImGuiKey.RightBracket, VirtualKey.OEM_7 => ImGuiKey.Apostrophe, _ => 0, }; return result != 0 || key == VirtualKey.NO_KEY; } public static void ChannelSelector(string headerText, Dictionary chatCodes) { var spacing = 3.0f * ImGuiHelpers.GlobalScale; using var channelNode = ImRaii.TreeNode(headerText); if (!channelNode.Success) return; foreach (var (header, types) in ChatTypeExt.SortOrder) { using var pushedId = ImRaii.PushId(header); if (ImGuiComponents.IconButton(FontAwesomeIcon.Check)) { foreach (var type in types) chatCodes.TryAdd(type, (ChatSourceExt.All, ChatSourceExt.All)); } if (ImGui.IsItemHovered()) ImGui.SetTooltip(Language.ChannelSelector_Select); ImGui.SameLine(0, spacing); if (ImGuiComponents.IconButton(FontAwesomeIcon.Times)) { foreach (var type in types) chatCodes.Remove(type); } if (ImGui.IsItemHovered()) ImGui.SetTooltip(Language.ChannelSelector_Unselect); ImGui.SameLine(0, spacing); using var headerNode = ImRaii.TreeNode(header); if (!headerNode.Success) continue; foreach (var type in types) { if (type.IsGm()) continue; var enabled = chatCodes.ContainsKey(type); if (ImGui.Checkbox($"##{type.Name()}", ref enabled)) { if (enabled) chatCodes[type] = (ChatSourceExt.All, ChatSourceExt.All); else chatCodes.Remove(type); } ImGui.SameLine(); if (!type.HasSource()) { ImGui.TextUnformatted(type.Name()); continue; } using var typeNode = ImRaii.TreeNode($"{type.Name()}"); if (!typeNode.Success) continue; ImGui.Text(Language.ImGuiUtil_ChannelSelector_Source); ImGui.SameLine(400.0f * ImGuiHelpers.GlobalScale); ImGui.Text(Language.ImGuiUtil_ChannelSelector_Target); chatCodes.TryGetValue(type, out var sourcesEnum); var sources = (uint)sourcesEnum.Source; var targets = (uint)sourcesEnum.Target; foreach (var kind in Enum.GetValues().Where(s => s != ChatSource.None)) { if (ImGui.CheckboxFlags($"{kind.Name()}##source", ref sources, (uint)kind)) chatCodes[type] = ((ChatSource)sources, sourcesEnum.Target); ImGui.SameLine(400.0f * ImGuiHelpers.GlobalScale); if (ImGui.CheckboxFlags($"{kind.Name()}##target", ref targets, (uint)kind)) chatCodes[type] = (sourcesEnum.Source, (ChatSource)targets); } } } } public static void ExtraChatSelector(string headerText, ref bool all, HashSet extraChatChannels) { if (Plugin.ExtraChat.ChannelNames.Count <= 0) return; using var extraTree = ImRaii.TreeNode(headerText); if (!extraTree.Success) return; ImGui.Checkbox(Language.Options_Tabs_ExtraChatAll, ref all); ImGui.Separator(); using var _ = ImRaii.Disabled(all); foreach (var (id, name) in Plugin.ExtraChat.ChannelNames) { var enabled = extraChatChannels.Contains(id); if (!ImGui.Checkbox($"{name}##ec-{id}", ref enabled)) continue; if (enabled) extraChatChannels.Add(id); else extraChatChannels.Remove(id); } } }