Files
tooll3--t3/Editor/Gui/MagGraph/Ui/MagGraphCanvas.DrawSection.cs
2026-07-13 13:13:17 +08:00

288 lines
14 KiB
C#

using ImGuiNET;
using T3.Core.Utils;
using T3.Editor.Gui.MagGraph.Interaction;
using T3.Editor.Gui.MagGraph.Model;
using T3.Editor.Gui.MagGraph.States;
using T3.Editor.Gui.Styling;
using T3.Editor.Gui.UiHelpers;
using T3.Editor.UiModel;
using T3.Editor.UiModel.Commands;
using T3.Editor.UiModel.Commands.Graph;
using T3.Editor.UiModel.Commands.Sections;
using T3.Editor.UiModel.Selection;
namespace T3.Editor.Gui.MagGraph.Ui;
internal sealed partial class MagGraphView
{
private void DrawSection(MagGraphSection magSection, ImDrawListPtr drawList, GraphUiContext context)
{
var canvas = context.View;
var section = magSection.Section;
var area = section.Collapsed
? ImRect.RectWithSize(section.PosOnCanvas, new Vector2(section.Size.X, MagGraphItem.LineHeight))
: ImRect.RectWithSize(section.PosOnCanvas, section.Size);
if (!IsRectVisible(area))
return;
// ImGui 1.91.2 ID-conflict guard: ensure all widgets in this section
// get a unique ID per section so the "##sectionHeader" / "##resize"
// labels don't collide between sections.
ImGui.PushID(magSection.Id.GetHashCode());
var pMin = TransformPosition(magSection.DampedPosOnCanvas);
var dampedSize = section.Collapsed
? new Vector2(magSection.DampedSize.X, MagGraphItem.LineHeight)
: magSection.DampedSize;
var pMax = TransformPosition(magSection.DampedPosOnCanvas + dampedSize);
drawList.PushClipRect(pMin, pMax, true); // Start with a simple rectangular clip
// Background
var backgroundColor = ColorVariations.SectionBackground.Apply(section.Color).Fade(0.8f);
var rounding = 8; // * canvas.Scale.X;
var flags = ImDrawFlags.RoundCornersTop | ImDrawFlags.RoundCornersBottomLeft;
drawList.AddRectFilled(pMin + Vector2.One,
pMax,
backgroundColor,
rounding, flags);
var isNodeSelected = context.Selector.IsNodeSelected(section);
// Outline
var borderColor = isNodeSelected
? UiColors.ForegroundFull
: ColorVariations.SectionOutline.Apply(section.Color);
drawList.AddRect(pMin,
pMax,
borderColor.Fade(_context.GraphOpacity),
rounding,
flags);
// Keep height of title area at a minimum height when zooming out
var screenArea = new ImRect(pMin, pMax);
var clickableArea = new ImRect(pMin, pMax);
clickableArea.Max.Y = clickableArea.Min.Y + MathF.Min(16 * T3Ui.UiScaleFactor, screenArea.GetHeight());
var canvasScale = canvas.Scale.X;
// The title font size also drives the collapse toggle and the label indent
float titleFontSize = 0;// Fonts.FontLarge.FontSize;
if (section.Collapsed)
{
titleFontSize = canvasScale > 1
? Fonts.FontLarge.FontSize
: canvasScale > 1f / Fonts.FontLarge.Scale
? Fonts.FontLarge.FontSize
: Fonts.FontLarge.FontSize * canvasScale * 1;
}
else
{
titleFontSize = canvasScale > 1
? Fonts.FontLarge.FontSize
: canvasScale > 0.333f / Fonts.FontLarge.Scale
? Fonts.FontLarge.FontSize
: Fonts.FontLarge.FontSize * canvasScale * 3;
}
// Collapse toggle - scaled with the title font, with a clickable minimum when zoomed out.
// Emitted before header and resize handles so it wins their overlap zones.
var toggleSize = MathF.Max(titleFontSize, 13 * T3Ui.UiScaleFactor);
{
var togglePos = pMin + new Vector2(4, 3) * T3Ui.UiScaleFactor;
ImGui.SetCursorScreenPos(togglePos);
if (ImGui.InvisibleButton("##collapse", new Vector2(toggleSize, toggleSize)))
{
ToggleSectionCollapse(section, context);
}
var toggleColor = ColorVariations.OperatorLabel.Apply(section.Color)
.Fade(ImGui.IsItemHovered() ? 1 : 0.7f);
Icons.DrawIconAtScreenPosition(section.Collapsed ? Icon.ChevronRight : Icon.ChevronDown,
togglePos,
new Vector2(toggleSize, toggleSize),
drawList,
toggleColor);
}
// Resize handles on all edges and corners (ImGui gives hover to the first item
// submitted at a position, so corners come before edges, and all before the header)
if (!section.Collapsed)
{
var corner = 13 * T3Ui.UiScaleFactor;
var edge = 5 * T3Ui.UiScaleFactor;
// On small frames only the bottom-right corner is offered - full handles
// would swallow most of the frame's clickable area
var largeEnough = screenArea.GetWidth() > 6 * corner && screenArea.GetHeight() > 4 * corner;
if (largeEnough)
{
// No top-left corner handle - the collapse toggle occupies that spot;
// top-left resizing works via the left and top edge strips
EmitSectionResizeHandle(context, magSection, "##resizeTR", new ImRect(new Vector2(pMax.X - corner, pMin.Y), new Vector2(pMax.X, pMin.Y + corner)), SectionResizing.Handles.TopRight);
EmitSectionResizeHandle(context, magSection, "##resizeBL", new ImRect(new Vector2(pMin.X, pMax.Y - corner), new Vector2(pMin.X + corner, pMax.Y)), SectionResizing.Handles.BottomLeft);
}
EmitSectionResizeHandle(context, magSection, "##resizeBR", new ImRect(pMax - new Vector2(corner, corner), pMax), SectionResizing.Handles.BottomRight);
if (largeEnough)
{
EmitSectionResizeHandle(context, magSection, "##resizeL", new ImRect(new Vector2(pMin.X, pMin.Y + corner), new Vector2(pMin.X + edge, pMax.Y - corner)), SectionResizing.Handles.Left);
EmitSectionResizeHandle(context, magSection, "##resizeR", new ImRect(new Vector2(pMax.X - edge, pMin.Y + corner), new Vector2(pMax.X, pMax.Y - corner)), SectionResizing.Handles.Right);
EmitSectionResizeHandle(context, magSection, "##resizeT", new ImRect(new Vector2(pMin.X + corner, pMin.Y - edge), new Vector2(pMax.X - corner, pMin.Y - 1)), SectionResizing.Handles.Top);
EmitSectionResizeHandle(context, magSection, "##resizeB", new ImRect(new Vector2(pMin.X + corner, pMax.Y - edge), new Vector2(pMax.X - corner, pMax.Y)), SectionResizing.Handles.Bottom);
}
// Bottom-right affordance triangle
drawList.AddTriangleFilled(screenArea.Max - new Vector2(11, 1) * T3Ui.UiScaleFactor,
screenArea.Max - new Vector2(1, 11) * T3Ui.UiScaleFactor,
screenArea.Max - new Vector2(1, 1) * T3Ui.UiScaleFactor,
UiColors.BackgroundButton);
}
ImGui.SetCursorScreenPos(clickableArea.Min);
// When zoomed in so the frame covers most of the view, the user is working *inside*
// the section - an invisible header row along the top edge would swallow clicks
// meant for fence-selection (issue #1037). Grabbing it again requires zooming out.
var visibleWidth = MathF.Min(pMax.X, canvas.WindowPos.X + canvas.WindowSize.X) - MathF.Max(pMin.X, canvas.WindowPos.X);
var visibleHeight = MathF.Min(pMax.Y, canvas.WindowPos.Y + canvas.WindowSize.Y) - MathF.Max(pMin.Y, canvas.WindowPos.Y);
var viewCoverage = MathF.Max(0, visibleWidth) * MathF.Max(0, visibleHeight)
/ (canvas.WindowSize.X * canvas.WindowSize.Y);
var headerEnabled = viewCoverage < 0.7f;
var isRenaming = context.ActiveSectionId == magSection.Id &&
context.StateMachine.CurrentState == GraphStates.RenameSection;
if (!isRenaming && headerEnabled)
{
var headerSize = clickableArea.GetSize();
if (headerSize.X < 1f) headerSize.X = 1f;
if (headerSize.Y < 1f) headerSize.Y = 1f;
ImGui.InvisibleButton("##sectionHeader", headerSize);
DrawUtils.DebugItemRect();
var isHeaderHovered = ImGui.IsItemHovered() && context.StateMachine.CurrentState == GraphStates.Default;
if (isHeaderHovered)
{
ImGui.SetMouseCursor(ImGuiMouseCursor.Hand);
}
const float headerHoverAlpha = 0.1f;
drawList.AddRectFilled(clickableArea.Min, clickableArea.Max,
UiColors.ForegroundFull.Fade(isHeaderHovered
? headerHoverAlpha
: 0), rounding, ImDrawFlags.RoundCornersTop);
// Clicked -> Drag
if (ImGui.IsItemClicked(ImGuiMouseButton.Left) && !ImGui.GetIO().KeyAlt)
{
context.ActiveSectionId = magSection.Id;
context.StateMachine.SetState(GraphStates.DragSection, context);
}
// Double-Click -> Rename
if (ImGui.IsItemHovered() && ImGui.IsMouseDoubleClicked(ImGuiMouseButton.Left))
{
context.ActiveSectionId = magSection.Id;
context.StateMachine.SetState(GraphStates.RenameSection, context);
}
}
// Label and description
if (context.ActiveSectionId != magSection.Id || context.StateMachine.CurrentState != GraphStates.RenameSection)
{
var labelHeight = 0f;
{
if (!string.IsNullOrEmpty(section.Label))
{
var fade = MathUtils.SmootherStep(0.1f, 0.2f, canvasScale) * 0.8f * _context.GraphOpacity;
drawList.AddText(Fonts.FontLarge,
titleFontSize,
pMin + new Vector2(toggleSize + 8 * T3Ui.UiScaleFactor, 3 * T3Ui.UiScaleFactor),
ColorVariations.OperatorLabel.Apply(section.Color.Fade(fade)),
section.Label);
labelHeight = Fonts.FontLarge.FontSize;
}
}
if (!string.IsNullOrEmpty(section.Title))
{
var font = section.Title.StartsWith("# ") ? Fonts.FontLarge : Fonts.FontNormal;
drawList.PushClipRect(pMin, pMax, true);
var labelPos = pMin + new Vector2(8, 8 + labelHeight) * T3Ui.UiScaleFactor;
var fade = MathUtils.SmootherStep(0.25f, 0.6f, canvasScale) * 0.8f;
var fontSize = canvasScale > 1
? font.FontSize
: canvasScale > Fonts.FontSmall.Scale / Fonts.FontNormal.Scale
? font.FontSize
: font.FontSize * canvasScale;
drawList.AddText(font,
fontSize,
labelPos,
ColorVariations.OperatorLabel.Apply(section.Color.Fade(fade)),
section.Title);
drawList.PopClipRect();
}
}
drawList.PopClipRect();
ImGui.PopID(); // outer per-section PushID
}
/// <summary>
/// Collapsing just folds the frame. Expanding reveals the stored bounds again and
/// pushes neighboring frames and loose ops below out of the way — flag flip and
/// pushes form one undoable unit.
/// </summary>
private static void ToggleSectionCollapse(Section section, GraphUiContext context)
{
var symbolUi = context.CompositionInstance.GetSymbolUi();
if (!section.Collapsed)
{
UndoRedoStack.AddAndExecute(new ChangeSectionCollapseCommand(symbolUi, section, collapsed: true));
}
else
{
var macro = new MacroCommand("Expand Section");
macro.AddAndExecCommand(new ChangeSectionCollapseCommand(symbolUi, section, collapsed: false));
var expandedBounds = ImRect.RectWithSize(section.PosOnCanvas, section.Size);
SectionTree.ResolveBoundsExpansion(symbolUi, section, expandedBounds, Vector2.UnitY, macro, context.Selector);
UndoRedoStack.Add(macro);
}
context.Layout.FlagStructureAsChanged();
}
private static void EmitSectionResizeHandle(GraphUiContext context, MagGraphSection magSection, string id, ImRect rect,
SectionResizing.Handles handles)
{
if (rect.GetWidth() < 1f || rect.GetHeight() < 1f)
return;
ImGui.SetCursorScreenPos(rect.Min);
ImGui.InvisibleButton(id, rect.GetSize());
if (ImGui.IsItemHovered())
{
ImGui.SetMouseCursor(SectionResizing.GetCursorForHandles(handles));
ImGui.GetWindowDrawList().AddRectFilled(rect.Min, rect.Max, UiColors.ForegroundFull.Fade(0.1f));
}
if (!ImGui.IsItemClicked(ImGuiMouseButton.Left))
return;
SectionResizing.ActiveHandles = handles;
context.ActiveSectionId = magSection.Id;
context.StateMachine.SetState(GraphStates.ResizeSection, context);
}
}