#nullable enable
using System.IO;
using ImGuiNET;
using T3.Core.DataTypes.Vector;
using T3.Core.SystemUi;
using T3.Core.Utils;
using T3.Editor.Gui.Dialogs;
using T3.Editor.Gui.Help;
using T3.Editor.Gui.Interaction;
using T3.Editor.Gui.Input;
using T3.Editor.Gui.Styling;
using T3.Editor.Gui.Styling.Markdown;
using T3.Editor.UiModel;
using T3.Editor.UiModel.Helpers;
using T3.Editor.UiModel.InputsAndTypes;
namespace T3.Editor.Gui.Windows;
internal sealed class OperatorHelp
{
///
/// The help icon in the Parameter window header: clicking toggles the Help window (opening it on the
/// operator's doc); Ctrl-click edits the description. Hovering shows the doc as a tooltip while the
/// Help window is closed.
///
public static void DrawHelpIcon(SymbolUi symbolUi)
{
ImGui.SameLine();
var w = ImGui.GetFrameHeight();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + 2);
var toggledToEdit = ImGui.GetIO().KeyCtrl;
var icon = toggledToEdit ? Icon.PopUp : Icon.Help;
var helpWindowIsOpen = HelpWindow.IsOpen;
ImGui.PushStyleColor(ImGuiCol.Button, Color.Transparent.Rgba);
if (CustomComponents.IconButton(icon, new Vector2(w, w),
helpWindowIsOpen
? CustomComponents.ButtonStates.Activated
: CustomComponents.ButtonStates.Default))
{
if (toggledToEdit)
EditDescriptionDialog.ShowNextFrame(symbolUi.Symbol);
else if (helpWindowIsOpen)
HelpWindow.CloseWindow();
else
HelpWindow.ShowTopic(HelpTopic.ForOperator(symbolUi.Symbol.Id), showWindow: true);
}
ImGui.PopStyleColor();
// While closed, the doc tooltip below carries the affordance; a second tooltip would overlap it.
if (helpWindowIsOpen)
CustomComponents.TooltipForLastItem("Toggle help window");
// With the Help window open it already shows the selected op's doc — a tooltip would double it.
if (ImGui.IsItemHovered() && !HelpWindow.IsOpen)
{
_timeSinceTooltipHovered += ImGui.GetIO().DeltaTime;
var delayedFadeInToPreventImguiFlickering = (_timeSinceTooltipHovered * 3).Clamp(0.001f, 1);
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, delayedFadeInToPreventImguiFlickering * ImGui.GetStyle().Alpha);
ImGui.BeginTooltip();
ImGui.Dummy(new Vector2(500 * T3Ui.UiScaleFactor, 1));
DrawHelp(symbolUi, isInTooltip: true);
ImGui.PushStyleColor(ImGuiCol.Text, UiColors.TextMuted.Rgba);
ImGui.TextUnformatted("Click to open the Help window");
ImGui.PopStyleColor();
ImGui.EndTooltip();
ImGui.PopStyleVar();
}
else
{
_timeSinceTooltipHovered = 0;
}
}
public static bool DrawHelpSummary(SymbolUi symbolUi, bool showMoreIndicator = true)
{
if (string.IsNullOrEmpty(symbolUi.Description))
return false;
using var reader = new StringReader(symbolUi.Description);
var firstLine = reader.ReadLine();
if (string.IsNullOrEmpty(firstLine))
return false;
var helpRequested = false;
//ImGui.Indent(10);
ImGui.PushStyleColor(ImGuiCol.Text, UiColors.TextMuted.Rgba);
ImGui.TextWrapped(firstLine);
if (ImGui.IsItemHovered())
{
if (ImGui.IsMouseClicked(ImGuiMouseButton.Left))
{
EditDescriptionDialog.ShowNextFrame(symbolUi.Symbol);
}
CustomComponents.TooltipForLastItem("Click to edit descriptions and links");
}
ImGui.PopStyleColor();
if (showMoreIndicator)
{
ImGui.PushStyleColor(ImGuiCol.Text, UiColors.TextMuted.Fade(0.5f).Rgba);
FormInputs.AddVerticalSpace(5);
var anyParameterHasDescription = symbolUi.InputUis
.Values
.Any(inputUi => !string.IsNullOrEmpty(inputUi.Description));
if (firstLine != symbolUi.Description || anyParameterHasDescription)
{
ImGui.TextUnformatted("Read more...");
if (ImGui.IsItemHovered() && ImGui.IsMouseClicked(ImGuiMouseButton.Left))
{
helpRequested = true;
}
}
//FormInputs.AddVerticalSpace();
ImGui.PopStyleColor();
}
return helpRequested;
}
/// Title + namespace block, exposed so the Help window can lay a thumbnail beside it.
public static void DrawHeader(SymbolUi symbolUi)
{
ImGui.Indent(10);
FormInputs.AddSectionHeader(symbolUi.Symbol.Name);
ImGui.PushFont(Fonts.FontSmall);
ImGui.TextUnformatted("in ");
ImGui.SameLine();
ImGui.TextUnformatted(symbolUi.Symbol.Namespace);
ImGui.PopFont();
ImGui.Unindent(10);
}
public static void DrawHelp(SymbolUi symbolUi, bool isInTooltip = false, bool omitHeader = false)
{
// Inside a tooltip the op-ref hover must not open its own (nested) tooltip.
var opRefHandler = isInTooltip ? _operatorRefHandlerInTooltip : _operatorRefHandler;
if (!omitHeader)
DrawHeader(symbolUi);
FormInputs.SetIndentToLeft();
FormInputs.AddVerticalSpace();
// Description
ImGui.PushFont(Fonts.FontNormal);
ImGui.Indent(10);
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(10, 10));
if (!string.IsNullOrEmpty(symbolUi.Description))
{
_descriptionView.Draw(symbolUi.Description,
onOperatorRef: opRefHandler,
operatorColor: MarkdownOperatorLinks.GetOperatorColor);
}
else
{
FormInputs.AddHint("No description yet.");
if (ImGui.Button("Edit description... "))
EditDescriptionDialog.ShowNextFrame(symbolUi.Symbol);
}
_parametersWithDescription.Clear();
foreach (var inputUi in symbolUi.InputUis.Values)
{
if (!string.IsNullOrEmpty(inputUi.Description))
{
_parametersWithDescription.Add(inputUi);
}
}
// Parameter descriptions
if (_parametersWithDescription.Count > 0)
{
FormInputs.AddVerticalSpace(5);
ImGui.PushStyleColor(ImGuiCol.Text, UiColors.TextMuted.Rgba);
ImGui.PushFont(Fonts.FontNormal);
ImGui.TextUnformatted("Parameter Details");
ImGui.PopFont();
ImGui.PopStyleColor();
var parameterColorWidth = 150f * T3Ui.UiScaleFactor;
for (var parameterIndex = 0; parameterIndex < _parametersWithDescription.Count; parameterIndex++)
{
var p = _parametersWithDescription[parameterIndex];
ImGui.PushStyleColor(ImGuiCol.Text, UiColors.Text.Rgba);
var parameterNameWidth = ImGui.CalcTextSize(p.InputDefinition.Name).X;
ImGui.SetCursorPosX(parameterColorWidth - parameterNameWidth);
ImGui.TextUnformatted(p.InputDefinition.Name);
ImGui.PopStyleColor();
ImGui.SameLine(parameterColorWidth + 10);
GetParameterDescriptionView(parameterIndex).Draw(p.Description,
onOperatorRef: opRefHandler,
operatorColor: MarkdownOperatorLinks.GetOperatorColor);
}
}
ImGui.Dummy(Vector2.One);
DocumentationRenderer.DrawLinksAndExamples(symbolUi);
ImGui.PopStyleVar();
ImGui.Unindent(10); // Balance the Indent(10) above; bare Unindent() uses the larger default spacing and leaves a negative indent.
ImGui.Dummy(new Vector2(10, 10));
ImGui.PopFont();
}
public static class DocumentationRenderer
{
private static SymbolUi? _cachedSymbolUi;
private static Guid _cachedSymbolId;
private static readonly List<(string Title, string Description, string Url, Icon? Icon)> _cachedLinks = [];
private static int _cachedSymbolUiVersion = -1;
public static void DrawLinksAndExamples(SymbolUi symbolUi)
{
// Check if symbolUi changed
if (symbolUi != _cachedSymbolUi || symbolUi.Symbol.Id != _cachedSymbolId || symbolUi.VersionCounter != _cachedSymbolUiVersion)
{
_cachedSymbolUiVersion = symbolUi.VersionCounter;
_cachedSymbolUi = symbolUi;
_cachedSymbolId = symbolUi.Symbol.Id;
CacheSymbolData(symbolUi);
}
ImGui.PushFont(Fonts.FontSmall);
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(5, 5));
// The symbol links now rank into the unified "watch & learn" list (see GetLinkRows / VideoResourceList),
// so only the example operators remain here.
DrawExamples();
ImGui.PopFont();
ImGui.PopStyleVar();
}
/// The hand-authored symbol links as rows for the merged help resource list (replaces the old "Links:" section).
public static IReadOnlyList GetLinkRows(SymbolUi symbolUi)
{
var rows = new List();
foreach (var link in symbolUi.Links.Values)
{
if (string.IsNullOrEmpty(link.Url))
continue;
var title = link.Title ?? link.Type.ToString();
var icon = ExternalLink.LinkIcons.TryGetValue(link.Type, out var i) ? i : (Icon?)null;
rows.Add(new Help.VideoResourceList.LinkRow(title, link.Url, link.Description, icon));
}
return rows;
}
private static void CacheSymbolData(SymbolUi symbolUi)
{
_cachedLinks.Clear();
foreach (var link in symbolUi.Links.Values)
{
if (!string.IsNullOrEmpty(link.Url))
{
var title = link.Title ?? link.Type.ToString();
_cachedLinks.Add((title,
link.Url,
link.Description,
ExternalLink.LinkIcons.TryGetValue(link.Type, out var icon) ? icon : (Icon?)null)
);
}
}
}
private static void DrawLinks()
{
if (_cachedLinks.Count == 0) return;
ImGui.AlignTextToFramePadding();
ImGui.PushStyleColor(ImGuiCol.Text, UiColors.TextMuted.Rgba);
ImGui.TextUnformatted("Links:");
ImGui.PopStyleColor();
ImGui.SameLine();
ImGui.PushStyleColor(ImGuiCol.Button, Color.Transparent.Rgba);
foreach (var (title, url, description, icon) in _cachedLinks)
{
ImGui.PushStyleColor(ImGuiCol.Text, UiColors.StatusAutomated.Rgba);
bool clicked;
if (icon.HasValue)
{
ImGui.Button(" " + title);
Icons.DrawIconOnLastItem(icon.Value, UiColors.StatusAutomated, 0);
clicked = ImGui.IsItemClicked();
}
else
{
clicked = ImGui.Button(title);
}
ImGui.PopStyleColor();
CustomComponents.TooltipForLastItem(string.IsNullOrEmpty(description) ? _openLink : description + "\n" + _openLink, url);
if (clicked)
CoreUi.Instance.OpenWithDefaultApplication(url);
ImGui.SameLine();
}
ImGui.Dummy(new Vector2(2, 2));
ImGui.PopStyleColor();
}
private static void DrawExamples()
{
if (ExampleSymbolLinking.TryGetExamples(_cachedSymbolUi!.Symbol.Id, out var examples))
{
foreach (var exampleUi in examples)
{
UiElements.DrawExampleOperator(exampleUi, "Example");
}
}
}
}
private static void DrawGroupLabel(string title)
{
ImGui.AlignTextToFramePadding();
ImGui.PushStyleColor(ImGuiCol.Text, UiColors.TextMuted.Rgba);
ImGui.TextUnformatted(title);
ImGui.PopStyleColor();
}
internal static readonly EditSymbolDescriptionDialog EditDescriptionDialog = new();
private static readonly List _parametersWithDescription = new(10);
private static readonly MarkdownView _descriptionView = new(new MarkdownView.Options());
///
/// One view per parameter row — MarkdownView caches a single document, so sharing one
/// instance across parameters would reparse every description every frame.
/// Most descriptions were authored as plain text, so newlines stay hard line breaks.
///
private static readonly List _parameterDescriptionViews = new(10);
private static MarkdownView GetParameterDescriptionView(int parameterIndex)
{
while (_parameterDescriptionViews.Count <= parameterIndex)
{
_parameterDescriptionViews.Add(new MarkdownView(new MarkdownView.Options
{
HardLineBreaks = true,
MutedBodyText = true,
}));
}
return _parameterDescriptionViews[parameterIndex];
}
// Cached so the per-frame Draw calls don't allocate new delegates.
private static readonly MarkdownView.OperatorRefRendered _operatorRefHandler =
static opName => MarkdownOperatorLinks.HandleOperatorRef(opName);
private static readonly MarkdownView.OperatorRefRendered _operatorRefHandlerInTooltip =
static opName => MarkdownOperatorLinks.HandleOperatorRef(opName, suppressTooltip: true);
private static float _timeSinceTooltipHovered = 0;
private static readonly string _openLink = "Open link in browser:";
}