using System.Numerics; using System.Text; using Dalamud.Bindings.ImGui; using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; using HellionChat.Code; using HellionChat.Resources; using HellionChat.Util; using Microsoft.Extensions.Logging; namespace HellionChat.Ui; internal sealed class InputPreview : Window { private readonly Components.ChunkRenderer _chunkRenderer; private readonly Lender _handlerLender; private readonly Windows.MainWindow _mainWindow; private readonly Components.InputBar _inputBar; private readonly ILogger _logger; private bool _drawing; private bool _hasEvaluation; internal float PreviewHeight; private int _lastLength; private Message? _previewMessage; internal InputPreview( Components.ChunkRenderer chunkRenderer, Lender handlerLender, Windows.MainWindow mainWindow, Components.InputBar inputBar, ILogger logger ) : base("##chat2-inputpreview") { _chunkRenderer = chunkRenderer; _handlerLender = handlerLender; _mainWindow = mainWindow; _inputBar = inputBar; _logger = logger; Flags = ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoScrollbar; RespectCloseHotkey = false; DisableWindowSounds = true; IsOpen = true; // TODO: remove discard once logging call sites exist _ = _logger; } public void Dispose() { } private bool ValidDraw => !string.IsNullOrEmpty(_inputBar.PendingMessage) && _inputBar.PendingMessage.Length >= Plugin.Config.PreviewMinimum; // IsDrawable gates DrawConditions; it is also consumed externally by // any component that needs to know whether the preview popup is visible. internal bool IsDrawable => ValidDraw && _hasEvaluation; private static bool IsWindowMode => Plugin.Config.PreviewPosition is PreviewPosition.Top or PreviewPosition.Bottom; // PreOpenCheck owns state: it runs once per frame before the visibility // gate so Drawing/PreviewMessage/HasEvaluation stay fresh even when the // window is not ultimately drawn. PreDraw owns position/size to avoid // wasted computation on frames where DrawConditions returns false // (position math only matters when the window is about to render). // This matches the v1.5.6 UpdateConditionCheck/PreDraw split — the // Framework.Update subscribe is removed; PreOpenCheck runs at the same // cadence via Dalamud's WindowSystem. public override void PreOpenCheck() { _drawing = ValidDraw; if (!_drawing) { _lastLength = 0; PreviewHeight = 0; _previewMessage = null; _hasEvaluation = false; return; } if (_previewMessage == null || _lastLength != _inputBar.PendingMessage.Length) { _lastLength = _inputBar.PendingMessage.Length; var bytes = Encoding.UTF8.GetBytes(_inputBar.PendingMessage.Trim()); AutoTranslate.ReplaceWithPayload(ref bytes); var chunks = ChunkUtil .ToChunks(SeString.Parse(bytes), ChunkSource.Content, ChatType.Say) .ToList(); _previewMessage = Message.FakeMessage(chunks, new ChatCode(XivChatType.Say, 0, 0)); _previewMessage.DecodeTextParam(); } _hasEvaluation = !Plugin.Config.OnlyPreviewIf || _previewMessage.Content.Count > 1; } public override bool DrawConditions() { return IsWindowMode && IsDrawable; } public override void PreDraw() { var pos = _mainWindow.LastWindowPos; var size = _mainWindow.LastWindowSize; Size = size with { Y = PreviewHeight }; var y = Plugin.Config.PreviewPosition switch { PreviewPosition.Top => pos.Y - PreviewHeight, PreviewPosition.Bottom => pos.Y + size.Y, _ => throw new ArgumentOutOfRangeException( nameof(Plugin.Config.PreviewPosition), Plugin.Config.PreviewPosition, null ), }; Position = pos with { Y = y }; PositionCondition = ImGuiCond.Always; } public override void Draw() { CalculatePreviewHeight(); DrawPreview(); } internal void CalculatePreviewHeight() { // Pre-draw offscreen once to measure actual rendered height; value is // consumed next frame by PreDraw() for window sizing. PreviewHeight = 0; var pos = ImGui.GetCursorPos(); ImGui.SetCursorPos(new Vector2(-500, -500)); var before = ImGui.GetCursorPosY(); using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)) { ImGui.TextUnformatted(Language.Options_Preview_Header); _chunkRenderer.DrawChunks(_previewMessage!.Content, wrap: true, lineWidth: 0f); } var after = ImGui.GetCursorPosY(); ImGui.SetCursorPos(pos); PreviewHeight = after - before; PreviewHeight += IsWindowMode ? ImGui.GetStyle().WindowPadding.Y * 2 : 0; } internal void DrawPreview() { using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)) { ImGui.TextUnformatted(Language.Options_Preview_Header); // Primary path resets the Lender counter in MainWindow.Draw(); // this fallback covers the edge-case where MainWindow is closed but // InputPreview is still open, preventing handler pool growth. if (!_mainWindow.IsOpen) _handlerLender.ResetCounter(); var handler = _handlerLender.Borrow(); _chunkRenderer.DrawChunks( _previewMessage!.Content, wrap: true, handler: handler, lineWidth: 0f ); handler.Draw(); } } }