using System.Text.RegularExpressions; using ImGuiNET; using T3.Core.DataTypes.Vector; using T3.Core.Operator; using T3.Core.SystemUi; using T3.Core.Utils; using T3.Editor.Gui.Input; using T3.Editor.Gui.UiHelpers; using T3.Editor.UiModel; namespace T3.Editor.Gui.Styling; /// /// A set of special wrappers for ImGui components. /// Also, checkout the FormInputs class. /// internal static partial class CustomComponents { /// Draw a splitter /// /// Take from https://github.com/ocornut/imgui/issues/319#issuecomment-147364392 /// public static bool SplitFromBottom(ref float offsetFromBottom) { const float thickness = 3; var hasBeenDragged = false; var backupPos = ImGui.GetCursorPos(); var windowPos = ImGui.GetWindowPos(); var windowSize = ImGui.GetWindowSize(); var pos = new Vector2(windowPos.X, windowPos.Y + windowSize.Y - offsetFromBottom - thickness - 1); ImGui.SetCursorScreenPos(pos); ImGui.PushStyleColor(ImGuiCol.Button, UiColors.BackgroundGaps.Rgba); ImGui.PushStyleColor(ImGuiCol.ButtonActive, UiColors.BackgroundActive.Rgba); ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UiColors.BackgroundActive.Rgba); ImGui.Button("##Splitter", new Vector2(-1, thickness)); ImGui.PopStyleColor(3); // Disabled for now, since Setting MouseCursor wasn't working reliably // if (ImGui.IsItemHovered() ) // { // //ImGui.SetMouseCursor(ImGuiMouseCursor.ResizeNS); // } if (ImGui.IsItemActive()) { if (Math.Abs(ImGui.GetIO().MouseDelta.Y) > 0) { hasBeenDragged = true; offsetFromBottom = (offsetFromBottom - ImGui.GetIO().MouseDelta.Y) .Clamp(0, windowSize.Y - thickness); } } ImGui.SetCursorPos(backupPos); return hasBeenDragged; } public static void HelpText(string text) { ImGui.PushFont(Fonts.FontSmall); ImGui.PushStyleColor(ImGuiCol.Text, UiColors.TextMuted.Rgba); ImGui.TextWrapped(text); ImGui.PopStyleColor(); ImGui.PopFont(); ImGui.Dummy(new Vector2(0, 4 * T3Ui.UiScaleFactor)); } public static void SmallGroupHeader(string text) { FormInputs.AddVerticalSpace(5); ImGui.PushFont(Fonts.FontSmall); ImGui.PushStyleColor(ImGuiCol.Text, UiColors.TextMuted.Rgba); ImGui.TextUnformatted(text.ToUpperInvariant()); ImGui.PopStyleColor(); ImGui.PopFont(); FormInputs.AddVerticalSpace(2); } public static void MenuGroupHeader(string text) { FormInputs.AddVerticalSpace(); ImGui.PushFont(Fonts.FontSmall); ImGui.PushStyleColor(ImGuiCol.Text, UiColors.TextMuted.Rgba); ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetFrameHeight()); ImGui.TextUnformatted(text); ImGui.PopStyleColor(); ImGui.PopFont(); } /// /// Uses slightly different styling than ImGui.Separator() /// public static void SeparatorLine() { FormInputs.AddVerticalSpace(4); var x = ImGui.GetCursorPosX(); ImGui.SetCursorPosX(0); var p = ImGui.GetCursorScreenPos(); //var p = ImGui.GetCursorStartPos() + ImGui.GetWindowPos() + new Vector2(1,1); ImGui.GetWindowDrawList() .AddRectFilled(p, p + new Vector2(ImGui.GetWindowSize().X, 1), UiColors.ForegroundFull.Fade(0.1f)); FormInputs.AddVerticalSpace(5); ImGui.SetCursorPosX(x); } /// /// A small label that can be used to structure context menus /// public static void HintLabel(string label) { ImGui.PushFont(Fonts.FontSmall); ImGui.PushStyleColor(ImGuiCol.Text, UiColors.Gray.Rgba); ImGui.TextUnformatted(label); ImGui.PopStyleColor(); ImGui.PopFont(); } public static void FillWithStripes(ImDrawListPtr drawList, ImRect areaOnScreen, float canvasScale, float patternWidth = 16) { drawList.PushClipRect(areaOnScreen.Min, areaOnScreen.Max, true); var lineColor = new Color(0f, 0f, 0f, 0.2f); var stripeOffset = (patternWidth / 2 * canvasScale); var lineWidth = stripeOffset / 2.7f; var h = areaOnScreen.GetHeight(); var stripeCount = (int)((areaOnScreen.GetWidth() + h + 3 * lineWidth) / stripeOffset); var p = areaOnScreen.Min - new Vector2(h + lineWidth, +lineWidth); var offset = new Vector2(h + 2 * lineWidth, h + 2 * lineWidth); for (var i = 0; i < stripeCount; i++) { drawList.AddLine(p, p + offset, lineColor, lineWidth); p.X += stripeOffset; } drawList.PopClipRect(); } public static bool EmptyWindowMessage(string message, string buttonLabel = null) { var center = ImGui.GetWindowPos() + ImGui.GetWindowSize() * 0.5f; var lines = message.Split('\n').ToArray(); var lineCount = lines.Length; if (!string.IsNullOrEmpty(buttonLabel)) lineCount++; var textLineHeight = ImGui.GetTextLineHeight(); var y = center.Y - lineCount * textLineHeight / 2; var drawList = ImGui.GetWindowDrawList(); var emptyMessageColor = UiColors.TextMuted; foreach (var line in lines) { if (!string.IsNullOrEmpty(line)) { var textSize = ImGui.CalcTextSize(line); var position = new Vector2(center.X - textSize.X / 2, y); drawList.AddText(position, emptyMessageColor, line); } y += textLineHeight; } if (!string.IsNullOrEmpty(buttonLabel)) { y += 10; var style = ImGui.GetStyle(); var textSize = ImGui.CalcTextSize(buttonLabel) + style.FramePadding; var position = new Vector2(center.X - textSize.X / 2, y); ImGui.SetCursorScreenPos(position); return ImGui.Button(buttonLabel); } return false; } public static bool DrawInputFieldWithPlaceholder(string placeHolderLabel, ref string value, float width = 0, bool showClear = true, ImGuiInputTextFlags inputFlags = ImGuiInputTextFlags.None) { ImGui.PushID(placeHolderLabel.GetHashCode(StringComparison.Ordinal)); var notEmpty = !string.IsNullOrEmpty(value); var wasNull = value == null; if (wasNull) value = string.Empty; ImGui.SetNextItemWidth(width - FormInputs.ParameterSpacing - (notEmpty ? ImGui.GetFrameHeight() : 0)); var modified = ImGui.InputText("##", ref value, 1000, inputFlags); if (!modified && wasNull) value = null; if (notEmpty) { if (showClear) { ImGui.SameLine(0, 0); if (ImGui.Button("×" + "##" + placeHolderLabel)) { value = null; modified = true; } } } else { var drawList = ImGui.GetWindowDrawList(); var minPos = ImGui.GetItemRectMin(); var maxPos = ImGui.GetItemRectMax(); drawList.PushClipRect(minPos, maxPos); drawList.AddText(minPos + new Vector2(8, 5), UiColors.ForegroundFull.Fade(0.25f), placeHolderLabel); drawList.PopClipRect(); } ImGui.PopID(); return modified; } /// /// Draws a frame that indicates if the current window is focused. /// This is useful for windows that have window specific keyboard short cuts. /// Returns true if the window is focused /// public static void DrawWindowFocusFrame() { if (!ImGui.IsWindowFocused()) return; var min = ImGui.GetWindowPos(); ImGui.GetWindowDrawList().AddRect(min, min + ImGui.GetWindowSize() + new Vector2(0, 0), UiColors.ForegroundFull.Fade(0.2f)); } public static string HumanReadablePascalCase(string f) { return Regex.Replace(f, "(\\B[A-Z])", " $1"); } public static bool RoundedButton(string id, float width, ImDrawFlags roundedCorners) { var size = new Vector2(width, ImGui.GetFrameHeight()); if (width == 0) size.X = size.Y; var clicked = ImGui.InvisibleButton(id, size); var dl = ImGui.GetWindowDrawList(); var color = ImGui.IsItemHovered() ? ImGuiCol.ButtonHovered.GetStyleColor() : ImGuiCol.Button.GetStyleColor(); dl.AddRectFilled(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), color, 7, roundedCorners); return clicked; } private static Vector2 _dragScrollStart; public static bool IsDragScrolling => _draggedWindowObject != null; private static object _draggedWindowObject; public static bool IsAnotherWindowDragScrolling(object windowObject) { return _draggedWindowObject != null && _draggedWindowObject != windowObject; } public static void HandleDragScrolling(object windowObject) { if (_draggedWindowObject == windowObject) { if (ImGui.IsMouseReleased(ImGuiMouseButton.Right)) { _draggedWindowObject = null; } if (ImGui.IsMouseDragging(ImGuiMouseButton.Right)) { var scrollY = _dragScrollStart.Y - ImGui.GetMouseDragDelta(ImGuiMouseButton.Right).Y; ImGui.SetScrollY(scrollY); } return; } if (ImGui.IsWindowHovered( ) && !T3Ui.DragFieldWasHoveredLastFrame && ImGui.IsMouseClicked(ImGuiMouseButton.Right)) { _dragScrollStart = new Vector2(ImGui.GetScrollX(), ImGui.GetScrollY()); _draggedWindowObject = windowObject; } } internal static bool DrawProjectDropdown(ref EditableSymbolProject selectedValue) { return FormInputs.AddDropdown(ref selectedValue, EditableSymbolProject.AllProjects.OrderBy(x => x.DisplayName), "Project", x => x.DisplayName, "Project to edit symbols in."); } public static void DrawSymbolCodeContextMenuItem(Symbol symbol) { var symbolPackage = symbol.SymbolPackage; var project = symbolPackage as EditableSymbolProject; var enabled = project != null; if (ImGui.MenuItem("Open C# code", enabled)) { if (!project!.TryOpenCSharpInEditor(symbol)) { BlockingWindow.Instance.ShowMessageBox($"Failed to open C# code for {symbol.Name}\nCheck the logs for details.", "Error"); } } } public static void StylizedText(string text, ImFontPtr imFont, Color color, bool addPadding = false) { ImGui.PushFont(imFont); ImGui.PushStyleColor(ImGuiCol.Text, color.Rgba); ImGui.TextUnformatted(text); ImGui.PopStyleColor(); ImGui.PopFont(); if (addPadding) ImGui.Dummy(new Vector2(1, 5 * T3Ui.UiScaleFactor)); } /// /// Draws the subtle rounded hover background used for library tree folders, covering only the /// clickable label area. Push transparent HeaderHovered/HeaderActive colors before drawing the /// tree node so ImGui's default header highlight doesn't show through. /// public static void DrawHoverHighlightOnLastItem() { if (!ImGui.IsItemHovered()) return; ImGui.GetWindowDrawList().AddRectFilled(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), UiColors.BackgroundActive.Fade(0.2f), 5 * T3Ui.UiScaleFactor); } public static void RightAlign(float itemWidth, bool sameLine = true) { if(sameLine) ImGui.SameLine(); var padding = ImGui.GetStyle().WindowPadding.X; var avail = ImGui.GetContentRegionAvail().X; ImGui.SetCursorPosX(ImGui.GetCursorPosX() + avail - itemWidth - padding); } /// /// A reusable popover/popup component. Draws a trigger button and opens a popup /// where you can draw any custom content via the action. /// /// Unique identifier for the popup. /// Label displayed on the trigger button. /// Action to draw the popup content. Return true to close the popup. /// Width of the trigger button. Use 0 for auto-size. /// True if the popup was just opened this frame. public static bool DrawPopover(string id, string triggerLabel, Func drawContent, float triggerWidth = 0) { var popupId = $"##Popover_{id}"; var wasOpened = false; if (triggerWidth > 0) ImGui.SetNextItemWidth(triggerWidth); if (ImGui.Button(triggerLabel + "##" + id)) { ImGui.OpenPopup(popupId); wasOpened = true; } ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(8 * T3Ui.UiScaleFactor)); ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(6 * T3Ui.UiScaleFactor)); if (ImGui.BeginPopup(popupId)) { var shouldClose = drawContent(); if (shouldClose) ImGui.CloseCurrentPopup(); ImGui.EndPopup(); } ImGui.PopStyleVar(2); return wasOpened; } /// /// Overload that uses an Action instead of Func, for content that doesn't need to close programmatically. /// public static bool DrawPopover(string id, string triggerLabel, Action drawContent, float triggerWidth = 0) { return DrawPopover(id, triggerLabel, () => { drawContent(); return false; }, triggerWidth); } }