using System; using System.Drawing; using System.Numerics; using System.Runtime.InteropServices; using Dalamud.Interface; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Graphics; using FFXIVClientStructs.FFXIV.Client.System.Input; using FFXIVClientStructs.FFXIV.Component.GUI; using InteropGenerator.Runtime; using KamiToolKit.Classes; using KamiToolKit.Enums; using KamiToolKit.Timelines; using Lumina.Text.ReadOnly; namespace KamiToolKit.Nodes; public unsafe class TextInputNode : ComponentNode { public readonly NineGridNode BackgroundNode; public readonly TextNode CurrentTextNode; public readonly CursorNode CursorNode; public readonly NineGridNode FocusNode; public readonly TextInputSelectionListNode SelectionListNode; public readonly TextNode TextLimitsNode; public readonly TextNode PlaceholderTextNode; private AtkComponentInputBase.CallbackDelegate? pinnedCallbackFunction; public TextInputNode() { SetInternalComponentType(ComponentType.TextInput); BackgroundNode = new SimpleNineGridNode { NodeId = 19, TexturePath = "ui/uld/TextInputA.tex", TextureCoordinates = new Vector2(24.0f, 0.0f), TextureSize = new Vector2(24.0f, 24.0f), Offsets = new Vector4(10.0f), Size = new Vector2(152.0f, 28.0f), }; BackgroundNode.AttachNode(this); FocusNode = new SimpleNineGridNode { NodeId = 18, TexturePath = "ui/uld/TextInputA.tex", TextureCoordinates = new Vector2(0.0f, 0.0f), TextureSize = new Vector2(24.0f, 24.0f), Offsets = new Vector4(10.0f), Size = new Vector2(152.0f, 28.0f), }; FocusNode.AttachNode(this); TextLimitsNode = new TextNode { NodeId = 17, Position = new Vector2(-24.0f, 6.0f), Size = new Vector2(170.0f, 19.0f), FontType = FontType.MiedingerMed, FontSize = 14, AlignmentType = (AlignmentType)21, }; TextLimitsNode.AttachNode(this); CurrentTextNode = new TextNode { NodeId = 16, Position = new Vector2(10.0f, 6.0f), Size = new Vector2(132.0f, 18.0f), AlignmentType = AlignmentType.TopLeft, TextFlags = TextFlags.AutoAdjustNodeSize, TextColor = ColorHelper.GetColor(1), }; CurrentTextNode.AttachNode(this); SelectionListNode = new TextInputSelectionListNode { NodeId = 4, Position = new Vector2(0.0f, 22.0f), Size = new Vector2(186.0f, 208.0f), }; SelectionListNode.AttachNode(this); CursorNode = new CursorNode { NodeId = 2, Position = new Vector2(10.0f, 2.0f), Size = new Vector2(4.0f, 24.0f), OriginY = 4.0f, }; CursorNode.AttachNode(this); PlaceholderTextNode = new TextNode { Position = new Vector2(8.0f, 0.0f), TextColor = ColorHelper.GetColor(3), }; PlaceholderTextNode.AttachNode(this); Data->Nodes[0] = CurrentTextNode.NodeId; Data->Nodes[1] = BackgroundNode.NodeId; Data->Nodes[2] = CursorNode.NodeId; Data->Nodes[3] = SelectionListNode.NodeId; Data->Nodes[4] = SelectionListNode.Buttons[8].NodeId; Data->Nodes[5] = SelectionListNode.Buttons[7].NodeId; Data->Nodes[6] = SelectionListNode.Buttons[6].NodeId; Data->Nodes[7] = SelectionListNode.Buttons[5].NodeId; Data->Nodes[8] = SelectionListNode.Buttons[4].NodeId; Data->Nodes[9] = SelectionListNode.Buttons[3].NodeId; Data->Nodes[10] = SelectionListNode.Buttons[2].NodeId; Data->Nodes[11] = SelectionListNode.Buttons[1].NodeId; Data->Nodes[12] = SelectionListNode.Buttons[0].NodeId; Data->Nodes[13] = SelectionListNode.LabelNode.NodeId; Data->Nodes[14] = SelectionListNode.BackgroundNode.NodeId; Data->Nodes[15] = TextLimitsNode.NodeId; Data->CandidateColor = new ByteColor { R = 66 }; Data->IMEColor = new ByteColor { R = 67 }; Data->FocusColor = KnownColor.Black.Vector().ToByteColor(); Flags = TextInputFlags.EnableIme | TextInputFlags.AllowUpperCase | TextInputFlags.AllowLowerCase | TextInputFlags.EnableDictionary | TextInputFlags.AllowNumberInput | TextInputFlags.AllowSymbolInput; EnableCompletion = false; Component->EnableTabCallback = true; LoadTimelines(); pinnedCallbackFunction = OnCallback; Component->Callback = (delegate* unmanaged) Marshal.GetFunctionPointerForDelegate(pinnedCallbackFunction); InitializeComponentEvents(); CollisionNode.AddEvent(AtkEventType.FocusStart, () => { PlaceholderTextNode.IsVisible = false; OnFocused?.Invoke(); if (AutoSelectAll && Component->EvaluatedString.Length > 0) { DalamudInterface.Instance.Framework.RunOnTick(() => { var keyModifiers = new AtkTextInput.KeyModifiers { IsControlDown = true, }; AtkStage.Instance()->AtkInputManager->TextInput->ProcessKeyShortcut(SeVirtualKey.A, &keyModifiers); }, delayTicks: 1); } }); CollisionNode.AddEvent(AtkEventType.FocusStop, () => { OnUnfocused?.Invoke(); if (!PlaceholderString.IsNullOrEmpty() && String.IsEmpty) { PlaceholderTextNode.IsVisible = true; PlaceholderTextNode.String = PlaceholderString; } }); } protected override void Dispose(bool disposing, bool isNativeDestructor) { if (disposing) { base.Dispose(disposing, isNativeDestructor); pinnedCallbackFunction = null; } } public bool IsFocused => AtkStage.Instance()->AtkInputManager->FocusedNode == CollisionNode.Node; public int MaxCharacters { get => (int)Component->ComponentTextData.MaxChar; set => Component->ComponentTextData.MaxChar = (uint)value; } public bool ShowLimitText { get => TextLimitsNode.IsVisible; set => TextLimitsNode.IsVisible = value; } public TextInputFlags Flags { get => (TextInputFlags) ((byte)Data->Flags1 | (byte)Data->Flags2 << 8); set { Data->Flags1 = (TextInputFlags1)((ushort)value & 0xFF); Data->Flags2 = (TextInputFlags2)((ushort)value >> 8); } } public bool EnableCompletion { get => Component->EnableCompletion; set => Component->EnableCompletion = value; } public bool EnableFocusSounds { get => Component->EnableFocusSounds; set => Component->EnableFocusSounds = value; } public virtual ReadOnlySeString String { get => Component->EvaluatedString.AsSpan(); set { Component->SetText(value); UpdatePlaceholderVisibility(); } } public string? PlaceholderString { get; set { field = value; UpdatePlaceholderVisibility(); } } public bool IsError { get => FocusNode.MultiplyColor == new Vector3(1.0f, 0.6f, 0.6f); set => FocusNode.MultiplyColor = value ? new Vector3(1.0f, 0.6f, 0.6f) : Vector3.One; } public bool AutoSelectAll { get; set; } = true; public void ClearFocus() { if (IsFocused) { AtkStage.Instance()->AtkInputManager->SetFocus(null, ParentAddon, 0); } } public virtual Action? OnInputReceived { get; set; } public virtual Action? OnInputComplete { get; set; } public Action? OnFocusLost { get; set; } public Action? OnEscapeEntered { get; set; } public Action? OnTabEntered { get; set; } public Action? OnFocused { get; set; } public Action? OnUnfocused { get; set; } private InputCallbackResult OnCallback(AtkUnitBase* addon, InputCallbackType type, CStringPointer rawString, CStringPointer evaluatedString, int eventKind) { try { switch (type) { case InputCallbackType.Enter: if (this is TextMultiLineInputNode) break; OnInputComplete?.Invoke(Component->EvaluatedString.AsSpan()); ClearFocus(); break; case InputCallbackType.TextChanged: OnInputReceived?.Invoke(Component->EvaluatedString.AsSpan()); break; case InputCallbackType.Escape: OnEscapeEntered?.Invoke(); break; case InputCallbackType.FocusLost: OnFocusLost?.Invoke(); break; case InputCallbackType.Tab: OnTabEntered?.Invoke(); break; } return InputCallbackResult.None; } catch (Exception e) { Log.Exception(e); return InputCallbackResult.None; } } private void UpdatePlaceholderVisibility() { PlaceholderTextNode.String = PlaceholderString ?? string.Empty; PlaceholderTextNode.IsVisible = String.IsEmpty && !PlaceholderString.IsNullOrEmpty(); } protected override void OnSizeChanged() { base.OnSizeChanged(); BackgroundNode.Size = Size; FocusNode.Size = Size; PlaceholderTextNode.Size = Size; TextLimitsNode.Size = new Vector2(Width + 18.0f, Height - 9.0f); CurrentTextNode.Size = new Vector2(Width - 20.0f, Height - 10.0f); } private void LoadTimelines() { AddTimeline(new TimelineBuilder() .BeginFrameSet(1, 29) .AddLabelPair(1, 9, 17) .AddLabelPair(10, 19, 18) .AddLabelPair(20, 29, 7) .EndFrameSet() .Build()); BackgroundNode.AddTimeline(new TimelineBuilder() .AddFrameSetWithFrame(1, 9, 1, alpha: 255) .BeginFrameSet(10, 19) .AddFrame(10, alpha: 255) .AddFrame(12, alpha: 255) .EndFrameSet() .AddFrameSetWithFrame(20, 29, 20, alpha: 127) .Build()); FocusNode.AddTimeline(new TimelineBuilder() .BeginFrameSet(10, 19) .AddFrame(10, alpha: 0) .AddFrame(12, alpha: 255) .EndFrameSet() .Build()); TextLimitsNode.AddTimeline(new TimelineBuilder() .AddFrameSetWithFrame(1, 9, 1, alpha: 102) .BeginFrameSet(10, 19) .AddFrame(10, alpha: 102) .AddFrame(12, alpha: 127) .EndFrameSet() .AddFrameSetWithFrame(20, 29, 20, alpha: 76) .Build()); CursorNode.AddTimeline(new TimelineBuilder() .BeginFrameSet(1, 15) .AddLabel(1, 101, AtkTimelineJumpBehavior.Start, 0) .AddLabel(15, 0, AtkTimelineJumpBehavior.LoopForever, 101) .EndFrameSet() .Build()); } }