628 lines
17 KiB
ObjectPascal
628 lines
17 KiB
ObjectPascal
unit Myc.Fmx.AstEditor.Render;
|
|
|
|
interface
|
|
|
|
uses
|
|
System.Types,
|
|
System.UITypes,
|
|
System.Classes,
|
|
System.SysUtils,
|
|
System.Math,
|
|
System.Generics.Collections,
|
|
FMX.Types,
|
|
FMX.Graphics,
|
|
FMX.TextLayout;
|
|
|
|
type
|
|
TVisualNode = class;
|
|
|
|
// Interface for the host (e.g. TWorkspace) to request repaints/layouts
|
|
IVisualHost = interface
|
|
['{A9E2BD60-281B-42A2-9DB5-3100E8EF1519}']
|
|
procedure InvalidateRect(const R: TRectF);
|
|
procedure RequestLayout;
|
|
function GetCanvas: TCanvas;
|
|
end;
|
|
|
|
// Helper structure for margins and padding
|
|
TMarginRect = record
|
|
Left, Top, Right, Bottom: Single;
|
|
function Width: Single; inline;
|
|
function Height: Single; inline;
|
|
class function Create(L, T, R, B: Single): TMarginRect; static; inline;
|
|
class operator Initialize(out Dest: TMarginRect);
|
|
end;
|
|
|
|
// The base class for all virtual UI elements (Flyweight)
|
|
TVisualNode = class
|
|
private
|
|
FHost: IVisualHost;
|
|
FParent: TVisualNode;
|
|
FChildren: TList<TVisualNode>;
|
|
|
|
// Data stored as Position and Size (Layout Model)
|
|
// Direct field access is preferred within the unit
|
|
FPosition: TPointF;
|
|
FSize: TSizeF;
|
|
|
|
FPadding: TMarginRect;
|
|
FMargins: TMarginRect;
|
|
FVisible: Boolean;
|
|
FHitTest: Boolean;
|
|
FTag: NativeInt;
|
|
FOpacity: Single;
|
|
|
|
procedure SetParent(const Value: TVisualNode);
|
|
|
|
protected
|
|
procedure SetHost(const Value: IVisualHost); virtual;
|
|
// Called to paint the node-specific content (background, etc.)
|
|
procedure DoPaint(Canvas: TCanvas; const Offset: TPointF); virtual;
|
|
public
|
|
constructor Create(AHost: IVisualHost = nil); virtual;
|
|
destructor Destroy; override;
|
|
|
|
// Layout system
|
|
procedure RequestLayout;
|
|
procedure RecalcLayout; virtual;
|
|
|
|
// Geometry
|
|
function GetAbsolutePosition: TPointF;
|
|
function LocalToAbsolute(const P: TPointF): TPointF;
|
|
function AbsoluteToLocal(const P: TPointF): TPointF;
|
|
function AbsoluteRect: TRectF;
|
|
|
|
function HitTest(const P: TPointF): TVisualNode; virtual;
|
|
|
|
// Rendering pipeline
|
|
// Offset is the absolute position of the parent content area
|
|
procedure Paint(Canvas: TCanvas; const Offset: TPointF);
|
|
|
|
// Child Management
|
|
procedure AddChild(Child: TVisualNode);
|
|
procedure RemoveChild(Child: TVisualNode);
|
|
procedure DeleteChildren;
|
|
function GetChildCount: Integer; inline;
|
|
function GetChild(Index: Integer): TVisualNode; inline;
|
|
|
|
// Interaction stubs (called by the host)
|
|
procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Single); virtual;
|
|
procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Single); virtual;
|
|
procedure MouseMove(Shift: TShiftState; X, Y: Single); virtual;
|
|
procedure MouseEnter; virtual;
|
|
procedure MouseLeave; virtual;
|
|
|
|
property Host: IVisualHost read FHost;
|
|
property Parent: TVisualNode read FParent;
|
|
|
|
// Direct field access via properties
|
|
property Position: TPointF read FPosition write FPosition;
|
|
property Size: TSizeF read FSize write FSize;
|
|
|
|
property Padding: TMarginRect read FPadding write FPadding;
|
|
property Margins: TMarginRect read FMargins write FMargins;
|
|
|
|
property Visible: Boolean read FVisible write FVisible;
|
|
property HitTestEnabled: Boolean read FHitTest write FHitTest;
|
|
property Opacity: Single read FOpacity write FOpacity;
|
|
property Tag: NativeInt read FTag write FTag;
|
|
end;
|
|
|
|
// Lightweight replacement for TLabel
|
|
TTextNode = class(TVisualNode)
|
|
private
|
|
FText: string;
|
|
FLayout: TTextLayout;
|
|
FFontSettings: TTextSettings; // Stores font properties
|
|
FColor: TAlphaColor;
|
|
FIsLayoutDirty: Boolean;
|
|
procedure SetText(const Value: string);
|
|
procedure SetColor(const Value: TAlphaColor);
|
|
protected
|
|
procedure DoPaint(Canvas: TCanvas; const Offset: TPointF); override;
|
|
procedure UpdateTextLayout(Canvas: TCanvas);
|
|
public
|
|
constructor Create(AHost: IVisualHost = nil); override;
|
|
destructor Destroy; override;
|
|
procedure RecalcLayout; override;
|
|
|
|
property Text: string read FText write SetText;
|
|
property Color: TAlphaColor read FColor write SetColor;
|
|
property FontSettings: TTextSettings read FFontSettings;
|
|
end;
|
|
|
|
TLayoutOrientation = (loVertical, loHorizontal);
|
|
TLayoutAlignment = (laCenter, laFlush);
|
|
|
|
// The Layout Manager
|
|
TAutoFitLayout = class(TVisualNode)
|
|
private
|
|
FOrientation: TLayoutOrientation;
|
|
FAlignment: TLayoutAlignment;
|
|
procedure SetOrientation(const Value: TLayoutOrientation);
|
|
procedure SetAlignment(const Value: TLayoutAlignment);
|
|
protected
|
|
// TAutoFitLayout is a container, draws no background by default, but propagates Paint to children
|
|
procedure DoPaint(Canvas: TCanvas; const Offset: TPointF); override;
|
|
public
|
|
constructor Create(AHost: IVisualHost = nil); override;
|
|
procedure RecalcLayout; override;
|
|
|
|
// Calculates the insertion index for drag & drop based on coordinates
|
|
function GetInsertionIndex(const LocalP: TPointF): Integer;
|
|
|
|
property Orientation: TLayoutOrientation read FOrientation write SetOrientation;
|
|
property Alignment: TLayoutAlignment read FAlignment write SetAlignment;
|
|
end;
|
|
|
|
implementation
|
|
|
|
{ TMarginRect }
|
|
|
|
class function TMarginRect.Create(L, T, R, B: Single): TMarginRect;
|
|
begin
|
|
Result.Left := L;
|
|
Result.Top := T;
|
|
Result.Right := R;
|
|
Result.Bottom := B;
|
|
end;
|
|
|
|
class operator TMarginRect.Initialize(out Dest: TMarginRect);
|
|
begin
|
|
Dest.Left := 0;
|
|
Dest.Top := 0;
|
|
Dest.Right := 0;
|
|
Dest.Bottom := 0;
|
|
end;
|
|
|
|
function TMarginRect.Height: Single;
|
|
begin
|
|
Result := Top + Bottom;
|
|
end;
|
|
|
|
function TMarginRect.Width: Single;
|
|
begin
|
|
Result := Left + Right;
|
|
end;
|
|
|
|
{ TVisualNode }
|
|
|
|
constructor TVisualNode.Create(AHost: IVisualHost);
|
|
begin
|
|
inherited Create;
|
|
FHost := AHost;
|
|
FChildren := TList<TVisualNode>.Create;
|
|
FVisible := True;
|
|
FHitTest := True;
|
|
FOpacity := 1.0;
|
|
FPosition := TPointF.Create(0, 0);
|
|
FSize := TSizeF.Create(10, 10);
|
|
end;
|
|
|
|
destructor TVisualNode.Destroy;
|
|
begin
|
|
// Recursive release
|
|
for var i := 0 to FChildren.Count - 1 do
|
|
FChildren[i].Free;
|
|
FChildren.Free;
|
|
inherited;
|
|
end;
|
|
|
|
procedure TVisualNode.SetParent(const Value: TVisualNode);
|
|
begin
|
|
if FParent <> Value then
|
|
begin
|
|
FParent := Value;
|
|
// Propagate host if parent has one
|
|
if (FHost = nil) and Assigned(FParent) then
|
|
SetHost(FParent.Host);
|
|
end;
|
|
end;
|
|
|
|
procedure TVisualNode.SetHost(const Value: IVisualHost);
|
|
begin
|
|
FHost := Value;
|
|
for var Child in FChildren do
|
|
Child.SetHost(Value);
|
|
end;
|
|
|
|
procedure TVisualNode.AddChild(Child: TVisualNode);
|
|
begin
|
|
Assert(Child <> nil);
|
|
if Child.Parent <> nil then
|
|
Child.Parent.RemoveChild(Child);
|
|
|
|
FChildren.Add(Child);
|
|
Child.SetParent(Self);
|
|
RequestLayout;
|
|
end;
|
|
|
|
procedure TVisualNode.RemoveChild(Child: TVisualNode);
|
|
begin
|
|
FChildren.Remove(Child);
|
|
Child.FParent := nil;
|
|
RequestLayout;
|
|
end;
|
|
|
|
procedure TVisualNode.DeleteChildren;
|
|
begin
|
|
for var i := 0 to FChildren.Count - 1 do
|
|
FChildren[i].Free;
|
|
FChildren.Clear;
|
|
RequestLayout;
|
|
end;
|
|
|
|
function TVisualNode.GetChild(Index: Integer): TVisualNode;
|
|
begin
|
|
Result := FChildren[Index];
|
|
end;
|
|
|
|
function TVisualNode.GetChildCount: Integer;
|
|
begin
|
|
Result := FChildren.Count;
|
|
end;
|
|
|
|
procedure TVisualNode.RequestLayout;
|
|
begin
|
|
if Assigned(FParent) then
|
|
FParent.RequestLayout
|
|
else if Assigned(FHost) then
|
|
FHost.RequestLayout;
|
|
end;
|
|
|
|
procedure TVisualNode.RecalcLayout;
|
|
begin
|
|
// Base implementation: Does nothing.
|
|
// Subclasses (Layouts) must implement calculation here.
|
|
end;
|
|
|
|
function TVisualNode.GetAbsolutePosition: TPointF;
|
|
var
|
|
Curr: TVisualNode;
|
|
begin
|
|
Result := FPosition;
|
|
Curr := FParent;
|
|
while Assigned(Curr) do
|
|
begin
|
|
// Add parent position + padding (since we are in the content area)
|
|
Result := Result + Curr.FPosition + TPointF.Create(Curr.Padding.Left, Curr.Padding.Top);
|
|
Curr := Curr.Parent;
|
|
end;
|
|
end;
|
|
|
|
function TVisualNode.LocalToAbsolute(const P: TPointF): TPointF;
|
|
begin
|
|
Result := GetAbsolutePosition + P;
|
|
end;
|
|
|
|
function TVisualNode.AbsoluteToLocal(const P: TPointF): TPointF;
|
|
begin
|
|
Result := P - GetAbsolutePosition;
|
|
end;
|
|
|
|
function TVisualNode.AbsoluteRect: TRectF;
|
|
begin
|
|
Result := TRectF.Create(GetAbsolutePosition, FSize.Width, FSize.Height);
|
|
end;
|
|
|
|
procedure TVisualNode.Paint(Canvas: TCanvas; const Offset: TPointF);
|
|
var
|
|
AbsPos: TPointF;
|
|
ChildOffset: TPointF;
|
|
Child: TVisualNode;
|
|
begin
|
|
if not FVisible then
|
|
exit;
|
|
|
|
// Absolute position of this node
|
|
AbsPos := Offset + FPosition;
|
|
|
|
// 1. Background / Own content
|
|
DoPaint(Canvas, AbsPos);
|
|
|
|
// 2. Draw children
|
|
if FChildren.Count > 0 then
|
|
begin
|
|
// The 0,0 point for children is shifted by padding
|
|
ChildOffset := AbsPos + TPointF.Create(FPadding.Left, FPadding.Top);
|
|
for Child in FChildren do
|
|
Child.Paint(Canvas, ChildOffset);
|
|
end;
|
|
end;
|
|
|
|
procedure TVisualNode.DoPaint(Canvas: TCanvas; const Offset: TPointF);
|
|
begin
|
|
// Stub
|
|
end;
|
|
|
|
function TVisualNode.HitTest(const P: TPointF): TVisualNode;
|
|
var
|
|
i: Integer;
|
|
Child: TVisualNode;
|
|
LocalP: TPointF;
|
|
ContentOrigin: TPointF;
|
|
begin
|
|
Result := nil;
|
|
if not FVisible or not FHitTest then
|
|
exit;
|
|
|
|
// P is relative to Position of this node. Check if P is inside rect.
|
|
if not TRectF.Create(0, 0, FSize.Width, FSize.Height).Contains(P) then
|
|
exit;
|
|
|
|
ContentOrigin := TPointF.Create(FPadding.Left, FPadding.Top);
|
|
|
|
// Check children in reverse Z-order (topmost first)
|
|
for i := FChildren.Count - 1 downto 0 do
|
|
begin
|
|
Child := FChildren[i];
|
|
// Transformation into child coordinates
|
|
LocalP := P - ContentOrigin - Child.FPosition;
|
|
|
|
Result := Child.HitTest(LocalP);
|
|
if Result <> nil then
|
|
exit;
|
|
end;
|
|
|
|
// If no child hit, it's us
|
|
Result := Self;
|
|
end;
|
|
|
|
procedure TVisualNode.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Single);
|
|
begin
|
|
end;
|
|
procedure TVisualNode.MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Single);
|
|
begin
|
|
end;
|
|
procedure TVisualNode.MouseMove(Shift: TShiftState; X, Y: Single);
|
|
begin
|
|
end;
|
|
procedure TVisualNode.MouseEnter;
|
|
begin
|
|
end;
|
|
procedure TVisualNode.MouseLeave;
|
|
begin
|
|
end;
|
|
|
|
{ TTextNode }
|
|
|
|
constructor TTextNode.Create(AHost: IVisualHost);
|
|
begin
|
|
inherited;
|
|
FHitTest := False; // Text is usually passive
|
|
FIsLayoutDirty := True;
|
|
FFontSettings := TTextSettings.Create(nil);
|
|
FColor := TAlphaColors.Black;
|
|
end;
|
|
|
|
destructor TTextNode.Destroy;
|
|
begin
|
|
FreeAndNil(FLayout);
|
|
FreeAndNil(FFontSettings);
|
|
inherited;
|
|
end;
|
|
|
|
procedure TTextNode.SetText(const Value: string);
|
|
begin
|
|
if FText <> Value then
|
|
begin
|
|
FText := Value;
|
|
FIsLayoutDirty := True;
|
|
RequestLayout;
|
|
end;
|
|
end;
|
|
|
|
procedure TTextNode.SetColor(const Value: TAlphaColor);
|
|
begin
|
|
if FColor <> Value then
|
|
begin
|
|
FColor := Value;
|
|
if Assigned(FHost) then
|
|
FHost.InvalidateRect(AbsoluteRect);
|
|
end;
|
|
end;
|
|
|
|
procedure TTextNode.UpdateTextLayout(Canvas: TCanvas);
|
|
begin
|
|
if FIsLayoutDirty or (FLayout = nil) then
|
|
begin
|
|
if FLayout = nil then
|
|
FLayout := TTextLayoutManager.DefaultTextLayout.Create(Canvas);
|
|
|
|
FLayout.BeginUpdate;
|
|
try
|
|
FLayout.Text := FText;
|
|
FLayout.Font.Assign(FFontSettings.Font);
|
|
FLayout.Color := FColor;
|
|
FLayout.WordWrap := False;
|
|
finally
|
|
FLayout.EndUpdate;
|
|
end;
|
|
FIsLayoutDirty := False;
|
|
end;
|
|
end;
|
|
|
|
procedure TTextNode.RecalcLayout;
|
|
begin
|
|
if Assigned(FHost) then
|
|
begin
|
|
UpdateTextLayout(FHost.GetCanvas);
|
|
|
|
FSize.Width := FLayout.TextRect.Width + FPadding.Width;
|
|
FSize.Height := FLayout.TextRect.Height + FPadding.Height;
|
|
|
|
// Safety net for empty strings
|
|
if FSize.Width < 1 then
|
|
FSize.Width := 5;
|
|
if FSize.Height < 1 then
|
|
FSize.Height := 10;
|
|
end;
|
|
end;
|
|
|
|
procedure TTextNode.DoPaint(Canvas: TCanvas; const Offset: TPointF);
|
|
begin
|
|
UpdateTextLayout(Canvas);
|
|
var P := Offset + TPointF.Create(FPadding.Left, FPadding.Top);
|
|
|
|
// Set position and color on layout, then render
|
|
FLayout.TopLeft := P;
|
|
FLayout.Color := FColor;
|
|
FLayout.RenderLayout(Canvas);
|
|
end;
|
|
|
|
{ TAutoFitLayout }
|
|
|
|
constructor TAutoFitLayout.Create(AHost: IVisualHost);
|
|
begin
|
|
inherited;
|
|
FOrientation := loHorizontal;
|
|
FAlignment := laCenter;
|
|
end;
|
|
|
|
procedure TAutoFitLayout.SetOrientation(const Value: TLayoutOrientation);
|
|
begin
|
|
if FOrientation <> Value then
|
|
begin
|
|
FOrientation := Value;
|
|
RequestLayout;
|
|
end;
|
|
end;
|
|
|
|
procedure TAutoFitLayout.SetAlignment(const Value: TLayoutAlignment);
|
|
begin
|
|
if FAlignment <> Value then
|
|
begin
|
|
FAlignment := Value;
|
|
RequestLayout;
|
|
end;
|
|
end;
|
|
|
|
procedure TAutoFitLayout.DoPaint(Canvas: TCanvas; const Offset: TPointF);
|
|
begin
|
|
// TAutoFitLayout is a container, draws no background by default.
|
|
inherited DoPaint(Canvas, Offset);
|
|
end;
|
|
|
|
procedure TAutoFitLayout.RecalcLayout;
|
|
var
|
|
Child: TVisualNode;
|
|
CurrentX, CurrentY: Single;
|
|
ReqW, ReqH: Single;
|
|
begin
|
|
// 1. Recursion: Calculate children
|
|
for Child in FChildren do
|
|
begin
|
|
if Child.Visible then
|
|
Child.RecalcLayout;
|
|
end;
|
|
|
|
CurrentX := 0;
|
|
CurrentY := 0;
|
|
ReqW := 0;
|
|
ReqH := 0;
|
|
|
|
// 2. Stacking Logic
|
|
if FOrientation = loVertical then
|
|
begin
|
|
for Child in FChildren do
|
|
begin
|
|
if not Child.Visible then
|
|
continue;
|
|
|
|
// Set Y-Position
|
|
Child.FPosition := TPointF.Create(Child.Margins.Left, CurrentY + Child.Margins.Top);
|
|
|
|
var ChildTotalH := Child.Size.Height + Child.Margins.Top + Child.Margins.Bottom;
|
|
var ChildTotalW := Child.Size.Width + Child.Margins.Left + Child.Margins.Right;
|
|
|
|
CurrentY := CurrentY + ChildTotalH;
|
|
ReqW := Max(ReqW, ChildTotalW);
|
|
end;
|
|
ReqH := CurrentY;
|
|
|
|
// 3. Alignment Logic (Cross-Axis)
|
|
if FAlignment = laCenter then
|
|
begin
|
|
for Child in FChildren do
|
|
begin
|
|
if not Child.Visible then
|
|
continue;
|
|
var ChildTotalW := Child.Size.Width + Child.Margins.Left + Child.Margins.Right;
|
|
var OffsetX := (ReqW - ChildTotalW) * 0.5;
|
|
Child.FPosition := TPointF.Create(Child.Margins.Left + OffsetX, Child.FPosition.Y);
|
|
end;
|
|
end;
|
|
end
|
|
else // Horizontal
|
|
begin
|
|
for Child in FChildren do
|
|
begin
|
|
if not Child.Visible then
|
|
continue;
|
|
|
|
// Set X-Position
|
|
Child.FPosition := TPointF.Create(CurrentX + Child.Margins.Left, Child.Margins.Top);
|
|
|
|
var ChildTotalW := Child.Size.Width + Child.Margins.Left + Child.Margins.Right;
|
|
var ChildTotalH := Child.Size.Height + Child.Margins.Top + Child.Margins.Bottom;
|
|
|
|
CurrentX := CurrentX + ChildTotalW;
|
|
ReqH := Max(ReqH, ChildTotalH);
|
|
end;
|
|
ReqW := CurrentX;
|
|
|
|
// 3. Alignment Logic (Cross-Axis)
|
|
if FAlignment = laCenter then
|
|
begin
|
|
for Child in FChildren do
|
|
begin
|
|
if not Child.Visible then
|
|
continue;
|
|
var ChildTotalH := Child.Size.Height + Child.Margins.Top + Child.Margins.Bottom;
|
|
var OffsetY := (ReqH - ChildTotalH) * 0.5;
|
|
Child.FPosition := TPointF.Create(Child.FPosition.X, Child.Margins.Top + OffsetY);
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
// 4. Set own size
|
|
FSize.Width := ReqW + FPadding.Width;
|
|
FSize.Height := ReqH + FPadding.Height;
|
|
end;
|
|
|
|
function TAutoFitLayout.GetInsertionIndex(const LocalP: TPointF): Integer;
|
|
var
|
|
i: Integer;
|
|
Child: TVisualNode;
|
|
Center: Single;
|
|
ContentP: TPointF;
|
|
begin
|
|
// Assumption: Append to end as default
|
|
Result := FChildren.Count;
|
|
|
|
// Convert coords to content area
|
|
ContentP := LocalP - TPointF.Create(FPadding.Left, FPadding.Top);
|
|
|
|
for i := 0 to FChildren.Count - 1 do
|
|
begin
|
|
Child := FChildren[i];
|
|
if not Child.Visible then
|
|
continue;
|
|
|
|
if FOrientation = loVertical then
|
|
begin
|
|
Center := Child.FPosition.Y + (Child.Size.Height * 0.5);
|
|
if ContentP.Y < Center then
|
|
exit(i);
|
|
end
|
|
else
|
|
begin
|
|
Center := Child.FPosition.X + (Child.Size.Width * 0.5);
|
|
if ContentP.X < Center then
|
|
exit(i);
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
end.
|