Files
MycLib/ASTPlayground/Myc.Fmx.AstVisualizer.pas
T
Michael Schimmel f1735e2678 New visualizer
2025-10-29 23:07:49 +01:00

1331 lines
58 KiB
ObjectPascal

unit Myc.Fmx.AstVisualizer deprecated;
interface
uses
System.SysUtils,
System.Classes,
System.UITypes,
System.Types,
System.Math.Vectors,
System.Generics.Collections,
System.Generics.Defaults,
FMX.Types,
FMX.Controls,
FMX.Objects,
FMX.Graphics,
FMX.StdCtrls,
Myc.Data.Value,
Myc.Ast.Nodes,
Myc.Ast.Scope,
Myc.Ast.Types,
Myc.Ast.Visitor,
Myc.Fmx.AstEditor.Node,
Myc.Fmx.AstEditor.Workspace;
type
// Visits the AST and creates visual TAuraNode representations on a TAuraWorkspace.
TAstVisualizer = class(TAstVisitor, IAstVisualizer)
private
FCurrentPos: TPointF;
FExprDepth: Integer;
FWorkspace: TAuraWorkspace;
FNodeVerticalGap: Single;
FParentControl: TControl;
// Helper to create and initialize a TAuraNode for a given AST node.
function CreateNode(const Node: IAstNode): TAuraNode;
function AddLabel(const ParentNode: TAuraNode; const Txt: String): TLabel;
function AddTitleLabel(const ParentNode: TAuraNode; const Title: string): Single;
// Adjusts the size of the parent node to fit its children.
procedure AdjustParentNodeSize(const ParentNode: TAuraNode; const Children: TArray<IAstNode>);
// Generic processing logic for AST nodes with children. Returns the created TAuraNode.
function ProcessStub(const ATitle: string; const ANode: IAstNode; const AChildren: TArray<IAstNode>): TAuraNode;
// Calls Accept on the node and returns the resulting TAuraNode.
function CallAccept(const Node: IAstNode): TAuraNode;
function GetCurrentPos: TPointF;
function GetExprDepth: Integer;
function GetParentControl: TControl;
procedure SetCurrentPos(const Value: TPointF);
procedure SetExprDepth(const Value: Integer);
procedure SetParentControl(const Value: TControl);
protected
// Visitor implementations for each AST node type.
function VisitConstant(const Node: IConstantNode): TDataValue; override;
function VisitIdentifier(const Node: IIdentifierNode): TDataValue; override;
function VisitBinaryExpression(const Node: IBinaryExpressionNode): TDataValue; override;
function VisitUnaryExpression(const Node: IUnaryExpressionNode): TDataValue; override;
function VisitIfExpression(const Node: IIfExpressionNode): TDataValue; override;
function VisitTernaryExpression(const Node: ITernaryExpressionNode): TDataValue; override;
function VisitLambdaExpression(const Node: ILambdaExpressionNode): TDataValue; override;
function VisitFunctionCall(const Node: IFunctionCallNode): TDataValue; override;
function VisitMacroExpansionNode(const Node: IMacroExpansionNode): TDataValue; override;
function VisitBlockExpression(const Node: IBlockExpressionNode): TDataValue; override;
function VisitVariableDeclaration(const Node: IVariableDeclarationNode): TDataValue; override;
function VisitAssignment(const Node: IAssignmentNode): TDataValue; override;
function VisitMacroDefinition(const Node: IMacroDefinitionNode): TDataValue; override;
function VisitQuasiquote(const Node: IQuasiquoteNode): TDataValue; override;
function VisitUnquote(const Node: IUnquoteNode): TDataValue; override;
function VisitUnquoteSplicing(const Node: IUnquoteSplicingNode): TDataValue; override;
function VisitIndexer(const Node: IIndexerNode): TDataValue; override;
function VisitMemberAccess(const Node: IMemberAccessNode): TDataValue; override;
function VisitCreateSeries(const Node: ICreateSeriesNode): TDataValue; override;
function VisitAddSeriesItem(const Node: IAddSeriesItemNode): TDataValue; override;
function VisitSeriesLength(const Node: ISeriesLengthNode): TDataValue; override;
function VisitRecurNode(const Node: IRecurNode): TDataValue; override;
public
constructor Create(
AWorkspace: TAuraWorkspace;
AParentControl: TControl;
const AStartPosition: TPointF;
ANodeVerticalGap: Single = 15;
AExprDepth: Integer = 0
);
destructor Destroy; override;
function Clone: IAstVisualizer;
class function CreateVisitor(AWorkspace: TAuraWorkspace; const StartPosition: TPointF): IAstVisitor;
property CurrentPos: TPointF read GetCurrentPos write SetCurrentPos;
property ExprDepth: Integer read GetExprDepth write SetExprDepth;
property ParentControl: TControl read GetParentControl write SetParentControl;
end;
implementation
uses
System.Math,
System.StrUtils,
FMX.Platform,
FMX.Layouts, // For TLayout
Myc.Data.Scalar,
Myc.Ast.Binding, // For TBound...Node classes
Myc.Fmx.AstEditor.Text;
const
cChildAreaTopPadding = 5;
cNodeVerticalGap = 10; // Reduced gap
{ TAstVisualizer }
constructor TAstVisualizer.Create(
AWorkspace: TAuraWorkspace;
AParentControl: TControl;
const AStartPosition: TPointF;
ANodeVerticalGap: Single = 15;
AExprDepth: Integer = 0
);
begin
inherited Create;
FWorkspace := AWorkspace;
FParentControl := AParentControl;
FCurrentPos := AStartPosition;
FNodeVerticalGap := ANodeVerticalGap;
FExprDepth := AExprDepth;
end;
destructor TAstVisualizer.Destroy;
begin
inherited;
end;
function TAstVisualizer.AddLabel(const ParentNode: TAuraNode; const Txt: String): TLabel;
begin
Result := TLabel.Create(ParentNode); // Owner is the node itself
Result.Parent := ParentNode;
Result.Position.Point := TPoint.Create(cNodePadding, cNodePadding);
Result.Margins.Left := cNodePadding;
Result.Margins.Right := cNodePadding;
Result.Margins.Top := cTitleTopPadding;
Result.Margins.Bottom := cTitleBottomPadding;
Result.WordWrap := false;
Result.AutoSize := True; // Let label determine its own size initially (do NOT use Align, because it will override AutoSize)
Result.HitTest := False; // Title label should not intercept clicks intended for the node
Result.StyledSettings := Result.StyledSettings - [TStyledSetting.Style]; // Tell FMX not to manage Font.Style via styles
Result.Text := Txt;
Result.ApplyStyleLookup;
end;
class function TAstVisualizer.CreateVisitor(AWorkspace: TAuraWorkspace; const StartPosition: TPointF): IAstVisitor;
begin
Result := TAstVisualizer.Create(AWorkspace, AWorkspace, StartPosition);
end;
// Creates a TAuraNode, sets its parent and position. Title is added separately.
function TAstVisualizer.CreateNode(const Node: IAstNode): TAuraNode;
begin
Result := TAuraNode.Create(FWorkspace); // Owner is always the workspace
Result.Parent := FParentControl; // Set visual parent
Result.Position.Point := FCurrentPos;
// Set a default minimum size, will be adjusted later
Result.Width := cMinNodeWidth;
Result.Height := cMinNodeHeight;
var cn: cardinal := $ea - (7 * FExprDepth);
var c: cardinal := $ff000000 or (cn shl 16) or (cn shl 8) or cn;
Result.BackgroundColor := c;
// Store the original AST node reference if needed later
// Result.TagObject := TObject(Node);
end;
// Creates and adds a TLabel for the title to the given ParentNode.
// Returns the calculated height of the title area (label + margins).
function TAstVisualizer.AddTitleLabel(const ParentNode: TAuraNode; const Title: string): Single;
var
titleLabel: TLabel;
begin
Result := 0;
if Title.IsEmpty then
Exit;
titleLabel := AddLabel(ParentNode, Title);
titleLabel.Font.Style := titleLabel.Font.Style + [TFontStyle.fsBold];
// Force immediate resize calculation for height measurement (usually not needed with Align=Top + AutoSize)
// titleLabel.ApplyStyleLookup; titleLabel.Realign;
Result := titleLabel.Height + titleLabel.Margins.Top + titleLabel.Margins.Bottom;
end;
// Adjusts the parent node's size after all children have been added,
// considering both labels and child AuraNodes.
procedure TAstVisualizer.AdjustParentNodeSize(const ParentNode: TAuraNode; const Children: TArray<IAstNode>);
var
i: Integer;
maxLabelWidth: Single;
maxChildNodeWidth: Single;
totalChildHeight: Single;
childControl: TControl;
isLabel: Boolean;
requiredWidth: Single;
begin
maxLabelWidth := 0;
maxChildNodeWidth := 0;
totalChildHeight := cNodePadding; // Start with top padding
// Iterate through FMX children of the ParentNode to calculate required size
for i := 0 to ParentNode.ControlsCount - 1 do
begin
childControl := ParentNode.Controls[i];
isLabel := childControl is TLabel;
// Calculate required width for this child (including its margins)
requiredWidth := childControl.Width + childControl.Margins.Left + childControl.Margins.Right;
if isLabel then
begin
maxLabelWidth := Max(maxLabelWidth, requiredWidth);
end
// Also consider TAuraNode children for width calculation, assuming they also have padding
else if childControl is TAuraNode then
begin
// Child nodes are positioned starting at cNodePadding, so their width relative to parent border is Width + cNodePadding
// We compare against the required width including parent padding
maxChildNodeWidth := Max(maxChildNodeWidth, childControl.Width);
end;
// Height calculation based on the last child's bottom edge relative to parent's top
totalChildHeight := Max(totalChildHeight, childControl.Position.Y + childControl.Height + childControl.Margins.Bottom);
end;
// Set parent size including padding
// Ensure minimum size is met AND the widest label fits AND the widest child node fits
ParentNode.Width :=
Max(
cMinNodeWidth, // At least min width
Max(
maxLabelWidth, // At least widest label width + padding (already included in label width calc)
maxChildNodeWidth + 2 * cNodePadding
)
); // At least widest child node width + padding
// Adjust height based on the bottom-most control + bottom padding
if ParentNode.ControlsCount > 0 then
ParentNode.Height := Max(cMinNodeHeight, totalChildHeight + cNodePadding)
else // Empty node
ParentNode.Height := cMinNodeHeight;
end;
// Calls Accept on the node and returns the resulting TAuraNode.
function TAstVisualizer.CallAccept(const Node: IAstNode): TAuraNode;
var
DataValue: TDataValue;
begin
if not Assigned(Node) then
Exit(nil); // Handle nil input node gracefully
DataValue := Node.Accept(Self);
// Check if the result is actually a TAuraNode before casting
if DataValue.Kind = TDataValueKind.vkGeneric then
begin
// It's recommended to add a type check here if possible,
// although AsGeneric doesn't inherently support it.
// Assuming the visitor correctly returns TAuraNode wrapped in TDataValue.
Result := DataValue.AsGeneric<TAuraNode>;
end
else
begin
// The visitor returned something unexpected.
// Decide how to handle this: raise an exception, return nil, log an error?
// For now, return nil as the caller might handle it.
Result := nil;
// Consider: Log.d('TAstVisualizer.CallAccept', 'Expected TAuraNode, got ' + DataValue.Kind.ToString);
// Consider: raise EMyAstError.Create('Visitor did not return a TAuraNode');
end;
end;
function TAstVisualizer.Clone: IAstVisualizer;
begin
Result := TAstVisualizer.Create(FWorkspace, FParentControl, FCurrentPos, FNodeVerticalGap, FExprDepth);
end;
function TAstVisualizer.GetCurrentPos: TPointF;
begin
Result := FCurrentPos;
end;
function TAstVisualizer.GetExprDepth: Integer;
begin
Result := FExprDepth;
end;
function TAstVisualizer.GetParentControl: TControl;
begin
Result := FParentControl;
end;
// Generic processing logic for AST nodes with children. Returns the created TAuraNode.
function TAstVisualizer.ProcessStub(const ATitle: string; const ANode: IAstNode; const AChildren: TArray<IAstNode>): TAuraNode;
var
auraNode: TAuraNode;
savedParent: TControl;
savedPos: TPointF;
child: IAstNode;
titleLabelHeight: Single;
begin
auraNode := CreateNode(ANode); // Create node without title
auraNode.BeginUpdate;
try
// Add Title Label
titleLabelHeight := AddTitleLabel(auraNode, ATitle);
savedParent := FParentControl;
savedPos := FCurrentPos;
FParentControl := auraNode;
// Start position for children inside the new node, below the title
FCurrentPos := TPointF.Create(cNodePadding, titleLabelHeight + cChildAreaTopPadding);
// Visit children
for child in AChildren do
begin
if Assigned(child) then // Ensure child node exists
{childNode :=}
CallAccept(child); // Use CallAccept, store result
// childNode is not explicitly used here for layout *after* the call,
// but its creation and positioning happened *within* CallAccept.
end;
FParentControl := savedParent; // Restore parent
FCurrentPos := savedPos; // Restore position for siblings
AdjustParentNodeSize(auraNode, AChildren);
finally
auraNode.EndUpdate;
end;
// Move current position down for the next sibling node in the outer scope
// Use the actual size of the created node
FCurrentPos.Y := savedPos.Y + auraNode.Height + FNodeVerticalGap;
Result := auraNode; // Return the created node
end;
procedure TAstVisualizer.SetCurrentPos(const Value: TPointF);
begin
FCurrentPos := Value;
end;
procedure TAstVisualizer.SetExprDepth(const Value: Integer);
begin
FExprDepth := Value;
end;
procedure TAstVisualizer.SetParentControl(const Value: TControl);
begin
FParentControl := Value;
end;
// --- Visitor Implementations ---
function TAstVisualizer.VisitConstant(const Node: IConstantNode): TDataValue;
begin
var auraNode := TAuraConstantNode.CreateConstantNode(FWorkspace, Node, Self);
// Move current position down for the next sibling node
FCurrentPos.Y := FCurrentPos.Y + auraNode.Height + FNodeVerticalGap;
Result := TDataValue.FromGeneric<TAuraNode>(auraNode); // Return the created node
end;
function TAstVisualizer.VisitIdentifier(const Node: IIdentifierNode): TDataValue;
var
auraNode: TAuraIdentifierNode; // Use specific type
begin
// Create the specific node type using its factory
auraNode := TAuraIdentifierNode.CreateIdentifierNode(FWorkspace, Node, Self);
// Move current position down for the next sibling node
FCurrentPos.Y := FCurrentPos.Y + auraNode.Height + FNodeVerticalGap;
Result := TDataValue.FromGeneric<TAuraNode>(auraNode); // Return as generic TAuraNode
end;
function TAstVisualizer.VisitBinaryExpression(const Node: IBinaryExpressionNode): TDataValue;
var
auraNode: TAuraBinaryExpressionNode; // Use specific type
savedPos: TPointF;
begin
// Store position before creating the node
savedPos := FCurrentPos;
// Create the specific node type using its factory
auraNode := TAuraBinaryExpressionNode.CreateBinaryExpressionNode(FWorkspace, Node, Self);
// Move current position down for the next sibling node
// Use the position saved *before* creating this node, plus the node's final height
FCurrentPos.Y := savedPos.Y + auraNode.Height + FNodeVerticalGap;
Result := TDataValue.FromGeneric<TAuraNode>(auraNode); // Return as generic TAuraNode
end;
function TAstVisualizer.VisitUnaryExpression(const Node: IUnaryExpressionNode): TDataValue;
var
auraNode: TAuraNode;
begin
auraNode := ProcessStub('Unary Op: ' + Node.Operator.ToString, Node, [Node.Right]);
Result := TDataValue.FromGeneric<TAuraNode>(auraNode);
end;
function TAstVisualizer.VisitIfExpression(const Node: IIfExpressionNode): TDataValue;
var
children: TArray<IAstNode>;
auraNode: TAuraNode;
begin
children := [Node.Condition, Node.ThenBranch];
if Assigned(Node.ElseBranch) then
children := children + [Node.ElseBranch];
auraNode := ProcessStub('If', Node, children);
Result := TDataValue.FromGeneric<TAuraNode>(auraNode);
end;
function TAstVisualizer.VisitTernaryExpression(const Node: ITernaryExpressionNode): TDataValue;
var
auraNode: TAuraNode;
conditionNode: TAuraNode;
qMarkLabel: TLabel;
thenNode: TAuraNode;
colonLabel: TLabel;
elseNode: TAuraNode;
savedParent: TControl;
savedPos: TPointF;
currentX: Single; // Track horizontal position
maxHeightInRow: Single; // Track max height for row alignment
yPos: Single; // Y position for elements in this row
begin
// Create the main node, frameless
auraNode := CreateNode(Node);
auraNode.BeginUpdate;
inc(FExprDepth);
try
auraNode.Frameless := True;
// Start position for children inside the node
currentX := cNodePadding;
yPos := cNodePadding; // Initial Y position
maxHeightInRow := 0;
// 1. Condition Node
savedParent := FParentControl;
savedPos := FCurrentPos;
FParentControl := auraNode;
FCurrentPos := TPointF.Create(currentX, yPos);
conditionNode := CallAccept(Node.Condition);
if Assigned(conditionNode) then
begin
currentX := conditionNode.Position.X + conditionNode.Width + conditionNode.Margins.Right + cNodePadding;
maxHeightInRow := Max(maxHeightInRow, conditionNode.Height + conditionNode.Margins.Top + conditionNode.Margins.Bottom);
end;
// 2. Question Mark Label
qMarkLabel := AddLabel(auraNode, '?');
qMarkLabel.Position.Point := TPointF.Create(currentX, yPos);
currentX := qMarkLabel.Position.X + qMarkLabel.Width + qMarkLabel.Margins.Right + cNodePadding;
maxHeightInRow := Max(maxHeightInRow, qMarkLabel.Height + qMarkLabel.Margins.Top + qMarkLabel.Margins.Bottom);
// 3. Then Branch Node
// FParentControl is already auraNode
FCurrentPos := TPointF.Create(currentX, yPos);
thenNode := CallAccept(Node.ThenBranch);
if Assigned(thenNode) then
begin
currentX := thenNode.Position.X + thenNode.Width + thenNode.Margins.Right + cNodePadding;
maxHeightInRow := Max(maxHeightInRow, thenNode.Height + thenNode.Margins.Top + thenNode.Margins.Bottom);
end;
// 4. Colon Label
colonLabel := AddLabel(auraNode, ':');
colonLabel.Position.Point := TPointF.Create(currentX, yPos);
currentX := colonLabel.Position.X + colonLabel.Width + colonLabel.Margins.Right + cNodePadding;
maxHeightInRow := Max(maxHeightInRow, colonLabel.Height + colonLabel.Margins.Top + colonLabel.Margins.Bottom);
// 5. Else Branch Node
// FParentControl is already auraNode
FCurrentPos := TPointF.Create(currentX, yPos);
elseNode := CallAccept(Node.ElseBranch);
if Assigned(elseNode) then
begin
currentX := elseNode.Position.X + elseNode.Width + elseNode.Margins.Right;
maxHeightInRow := Max(maxHeightInRow, elseNode.Height + elseNode.Margins.Top + elseNode.Margins.Bottom);
end;
FParentControl := savedParent;
FCurrentPos := savedPos;
// --- Adjust parent size ---
auraNode.Width := Max(cMinNodeWidth, currentX + cNodePadding);
auraNode.Height := Max(cMinNodeHeight, maxHeightInRow + 2 * cNodePadding);
// --- Optional Vertical Centering ---
var centerOffsetY: Single := (auraNode.Height / 2);
if Assigned(conditionNode) then
conditionNode.Position.Y :=
centerOffsetY
- (conditionNode.Height + conditionNode.Margins.Top + conditionNode.Margins.Bottom) / 2
+ conditionNode.Margins.Top;
if Assigned(qMarkLabel) then
qMarkLabel.Position.Y :=
centerOffsetY - (qMarkLabel.Height + qMarkLabel.Margins.Top + qMarkLabel.Margins.Bottom) / 2 + qMarkLabel.Margins.Top;
if Assigned(thenNode) then
thenNode.Position.Y :=
centerOffsetY - (thenNode.Height + thenNode.Margins.Top + thenNode.Margins.Bottom) / 2 + thenNode.Margins.Top;
if Assigned(colonLabel) then
colonLabel.Position.Y :=
centerOffsetY - (colonLabel.Height + colonLabel.Margins.Top + colonLabel.Margins.Bottom) / 2 + colonLabel.Margins.Top;
if Assigned(elseNode) then
elseNode.Position.Y :=
centerOffsetY - (elseNode.Height + elseNode.Margins.Top + elseNode.Margins.Bottom) / 2 + elseNode.Margins.Top;
// --- End Optional Vertical Centering ---
finally
dec(FExprDepth);
auraNode.EndUpdate;
end;
// Move current position down for the next sibling node in the outer scope
FCurrentPos.Y := savedPos.Y + auraNode.Height + FNodeVerticalGap;
Result := TDataValue.FromGeneric<TAuraNode>(auraNode);
end;
function TAstVisualizer.VisitLambdaExpression(const Node: ILambdaExpressionNode): TDataValue;
var
auraNode: TAuraNode;
savedParent: TControl;
savedPos: TPointF;
savedExprDepth: Integer;
i: Integer;
paramLabel: TLabel;
lambdaLabel: TLabel; // Label for the lambda symbol
titleParamHeight: Single; // Height required for the combined title/param line
currentX: Single; // Horizontal position tracker for title/params
maxHeightInTitleRow: Single; // Max height in the first row
lambdaSymbol: string;
bodyNode: TAuraNode;
begin
lambdaSymbol := WideChar($03BB); // Unicode for Greek Small Letter Lambda
auraNode := CreateNode(Node); // Create standard framed node
auraNode.BeginUpdate;
try
auraNode.Frameless := False;
auraNode.BackgroundColor := $090000ff;
// --- Create Title/Parameter Line ---
currentX := cNodePadding;
maxHeightInTitleRow := 0;
var yPos := cNodePadding; // Y position for the first row
// 1. Lambda Symbol Label (Title)
lambdaLabel := AddLabel(auraNode, lambdaSymbol);
lambdaLabel.Position.Point := TPointF.Create(currentX, yPos);
lambdaLabel.Font.Style := lambdaLabel.Font.Style + [TFontStyle.fsBold]; // Make symbol bold
currentX := lambdaLabel.Position.X + lambdaLabel.Width + lambdaLabel.Margins.Right + cNodePadding / 2; // Add small gap
maxHeightInTitleRow := Max(maxHeightInTitleRow, lambdaLabel.Height + lambdaLabel.Margins.Top + lambdaLabel.Margins.Bottom);
// 2. Add Parameter Labels (inline)
if High(Node.Parameters) >= 0 then
begin
// Add parenthesis
var openParen := AddLabel(auraNode, '(');
openParen.Font.Style := openParen.Font.Style - [TFontStyle.fsBold]; // Normal style
openParen.Position.Point := TPointF.Create(currentX, yPos);
currentX := openParen.Position.X + openParen.Width + openParen.Margins.Right + cNodePadding / 2;
maxHeightInTitleRow := Max(maxHeightInTitleRow, openParen.Height + openParen.Margins.Top + openParen.Margins.Bottom);
for i := 0 to High(Node.Parameters) do
begin
paramLabel := AddLabel(auraNode, Node.Parameters[i].Name);
paramLabel.Font.Style := paramLabel.Font.Style - [TFontStyle.fsBold]; // Normal style
paramLabel.Position.Point := TPointF.Create(currentX, yPos);
currentX :=
paramLabel.Position.X
+ paramLabel.Width
+ paramLabel.Margins.Right
+ IfThen(i < High(Node.Parameters), cNodePadding / 2, 0); // Gap or no gap
maxHeightInTitleRow := Max(maxHeightInTitleRow, paramLabel.Height + paramLabel.Margins.Top + paramLabel.Margins.Bottom);
// Add comma if not the last parameter
if i < High(Node.Parameters) then
begin
var commaLabel := AddLabel(auraNode, ',');
commaLabel.Font.Style := commaLabel.Font.Style - [TFontStyle.fsBold]; // Normal style
commaLabel.Position.Point := TPointF.Create(currentX, yPos);
currentX := commaLabel.Position.X + commaLabel.Width + commaLabel.Margins.Right + cNodePadding / 2;
maxHeightInTitleRow := Max(maxHeightInTitleRow, commaLabel.Height + commaLabel.Margins.Top + commaLabel.Margins.Bottom);
end;
end;
// Add closing parenthesis
var closeParen := AddLabel(auraNode, ')');
closeParen.Font.Style := closeParen.Font.Style - [TFontStyle.fsBold]; // Normal style
closeParen.Position.Point := TPointF.Create(currentX, yPos);
maxHeightInTitleRow := Max(maxHeightInTitleRow, closeParen.Height + closeParen.Margins.Top + closeParen.Margins.Bottom);
end
else
begin
// No parameters, add empty () for clarity
var emptyParens := AddLabel(auraNode, '()');
emptyParens.Font.Style := emptyParens.Font.Style - [TFontStyle.fsBold];
emptyParens.Position.Point := TPointF.Create(currentX, yPos);
maxHeightInTitleRow := Max(maxHeightInTitleRow, emptyParens.Height + emptyParens.Margins.Top + emptyParens.Margins.Bottom);
end;
titleParamHeight := maxHeightInTitleRow; // Total height needed for the first row
// --- Optional Vertical Centering for Title/Param Row ---
var centerOffsetYTitleRow: Single := yPos + titleParamHeight / 2;
for i := 0 to auraNode.ControlsCount - 1 do // Iterate controls created so far for the title row
begin
var ctrl := auraNode.Controls[i];
ctrl.Position.Y := centerOffsetYTitleRow - (ctrl.Height + ctrl.Margins.Top + ctrl.Margins.Bottom) / 2 + ctrl.Margins.Top;
end;
// --- End Centering ---
// 3. Visit Body
savedParent := FParentControl;
savedPos := FCurrentPos;
savedExprDepth := FExprDepth;
try
FParentControl := auraNode; // Body node is child of auraNode
// Start position for the body, below the title/parameter line
FCurrentPos :=
TPointF.Create(cNodePadding, yPos + titleParamHeight + cChildAreaTopPadding); // Start below the calculated height
FExprDepth := 0;
bodyNode := CallAccept(Node.Body); // Visit the body, get the node
bodyNode.Frameless := false;
finally
FParentControl := savedParent; // Restore parent for siblings
FCurrentPos := savedPos; // Restore position for siblings
FExprDepth := savedExprDepth;
end;
AdjustParentNodeSize(auraNode, [Node.Body]); // Adjust size based on title, params, and body
finally
auraNode.EndUpdate;
end;
// Move current position down for the next sibling node
FCurrentPos.Y := savedPos.Y + auraNode.Height + FNodeVerticalGap; // Use savedPos.Y
Result := TDataValue.FromGeneric<TAuraNode>(auraNode); // Return the created node
end;
function TAstVisualizer.VisitFunctionCall(const Node: IFunctionCallNode): TDataValue;
var
auraNode: TAuraNode;
savedParent: TControl;
savedPos: TPointF;
i: Integer;
calleeNode: TAuraNode; // Use TAuraNode
argNode: TAuraNode; // Use TAuraNode
parenLabel, commaLabel: TLabel;
// Layout variables
yPos: Single; // Current vertical position for the row
currentX: Single; // Horizontal position tracker
maxHeightInRow: Single; // Max height for row alignment
argStartIndex: Integer; // Index of first argument-related control
begin
auraNode := CreateNode(Node); // Create standard framed node
auraNode.Frameless := False; // Function calls generally have a frame
auraNode.BeginUpdate;
inc(FExprDepth);
try
yPos := cNodePadding; // Start directly with padding
currentX := cNodePadding;
maxHeightInRow := 0;
// --- Visit Callee ---
savedParent := FParentControl;
savedPos := FCurrentPos;
FParentControl := auraNode;
FCurrentPos := TPointF.Create(currentX, yPos); // Position for callee
calleeNode := CallAccept(Node.Callee); // Create callee's visual node
if Assigned(calleeNode) then
begin
// Position callee explicitly (redundant if FCurrentPos worked, but safe)
calleeNode.Position.Point := TPointF.Create(currentX, yPos);
currentX := calleeNode.Position.X + calleeNode.Width + calleeNode.Margins.Right + cNodePadding / 2; // Update X pos
maxHeightInRow := Max(maxHeightInRow, calleeNode.Height + calleeNode.Margins.Top + calleeNode.Margins.Bottom);
end
else
currentX := cNodePadding; // Start parens from left if no callee
// --- Add Arguments (Horizontal Layout) ---
// Add opening parenthesis
parenLabel := AddLabel(auraNode, '(');
parenLabel.Font.Style := parenLabel.Font.Style - [TFontStyle.fsBold]; // Normal style
parenLabel.Position.Point := TPointF.Create(currentX, yPos);
currentX := parenLabel.Position.X + parenLabel.Width + parenLabel.Margins.Right + cNodePadding / 2;
maxHeightInRow := Max(maxHeightInRow, parenLabel.Height + parenLabel.Margins.Top + parenLabel.Margins.Bottom);
for i := 0 to High(Node.Arguments) do
begin
// Set position for the argument node
FCurrentPos := TPointF.Create(currentX, yPos);
// FParentControl is already auraNode
argNode := CallAccept(Node.Arguments[i]); // Create argument node
if Assigned(argNode) then
begin
// Position it correctly (redundant, but safe)
argNode.Position.Point := TPointF.Create(currentX, yPos);
currentX :=
argNode.Position.X
+ argNode.Width
+ argNode.Margins.Right
+ IfThen(i < High(Node.Arguments), cNodePadding / 2, 0); // Gap or no gap after arg
maxHeightInRow := Max(maxHeightInRow, argNode.Height + argNode.Margins.Top + argNode.Margins.Bottom);
// Add comma if not the last argument
if i < High(Node.Arguments) then
begin
commaLabel := AddLabel(auraNode, ',');
commaLabel.Font.Style := commaLabel.Font.Style - [TFontStyle.fsBold]; // Normal style
commaLabel.Position.Point := TPointF.Create(currentX, yPos);
currentX := commaLabel.Position.X + commaLabel.Width + commaLabel.Margins.Right + cNodePadding / 2;
maxHeightInRow := Max(maxHeightInRow, commaLabel.Height + commaLabel.Margins.Top + commaLabel.Margins.Bottom);
end;
end
else // Should not happen
begin
currentX := currentX + cMinNodeWidth + cNodePadding;
maxHeightInRow := Max(maxHeightInRow, cMinNodeHeight);
end;
end;
// Add closing parenthesis
parenLabel := AddLabel(auraNode, ')');
parenLabel.Font.Style := parenLabel.Font.Style - [TFontStyle.fsBold]; // Normal style
parenLabel.Position.Point := TPointF.Create(currentX, yPos);
currentX := parenLabel.Position.X + parenLabel.Width + parenLabel.Margins.Right; // Final X position
maxHeightInRow := Max(maxHeightInRow, parenLabel.Height + parenLabel.Margins.Top + parenLabel.Margins.Bottom);
// --- Vertical Centering for the single row ---
var centerOffsetYRow: Single := yPos + maxHeightInRow / 2;
argStartIndex := 0; // Start check from first control
for i := argStartIndex to auraNode.ControlsCount - 1 do // Iterate ALL controls in the row
begin
var ctrl := auraNode.Controls[i];
// Adjust Y based on calculated center and control's own height/margins
ctrl.Position.Y := centerOffsetYRow - (ctrl.Height + ctrl.Margins.Top + ctrl.Margins.Bottom) / 2 + ctrl.Margins.Top;
end;
// --- End Centering ---
yPos := yPos + maxHeightInRow; // Update yPos based on row height for final size calculation
FParentControl := savedParent; // Restore parent for siblings
FCurrentPos := savedPos; // Restore position for siblings
// --- Adjust Size ---
auraNode.Width := Max(cMinNodeWidth, currentX + cNodePadding); // Width determined by final currentX
auraNode.Height := Max(cMinNodeHeight, yPos + cNodePadding); // Height determined by final yPos
finally
dec(FExprDepth);
auraNode.EndUpdate;
end;
// Move current position down for the next sibling node
FCurrentPos.Y := savedPos.Y + auraNode.Height + FNodeVerticalGap; // Use savedPos.Y
Result := TDataValue.FromGeneric<TAuraNode>(auraNode); // Return the created node
end;
function TAstVisualizer.VisitMacroExpansionNode(const Node: IMacroExpansionNode): TDataValue;
var
auraNode: TAuraNode;
begin
// Visualize the expanded body
auraNode := ProcessStub('Macro Expansion', Node, [Node.ExpandedBody]);
Result := TDataValue.FromGeneric<TAuraNode>(auraNode);
end;
function TAstVisualizer.VisitBlockExpression(const Node: IBlockExpressionNode): TDataValue;
var
auraNode: TAuraNode;
savedParent: TControl;
savedPos: TPointF;
expr: IAstNode;
prefixLabel: TLabel;
prefixText: string;
i: Integer;
prefixWidth: Single; // Width needed only for the 'return ' label
maxChildWidth: Single; // To calculate overall width
yPos: Single; // Current vertical position for the row
currentXOffset: Single; // Horizontal offset for the expression node
returnLabel: TLabel; // Label for measuring 'return '
exprNode: TAuraNode;
begin
// Create the main node, frameless and without title
auraNode := CreateNode(Node);
auraNode.BeginUpdate;
try
auraNode.Frameless := True;
savedParent := FParentControl;
savedPos := FCurrentPos; // Store original position for siblings
FParentControl := auraNode;
// Start position for children inside the node
yPos := cNodePadding;
maxChildWidth := 0; // Max width including prefix + expression
// --- Determine width needed for 'return ' prefix ---
returnLabel := AddLabel(auraNode, 'return '); // Create temporarily to measure
prefixWidth := returnLabel.Width + returnLabel.Margins.Left + returnLabel.Margins.Right + cNodePadding; // Width + gap
returnLabel.Free; // Free temporary label
// --- End prefix width calculation ---
// --- Visit children ---
for i := 0 to High(Node.Expressions) do
begin
expr := Node.Expressions[i];
prefixLabel := nil; // Initialize prefixLabel for this iteration
// 1. Create Prefix Label ONLY for the last statement
if i = High(Node.Expressions) then
begin
prefixText := 'return ';
prefixLabel := AddLabel(auraNode, prefixText);
prefixLabel.Position.Point := TPointF.Create(cNodePadding, yPos); // Place prefix label
currentXOffset := cNodePadding + prefixWidth; // Start expression node after the 'return ' prefix space
end
else
begin
prefixText := ''; // No prefix for other statements
currentXOffset := cNodePadding; // Start expression node directly at the padding edge
end;
// 2. Visit Expression Node
// Set position for the expression node
FCurrentPos := TPointF.Create(currentXOffset, yPos);
exprNode := CallAccept(expr); // This creates the node/label for the expression
// Get the created control and update max width/height
if Assigned(exprNode) then
begin
var currentExprHeight := exprNode.Height + exprNode.Margins.Top + exprNode.Margins.Bottom;
var currentPrefixHeight: Single := 0;
if Assigned(prefixLabel) then
currentPrefixHeight := prefixLabel.Height + prefixLabel.Margins.Top + prefixLabel.Margins.Bottom;
var rowHeight := Max(currentExprHeight, currentPrefixHeight);
// --- Optional Vertical Centering within the row ---
if Assigned(prefixLabel) then
prefixLabel.Position.Y := yPos + (rowHeight / 2) - currentPrefixHeight / 2 + prefixLabel.Margins.Top;
exprNode.Position.Y :=
yPos + (rowHeight / 2) - currentExprHeight / 2 + exprNode.Margins.Top; // Position expr node correctly
// --- End Vertical Centering ---
// Update max width needed for the block node
maxChildWidth := Max(maxChildWidth, exprNode.Position.X + exprNode.Width + exprNode.Margins.Right);
// Update Y position for the next row
yPos := yPos + rowHeight + FNodeVerticalGap; // Move down for the next statement
end
else // Only prefix label exists (shouldn't happen if expression is valid)
begin
if Assigned(prefixLabel) then
begin
yPos := yPos + prefixLabel.Height + prefixLabel.Margins.Top + prefixLabel.Margins.Bottom + FNodeVerticalGap;
maxChildWidth := Max(maxChildWidth, prefixLabel.Position.X + prefixLabel.Width + prefixLabel.Margins.Right);
end
else
begin
yPos := yPos + cMinNodeHeight + FNodeVerticalGap; // Add minimal height if no control added
maxChildWidth := Max(maxChildWidth, cMinNodeWidth);
end;
end;
end;
// --- Children Visited ---
FParentControl := savedParent; // Restore parent for siblings
FCurrentPos := savedPos; // Restore position for siblings
// --- Adjust parent size ---
auraNode.Width := Max(cMinNodeWidth, maxChildWidth + cNodePadding); // Total width + right padding
if auraNode.ControlsCount > 0 then // Check if anything was added
auraNode.Height :=
Max(cMinNodeHeight, yPos - FNodeVerticalGap + cNodePadding) // Use final yPos (minus last gap) + bottom padding
else
auraNode.Height := cMinNodeHeight; // Ensure minimal height if block is empty
finally
auraNode.EndUpdate;
end;
// Move current position down for the next sibling node in the outer scope
FCurrentPos.Y := savedPos.Y + auraNode.Height + FNodeVerticalGap; // Use savedPos.Y
Result := TDataValue.FromGeneric<TAuraNode>(auraNode); // Return the created node
end;
function TAstVisualizer.VisitVariableDeclaration(const Node: IVariableDeclarationNode): TDataValue;
var
auraNode: TAuraNode;
varLabel: TLabel;
nameLabel: TLabel;
opLabel: TLabel;
initNode: TAuraNode; // Use TAuraNode
savedParent: TControl;
savedPos: TPointF;
currentX: Single; // Track horizontal position
maxHeightInRow: Single; // Track max height for row alignment
yPos: Single; // Y position for elements in this row
begin
// Create the main node, frameless
auraNode := CreateNode(Node);
auraNode.BeginUpdate;
try
auraNode.Frameless := true;
// Start position for children inside the node
currentX := cNodePadding;
yPos := cNodePadding; // Initial Y position
maxHeightInRow := 0;
initNode := nil;
// 1. "var" Label
varLabel := AddLabel(auraNode, 'var');
varLabel.Position.Point := TPointF.Create(currentX, yPos);
currentX := varLabel.Position.X + varLabel.Width + varLabel.Margins.Right + cNodePadding;
maxHeightInRow := Max(maxHeightInRow, varLabel.Height + varLabel.Margins.Top + varLabel.Margins.Bottom);
// 2. Identifier Label
nameLabel := AddLabel(auraNode, Node.Identifier.Name);
nameLabel.Position.Point := TPointF.Create(currentX, yPos);
currentX := nameLabel.Position.X + nameLabel.Width + nameLabel.Margins.Right + cNodePadding;
maxHeightInRow := Max(maxHeightInRow, nameLabel.Height + nameLabel.Margins.Top + nameLabel.Margins.Bottom);
opLabel := nil;
if Assigned(Node.Initializer) then
begin
// 3. Operator Label (only if initializer exists)
opLabel := AddLabel(auraNode, ':=');
opLabel.Position.Point := TPointF.Create(currentX, yPos);
currentX := opLabel.Position.X + opLabel.Width + opLabel.Margins.Right + cNodePadding;
maxHeightInRow := Max(maxHeightInRow, opLabel.Height + opLabel.Margins.Top + opLabel.Margins.Bottom);
// 4. Initializer Expression Node (Visited Recursively)
savedParent := FParentControl;
savedPos := FCurrentPos; // Store original position for siblings
FParentControl := auraNode; // Children are added to this node
FCurrentPos := TPointF.Create(currentX, yPos); // Set position for the child node
initNode := CallAccept(Node.Initializer); // Create the node/label
if Assigned(initNode) then
begin
currentX := initNode.Position.X + initNode.Width + initNode.Margins.Right;
maxHeightInRow := Max(maxHeightInRow, initNode.Height + initNode.Margins.Top + initNode.Margins.Bottom);
end;
FParentControl := savedParent; // Restore parent for siblings
FCurrentPos := savedPos; // Restore position for siblings
end;
// --- Adjust parent size ---
auraNode.Width := Max(cMinNodeWidth, currentX + cNodePadding); // Total width needed + right padding
auraNode.Height := Max(cMinNodeHeight, maxHeightInRow + 2 * cNodePadding); // Max height + top/bottom padding
// --- Optional Vertical Centering ---
var centerOffsetY: Single := (auraNode.Height / 2);
if Assigned(varLabel) then
varLabel.Position.Y :=
centerOffsetY - (varLabel.Height + varLabel.Margins.Top + varLabel.Margins.Bottom) / 2 + varLabel.Margins.Top;
if Assigned(nameLabel) then
nameLabel.Position.Y :=
centerOffsetY - (nameLabel.Height + nameLabel.Margins.Top + nameLabel.Margins.Bottom) / 2 + nameLabel.Margins.Top;
if Assigned(opLabel) then
opLabel.Position.Y := centerOffsetY - (opLabel.Height + opLabel.Margins.Top + opLabel.Margins.Bottom) / 2 + opLabel.Margins.Top;
if Assigned(initNode) then
initNode.Position.Y :=
centerOffsetY - (initNode.Height + initNode.Margins.Top + initNode.Margins.Bottom) / 2 + initNode.Margins.Top;
// --- End Optional Vertical Centering ---
finally
auraNode.EndUpdate;
end;
// Move current position down for the next sibling node in the outer scope
FCurrentPos.Y := savedPos.Y + auraNode.Height + FNodeVerticalGap; // Use savedPos.Y
Result := TDataValue.FromGeneric<TAuraNode>(auraNode); // Return the created node
end;
function TAstVisualizer.VisitAssignment(const Node: IAssignmentNode): TDataValue;
var
auraNode: TAuraNode;
identifierLabel: TLabel;
operatorLabel: TLabel;
valueNode: TAuraNode; // Use TAuraNode
savedParent: TControl;
savedPos: TPointF;
currentX: Single; // Track horizontal position
maxHeightInRow: Single; // Track max height for row alignment
labelYPos: Single; // Y position for labels in this row
valueYPos: Single; // Y position for the value node in this row
begin
// Create the main node, frameless as it primarily serves as a container
auraNode := CreateNode(Node);
auraNode.BeginUpdate;
try
auraNode.Frameless := true;
// Start position for children inside the node
currentX := cNodePadding;
labelYPos := cNodePadding; // Initial Y position for labels
maxHeightInRow := 0;
// 1. Identifier Label
identifierLabel := AddLabel(auraNode, Node.Identifier.Name);
identifierLabel.Position.Point := TPointF.Create(currentX, labelYPos);
currentX :=
identifierLabel.Position.X
+ identifierLabel.Width
+ identifierLabel.Margins.Right
+ cNodePadding; // Update X pos after adding Margins.Right and gap
maxHeightInRow := Max(maxHeightInRow, identifierLabel.Height + identifierLabel.Margins.Top + identifierLabel.Margins.Bottom);
// 2. Operator Label
operatorLabel := AddLabel(auraNode, ':=');
operatorLabel.Position.Point := TPointF.Create(currentX, labelYPos);
currentX := operatorLabel.Position.X + operatorLabel.Width + operatorLabel.Margins.Right + cNodePadding; // Update X pos
maxHeightInRow := Max(maxHeightInRow, operatorLabel.Height + operatorLabel.Margins.Top + operatorLabel.Margins.Bottom);
// 3. Expression Node (Visited Recursively)
savedParent := FParentControl;
savedPos := FCurrentPos; // Store original position for siblings
FParentControl := auraNode; // Children are added to this node
valueYPos := cNodePadding; // Align top of value node with top of labels
FCurrentPos := TPointF.Create(currentX, valueYPos); // Set position for the child node
valueNode := CallAccept(Node.Value); // This creates the node/label for the expression
// Get the created control (it's the last one added to auraNode)
if Assigned(valueNode) then
begin
// Update currentX based on the right edge of the value control
currentX := valueNode.Position.X + valueNode.Width + valueNode.Margins.Right; // Added margin
// Update maxHeightInRow considering the value control's height and potential margins
maxHeightInRow := Max(maxHeightInRow, valueNode.Height + valueNode.Margins.Top + valueNode.Margins.Bottom);
end;
FParentControl := savedParent; // Restore parent for siblings
FCurrentPos := savedPos; // Restore position for siblings
// --- Adjust parent size ---
auraNode.Width := Max(cMinNodeWidth, currentX + cNodePadding); // Total width needed + right padding
auraNode.Height := Max(cMinNodeHeight, maxHeightInRow + 2 * cNodePadding); // Max height + top/bottom padding
// --- Optional Vertical Centering ---
var centerOffsetY: Single := (auraNode.Height / 2);
if Assigned(identifierLabel) then
identifierLabel.Position.Y :=
centerOffsetY
- (identifierLabel.Height + identifierLabel.Margins.Top + identifierLabel.Margins.Bottom) / 2
+ identifierLabel.Margins.Top;
if Assigned(operatorLabel) then
operatorLabel.Position.Y :=
centerOffsetY
- (operatorLabel.Height + operatorLabel.Margins.Top + operatorLabel.Margins.Bottom) / 2
+ operatorLabel.Margins.Top;
if Assigned(valueNode) then
valueNode.Position.Y :=
centerOffsetY - (valueNode.Height + valueNode.Margins.Top + valueNode.Margins.Bottom) / 2 + valueNode.Margins.Top;
// --- End Optional Vertical Centering ---
finally
auraNode.EndUpdate;
end;
// Move current position down for the next sibling node in the outer scope
FCurrentPos.Y := savedPos.Y + auraNode.Height + FNodeVerticalGap; // Use savedPos.Y
Result := TDataValue.FromGeneric<TAuraNode>(auraNode); // Return the created node
end;
function TAstVisualizer.VisitMacroDefinition(const Node: IMacroDefinitionNode): TDataValue;
var
i: Integer;
paramStr: string;
auraNode: TAuraNode;
begin
paramStr := '';
for i := 0 to High(Node.Parameters) do
begin
if i > 0 then
paramStr := paramStr + ', ';
paramStr := paramStr + Node.Parameters[i].Name;
end;
auraNode := ProcessStub('Macro Def: ' + Node.Name.Name + ' (' + paramStr + ')', Node, [Node.Body]);
Result := TDataValue.FromGeneric<TAuraNode>(auraNode);
end;
function TAstVisualizer.VisitQuasiquote(const Node: IQuasiquoteNode): TDataValue;
var
auraNode: TAuraNode;
begin
auraNode := ProcessStub('Quasiquote', Node, [Node.Expression]);
Result := TDataValue.FromGeneric<TAuraNode>(auraNode);
end;
function TAstVisualizer.VisitUnquote(const Node: IUnquoteNode): TDataValue;
var
auraNode: TAuraNode;
begin
auraNode := ProcessStub('Unquote', Node, [Node.Expression]);
Result := TDataValue.FromGeneric<TAuraNode>(auraNode);
end;
function TAstVisualizer.VisitUnquoteSplicing(const Node: IUnquoteSplicingNode): TDataValue;
var
auraNode: TAuraNode;
begin
auraNode := ProcessStub('Unquote-Splicing', Node, [Node.Expression]);
Result := TDataValue.FromGeneric<TAuraNode>(auraNode);
end;
function TAstVisualizer.VisitIndexer(const Node: IIndexerNode): TDataValue;
var
auraNode: TAuraNode;
begin
auraNode := ProcessStub('Indexer []', Node, [Node.Base, Node.Index]);
Result := TDataValue.FromGeneric<TAuraNode>(auraNode);
end;
function TAstVisualizer.VisitMemberAccess(const Node: IMemberAccessNode): TDataValue;
var
auraNode: TAuraNode;
begin
auraNode := ProcessStub('Member Access: .' + Node.Member.Name, Node, [Node.Base]);
Result := TDataValue.FromGeneric<TAuraNode>(auraNode);
end;
function TAstVisualizer.VisitCreateSeries(const Node: ICreateSeriesNode): TDataValue;
var
auraNode: TAuraNode;
labelHeight: Single;
begin
// Leaf node
auraNode := CreateNode(Node);
auraNode.BeginUpdate;
try
labelHeight := AddTitleLabel(auraNode, 'Create Series: ' + Node.Definition);
// Adjust size based on label size
if auraNode.ControlsCount > 0 then
begin
var Lbl := auraNode.Controls[0] as TLabel;
auraNode.Width := Max(cMinNodeWidth, Lbl.Width + Lbl.Margins.Left + Lbl.Margins.Right);
auraNode.Height := Max(cMinNodeHeight, labelHeight + cNodePadding);
end
else
AdjustParentNodeSize(auraNode, []);
finally
auraNode.EndUpdate;
end;
FCurrentPos.Y := FCurrentPos.Y + auraNode.Height + FNodeVerticalGap;
Result := TDataValue.FromGeneric<TAuraNode>(auraNode); // Return the created node
end;
function TAstVisualizer.VisitAddSeriesItem(const Node: IAddSeriesItemNode): TDataValue;
var
children: TArray<IAstNode>;
auraNode: TAuraNode;
begin
children := [Node.Value];
if Assigned(Node.Lookback) then
children := children + [Node.Lookback];
auraNode := ProcessStub('Add Item to: ' + Node.Series.Name, Node, children);
Result := TDataValue.FromGeneric<TAuraNode>(auraNode);
end;
function TAstVisualizer.VisitSeriesLength(const Node: ISeriesLengthNode): TDataValue;
var
auraNode: TAuraNode;
labelHeight: Single;
begin
// Leaf node
auraNode := CreateNode(Node);
auraNode.BeginUpdate;
try
labelHeight := AddTitleLabel(auraNode, 'Length of: ' + Node.Series.Name);
// Adjust size based on label size
if auraNode.ControlsCount > 0 then
begin
var Lbl := auraNode.Controls[0] as TLabel;
auraNode.Width := Max(cMinNodeWidth, Lbl.Width + Lbl.Margins.Left + Lbl.Margins.Right);
auraNode.Height := Max(cMinNodeHeight, labelHeight + cNodePadding);
end
else
AdjustParentNodeSize(auraNode, []);
finally
auraNode.EndUpdate;
end;
FCurrentPos.Y := FCurrentPos.Y + auraNode.Height + FNodeVerticalGap;
Result := TDataValue.FromGeneric<TAuraNode>(auraNode); // Return the created node
end;
function TAstVisualizer.VisitRecurNode(const Node: IRecurNode): TDataValue;
var
auraNode: TAuraNode;
savedParent: TControl;
savedPos: TPointF;
i: Integer;
recurLabel: TLabel;
argNode: TAuraNode; // Use TAuraNode
parenLabel, commaLabel: TLabel;
// Layout variables
yPos: Single; // Current vertical position for the row
currentX: Single; // Horizontal position tracker
maxHeightInRow: Single; // Max height for row alignment
argStartIndex: Integer; // Index of first argument-related control
begin
// Create the main node, frameless
auraNode := CreateNode(Node);
auraNode.BeginUpdate;
inc(FExprDepth);
try
auraNode.Frameless := True;
// Start position for children inside the node
currentX := cNodePadding;
yPos := cNodePadding; // Initial Y position
maxHeightInRow := 0;
// 1. "recur" Label
recurLabel := AddLabel(auraNode, WideChar($03BB));
recurLabel.Font.Style := recurLabel.Font.Style + [TFontStyle.fsBold]; // Make keyword bold
recurLabel.Position.Point := TPointF.Create(currentX, yPos);
currentX := recurLabel.Position.X + recurLabel.Width + recurLabel.Margins.Right + cNodePadding / 2; // Update X pos
maxHeightInRow := Max(maxHeightInRow, recurLabel.Height + recurLabel.Margins.Top + recurLabel.Margins.Bottom);
// --- Add Arguments (Horizontal Layout) ---
// Add opening parenthesis
parenLabel := AddLabel(auraNode, '(');
parenLabel.Font.Style := parenLabel.Font.Style - [TFontStyle.fsBold]; // Normal style
parenLabel.Position.Point := TPointF.Create(currentX, yPos);
currentX := parenLabel.Position.X + parenLabel.Width + parenLabel.Margins.Right + cNodePadding / 2;
maxHeightInRow := Max(maxHeightInRow, parenLabel.Height + parenLabel.Margins.Top + parenLabel.Margins.Bottom);
savedParent := FParentControl;
savedPos := FCurrentPos;
FParentControl := auraNode; // Arguments are children of auraNode
for i := 0 to High(Node.Arguments) do
begin
// Set position for the argument node
FCurrentPos := TPointF.Create(currentX, yPos);
argNode := CallAccept(Node.Arguments[i]); // Create argument node
if Assigned(argNode) then
begin
// Position it correctly (redundant, but safe)
argNode.Position.Point := TPointF.Create(currentX, yPos);
currentX :=
argNode.Position.X
+ argNode.Width
+ argNode.Margins.Right
+ IfThen(i < High(Node.Arguments), cNodePadding / 2, 0); // Gap or no gap after arg
maxHeightInRow := Max(maxHeightInRow, argNode.Height + argNode.Margins.Top + argNode.Margins.Bottom);
// Add comma if not the last argument
if i < High(Node.Arguments) then
begin
commaLabel := AddLabel(auraNode, ',');
commaLabel.Font.Style := commaLabel.Font.Style - [TFontStyle.fsBold]; // Normal style
commaLabel.Position.Point := TPointF.Create(currentX, yPos);
currentX := commaLabel.Position.X + commaLabel.Width + commaLabel.Margins.Right + cNodePadding / 2;
maxHeightInRow := Max(maxHeightInRow, commaLabel.Height + commaLabel.Margins.Top + commaLabel.Margins.Bottom);
end;
end
else // Should not happen
begin
currentX := currentX + cMinNodeWidth + cNodePadding;
maxHeightInRow := Max(maxHeightInRow, cMinNodeHeight);
end;
end;
// Add closing parenthesis
parenLabel := AddLabel(auraNode, ')');
parenLabel.Font.Style := parenLabel.Font.Style - [TFontStyle.fsBold]; // Normal style
parenLabel.Position.Point := TPointF.Create(currentX, yPos);
currentX := parenLabel.Position.X + parenLabel.Width + parenLabel.Margins.Right; // Final X position
maxHeightInRow := Max(maxHeightInRow, parenLabel.Height + parenLabel.Margins.Top + parenLabel.Margins.Bottom);
// --- Vertical Centering for the single row ---
var centerOffsetYRow: Single := yPos + maxHeightInRow / 2;
argStartIndex := 0; // Start check from first control ("recur")
for i := argStartIndex to auraNode.ControlsCount - 1 do // Iterate ALL controls in the row
begin
var ctrl := auraNode.Controls[i];
// Adjust Y based on calculated center and control's own height/margins
ctrl.Position.Y := centerOffsetYRow - (ctrl.Height + ctrl.Margins.Top + ctrl.Margins.Bottom) / 2 + ctrl.Margins.Top;
end;
// --- End Centering ---
yPos := yPos + maxHeightInRow; // Update yPos based on row height for final size calculation
FParentControl := savedParent; // Restore parent for siblings
FCurrentPos := savedPos; // Restore position for siblings
// --- Adjust Size ---
auraNode.Width := Max(cMinNodeWidth, currentX + cNodePadding); // Width determined by final currentX
auraNode.Height := Max(cMinNodeHeight, yPos + cNodePadding); // Height determined by final yPos
finally
dec(FExprDepth);
auraNode.EndUpdate;
end;
// Move current position down for the next sibling node
FCurrentPos.Y := savedPos.Y + auraNode.Height + FNodeVerticalGap; // Use savedPos.Y
Result := TDataValue.FromGeneric<TAuraNode>(auraNode); // Return the created node
end;
end.