unit Splitpan;

interface

uses
	SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls,
	Forms, Dialogs, StdCtrls, ExtCtrls;

type
	TPaneFrame = class;

	TPaneFlag = (pfCaption, pfCtl3D, pfBorder, pfCaptionBorder);
	TPaneFlags = set of TPaneFlag;
	TPaneButtonState = (pbsNormal, pbsDown, pbsUp);
	TPaneHitTest = (phtNone, phtCaption, phtButton, phtSplitter);

	TPaneRects = record
		Bounds: TRect;
		Caption: TRect;
		Button: TRect;
		Control: TRect;
		Splitter: TRect;
	end;

	TPane = class(TComponent)
	private
		FPaneFrame: TPaneFrame;
		FControl: TControl;
		FCaption: string;
		FPercent: integer;
		FBorderStyle: TBorderStyle;
		FCtl3D: boolean;
		FParentCtl3D: boolean;
		FFont: TFont;
		FParentFont: boolean;
		FCaptionVisible: boolean;
		FCloseButton: boolean;

		FOnClick: TNotifyEvent;
		FOnChangeVisible: TNotifyEvent;

		FVisible: Boolean;
		FPos,
		FSize: Integer;
		FMinSize: Integer;

		procedure SetPaneFrame(aPaneFrame: TPaneFrame);
		procedure SetControl(aControl: TControl);
		function GetPercent: Integer;
		procedure SetPercent(aPercent: Integer);

		procedure SetCaption(const aCaption: String);
		procedure SetBorderStyle(aBorderStyle: TBorderStyle);
		procedure SetCtl3D(aCtl3D: Boolean);
		procedure SetParentCtl3D(aParentCtl3D: Boolean);
		procedure SetFont(aFont: TFont);
		procedure SetParentFont(aParentFont: Boolean);
		procedure SetCaptionVisible(aCaptionVisible: Boolean);
		procedure SetCloseButton(aValue: Boolean);
		procedure ViewChanged;
		procedure OnChangeFont(aSender: TObject);
		procedure SetMinSize(aValue: Integer);
		procedure SetVisible(aValue: Boolean);
    procedure GetRects(var aRects: TPaneRects);

	protected
		procedure ReadState(aReader: TReader); override;
		procedure SetName(const aValue: TComponentName); override;
		procedure Perform(aMsg: Word);
		procedure Notification(aComponent: TComponent; anOperation: TOperation); override;

	public
		constructor Create(anOwner: TComponent); override;
		destructor Destroy; override;
		procedure Paint(aCanvas: TCanvas; const pr: TPaneRects);
		procedure Click; dynamic;
		function HasParent: Boolean; override;

		property PaneFrame: TPaneFrame read FPaneFrame write SetPaneFrame stored False;
		property Visible: Boolean read FVisible write SetVisible;

	published
		property MinSize: Integer read FMinSize write SetMinSize;
		property Percent: Integer read GetPercent write SetPercent;
		property Control: TControl read FControl write SetControl;
		property Caption: String read FCaption write SetCaption;
		property BorderStyle: TBorderStyle read FBorderStyle write SetBorderStyle default bsSingle;
		property Ctl3D: Boolean read FCtl3D write SetCtl3D default True;
		property ParentCtl3D: Boolean read FParentCtl3D write SetParentCtl3D default True;
		property Font: TFont read FFont write SetFont;
		property ParentFont: Boolean read FParentFont write SetParentFont default True;
		property CaptionVisible: Boolean read FCaptionVisible write SetCaptionVisible default True;
		property CloseButton: Boolean read FCloseButton write SetCloseButton default False;
		property OnClick: TNotifyEvent read FOnClick write FOnClick;
		property OnChangeVisible: TNotifyEvent read FOnChangeVisible write FOnChangeVisible;
	end;

	EPaneFrame = class(Exception);
	TSplitType = (stHSplit, stVSplit);
	TPaneFrameState = (pfsNormal, pfsButtonDown, pfsDragSplitter);

	TPaneFrame = class(TCustomControl)
	private
		{ Private declarations }
		FSelected: TPane;
		FPanes: TList;
		FSplitType: TSplitType;
		FFullResize: Boolean;
		FFlat: Boolean;
		FOnResize: TNotifyEvent;

		FButtonTimer: TTimer;
		FButtonPane: TPane;
		FButtonState: TPaneButtonState;

		FState: TPaneFrameState;

		FDragSplitter: record
			Pane: TPane;
			Anchor: TPoint;
			LastPos: TPoint;
			SaveSize: Integer;
		end;

		procedure OnButtonTimer(aSender: TObject);
		procedure UpdatePaneSizes;
		function IsPaneVisible(anIndex: Integer): Boolean;
		procedure RepaintDrag;

		procedure SetButtonState(aPane: TPane; aState: TPaneButtonState);

		function GetPaneCount: Integer;
		function GetPane(anIndex: integer): TPane;
		procedure SetSelected(aSelected: TPane);
		procedure SetSplitType(aSplitType: TSplitType);

		procedure SetPercentsAsString(const aSrc: string);
		function GetPercentsAsString: string;

		function GetSplitterSize: integer;

	protected
		{ Protected declarations }
		procedure CreateParams(var Params: TCreateParams); override;

		procedure SetName(const aValue: TComponentName); override;
		procedure GetChildren(aProc: TGetChildProc; aRoot: TComponent); override;

		procedure Loaded; override;

		procedure Paint; override;

		procedure ApplyDrag(aMouse: TPoint);
		function GetSplitterRect(aPane: TPane; aSize: Integer = 0): TRect;

		procedure MouseMove(aShift: TShiftState; aX, aY: Integer); override;
		procedure MouseDown(aButton: TMouseButton; aShift: TShiftState; aX, aY: Integer); override;
		procedure MouseUp(aButton: TMouseButton; aShift: TShiftState; aX, aY: Integer); override;

		procedure CMDesignHitTest(var aMsg: TCMDesignHitTest); message CM_DESIGNHITTEST;
		procedure WMSize(var aMsg: TWMSize); message WM_SIZE;
		procedure CMCtl3DChanged(var aMsg: TMessage); message CM_CTL3DCHANGED;
		procedure CMFontChanged(var aMsg: TMessage); message CM_FONTCHANGED;
	public
		{ Public declarations }
		constructor Create(anOwner: TComponent); override;
		destructor Destroy; override;

		procedure AddPane(aPane: TPane);
		procedure RemovePane(aPane: TPane);

		property Panes[anIndex: integer]: TPane read GetPane; default;
		property PercentsAsString: String read GetPercentsAsString write SetPercentsAsString;
		property Selected: TPane read FSelected write SetSelected;
		procedure GetHitTest(aX, aY: Integer;
			out aHitTest: TPaneHitTest; out aPane: TPane);

		procedure UpdatePanes;

		property PaneCount: Integer read GetPaneCount;
	published
		{ Published declarations }
		property Anchors;
		property Visible;
		property Align;
		property Ctl3D;
		property ParentCtl3D;
		property Font;
		property ParentFont;
		property ParentColor;
		property Color;
		property SplitType: TSplitType read FSplitType write SetSplitType default stHSplit;
		property FullResize: Boolean read FFullResize write FFullResize default False;
		property Flat: Boolean read FFlat write FFlat default True;
		property OnResize: TNotifyEvent read FOnResize write FOnResize;
	end;

var
  Designer_SelectComponent: procedure(aForm: TCustomForm; aComponent: TComponent) = nil;

implementation

uses
	InSysUtils, Math;

{	TPane }

constructor TPane.Create(anOwner: TComponent);
begin
	inherited Create(anOwner);
	FBorderStyle := bsSingle;
	FCtl3D := True;
	FParentCtl3D := True;
	FFont := TFont.Create;
	FFont.OnChange := OnChangeFont;
	FParentFont := True;
	FCaptionVisible := True;
	FVisible := True;
	FMinSize := GetSystemMetrics(SM_CXVSCROLL);
end;

destructor TPane.Destroy;
begin
	PaneFrame := nil;
	FFont.Free;
	inherited Destroy;
end;

procedure TPane.OnChangeFont(aSender: TObject);
begin
	FParentFont := False;
	ViewChanged;
end;

procedure TPane.Click;
begin
	if Assigned(FOnClick) then
		FOnClick(Self);
end;

procedure TPane.ViewChanged;
begin
	if not (csLoading in ComponentState) and Assigned(FPaneFrame) then
		FPaneFrame.UpdatePanes;
end;

function TPane.GetPercent: integer;
begin
	Result := IIF(Assigned(FPaneFrame) and (Self <> FPaneFrame.FPanes.Last),
		Round(FPercent / 100), 100);
end;

procedure TPane.SetVisible(aValue: Boolean);
begin
	if aValue <> FVisible then begin
		FVisible := aValue;
		if Assigned(FPaneFrame) then
			FPaneFrame.UpdatePanes;
		if Assigned(FControl) then
			FControl.Visible := aValue;
		if Assigned(FOnChangeVisible) then
			FOnChangeVisible(Self);
	end;
end;
	
procedure TPane.SetMinSize(aValue: Integer);
begin
	if aValue <> FMinSize then begin
		FMinSize := aValue;
		if Assigned(FPaneFrame) then
			FPaneFrame.UpdatePanes;
	end;
end;
	
procedure TPane.SetPercent(aPercent: integer);
begin
	FPercent := IIF(Assigned(FPaneFrame) and (Self = FPaneFrame.FPanes.Last),
		10000, aPercent * 100);
	if Assigned(FPaneFrame) and not (csLoading in ComponentState) then
		FPaneFrame.UpdatePanes;
end;

function TPane.HasParent: boolean;
begin
	HasParent := True;
end;

procedure TPane.ReadState(aReader: TReader);
begin
	inherited ReadState(aReader);
	if aReader.Parent is TPaneFrame then
		PaneFrame := TPaneFrame(aReader.Parent);
end;

procedure TPane.SetControl(aControl: TControl);
begin
	FControl := aControl;
	if Assigned(FPaneFrame) then
		FPaneFrame.UpdatePanes;
end;

procedure TPane.Notification(aComponent: TComponent; anOperation: TOperation);
begin
	inherited Notification(aComponent, anOperation);
	if (anOperation = opRemove) and (aComponent = FControl) then
		SetControl(nil); 
end;

procedure TPane.SetPaneFrame(aPaneFrame: TPaneFrame);
begin
	if FPaneFrame <> nil then
		FPaneFrame.RemovePane(Self);
	if aPaneFrame <> nil then
		aPaneFrame.AddPane(Self);
end;

procedure TPane.SetName(const aValue: TComponentName);
begin
	if Caption = Name then
		FCaption := aValue;
	inherited SetName(aValue);
end;

procedure TPane.Perform(aMsg: Word);
begin
	case aMsg of
		CM_PARENTFONTCHANGED:
			if FParentFont then begin
				SetFont(FPaneFrame.Font);
				FParentFont := True;
			end;

		CM_PARENTCTL3DCHANGED:
			if FParentCtl3D then begin
				SetCtl3D(FPaneFrame.Ctl3D);
				FParentCtl3D := True;
			end;
	end;
end;

procedure TPane.SetCaption(const aCaption: string);
begin
	FCaption := aCaption;
	ViewChanged
end;

procedure TPane.SetBorderStyle(aBorderStyle: TBorderStyle);
begin
	if aBorderStyle <> FBorderStyle then begin
		FBorderStyle := aBorderStyle;
		ViewChanged
	end;
end;

procedure TPane.SetCtl3D(aCtl3D: boolean);
begin
	if aCtl3D <> FCtl3D then begin
		FCtl3D := aCtl3D;
		ViewChanged
	end;
end;

procedure TPane.SetParentCtl3D(aParentCtl3D: boolean);
begin
	if aParentCtl3D <> FParentCtl3D then begin
		FParentCtl3D := aParentCtl3D;
		Perform(CM_PARENTCTL3DCHANGED);
	end;
end;

procedure TPane.SetFont(aFont: TFont);
begin
	FFont.Assign(aFont);
end;

procedure TPane.SetParentFont(aParentFont: Boolean);
begin
	if aParentFont <> FParentFont then begin
		FParentFont := aParentFont;
		Perform(CM_PARENTFONTCHANGED);
	end;
end;

procedure TPane.SetCaptionVisible(aCaptionVisible: Boolean);
begin
	if aCaptionVisible <> FCaptionVisible then begin
		FCaptionVisible := aCaptionVisible;
		ViewChanged
	end;
end;

procedure TPane.SetCloseButton(aValue: Boolean);
begin
	if aValue <> FCloseButton then begin
		FCloseButton := aValue;
		ViewChanged;
	end;
end;

function GetTextHeight(aFont: TFont; const aText: PChar; aTextLen: Integer): Integer;
var
	dc: HDC;
	hfontSave: HFONT;
	aSize: TSize;
begin
	dc := GetDC(0);
	hfontSave := SelectObject(dc, aFont.Handle);
	GetTextExtentPoint32(dc, aText, aTextLen, aSize);
	result := aSize.cy;
	SelectObject(dc, hfontSave);
	ReleaseDC(0, dc);
end;

procedure TPane.GetRects(var aRects: TPaneRects);
var
	aR: TRect;
	aCaption: TCaption;
begin
	aR := FPaneFrame.ClientRect;
	case FPaneFrame.FSplitType of
		stHSplit:
			begin
				aR.Left := FPos;
				aR.Right := FPos + FSize;
			end;
		stVSplit:
			begin
				aR.Top := FPos;
				aR.Bottom := FPos + FSize;
			end;
	end;
	aRects.Bounds := aR;
	if FBorderStyle <> bsNone then begin
		InflateRect(aR, -1, -1);
		Dec(aR.Right);
		Dec(aR.Bottom);
	end;
	aRects.Caption := aR;
	aRects.Control := aR;
	if FCaptionVisible then begin
		aCaption := SIF(FCaption = '', 'Ιπ', FCaption) + #0;
		aR.Bottom := aR.Top + 2 + GetTextHeight(FFont, @aCaption[1], Length(aCaption));
		if FBorderStyle <> bsNone then
			Inc(aR.Bottom, 2);
		aRects.Control.Top := aR.Bottom;
		aRects.Caption.Bottom := aR.Bottom;
	end;
	aRects.Button := aRects.Caption;
	InflateRect(aRects.Button, -2, -2);
	with aRects.Button do
		Left := Right - (Bottom - Top);

	aRects.Splitter := FPaneFrame.ClientRect;
	if FPaneFrame.FSplitType = stHSplit then begin
		aRects.Splitter.Left := FPos + FSize;
		aRects.Splitter.Right := aRects.Splitter.Left + FPaneFrame.GetSplitterSize + 1;
		if FBorderStyle <> bsNone then begin
			Dec(aRects.Splitter.Left);
			Inc(aRects.Splitter.Right);
		end;
	end
	else begin
		aRects.Splitter.Top := FPos + FSize;
		aRects.Splitter.Bottom := aRects.Splitter.Top + FPaneFrame.GetSplitterSize + 1;
		if FBorderStyle <> bsNone then begin
			Dec(aRects.Splitter.Top);
			Inc(aRects.Splitter.Bottom);
		end;
	end;
end;

procedure TPane.Paint(aCanvas: TCanvas; const pr: TPaneRects);
var
	aR: TRect;
	aSize: TSize;
	aP: TPoint;
	aButtonState: TPaneButtonState; 
begin
	aCanvas.Font := Font;
	aR := pr.Bounds;
	if FBorderStyle <> bsNone then begin
		DrawEdge(aCanvas.Handle, aR, BDR_SUNKENOUTER, BF_ADJUST or BF_RECT);
		DrawEdge(aCanvas.Handle, aR, BDR_RAISEDINNER, BF_ADJUST or BF_BOTTOMRIGHT);
	end;
	if FCaptionVisible then begin
		SetBkColor(aCanvas.Handle, ColorToRGB(clBtnFace));
		aCanvas.Font.Color := clBtnText;
		aR := pr.Caption;
		DrawEdge(aCanvas.Handle, aR, BDR_RAISEDINNER, BF_ADJUST or BF_LEFT or BF_TOP or BF_BOTTOM);
		aCanvas.TextRect(aR, aR.Left, aR.Top, ' ' + FCaption);
		if FCloseButton then begin
			aCanvas.Font.Name := 'Marlett';
			aCanvas.Font.Size := 6;
			aCanvas.Font.Style := [];
			aR := pr.Button;
			if FPaneFrame.FButtonPane = Self then
				aButtonState := FPaneFrame.FButtonState
			else
				aButtonState := pbsNormal;
			case aButtonState of
				pbsUp: DrawEdge(aCanvas.Handle, aR, BDR_RAISEDINNER, BF_ADJUST or BF_RECT);
				pbsDown: DrawEdge(aCanvas.Handle, aR, BDR_SUNKENOUTER, BF_ADJUST or BF_RECT);
			end;
			aSize := aCanvas.TextExtent('r');
			aP.X := (aR.Left + aR.Right - aSize.CX) div 2;
			aP.Y := (aR.Top + aR.Bottom - aSize.CY) div 2;
			if aButtonState = pbsDown then begin
				Inc(aP.X);
				Inc(aP.Y);
			end;
			aCanvas.TextRect(aR, aP.X, aP.Y, 'r');
		end;
	end;
	aCanvas.Brush.Style := bsSolid;
	aCanvas.Brush.Color := clWindow;
	aCanvas.FillRect(pr.Control);
end;

{	TPaneFrame }

constructor TPaneFrame.Create(anOwner: TComponent);
begin
	inherited Create(anOwner);
	ControlStyle := [csAcceptsControls, csCaptureMouse, csClickEvents,
										csOpaque, csDoubleClicks];
	FPanes := TList.Create;
	Width := 185;
	Height := 41;
	FFlat := True;
	FButtonTimer := TTimer.Create(nil);
	FButtonTimer.Interval := 100;
	FButtonTimer.Enabled := False;
	FButtonTimer.OnTimer := OnButtonTimer;
end;

destructor TPaneFrame.Destroy;
begin
	FButtonTimer.Free;
	while FPanes.Count > 0 do
		TPane(FPanes.Last).Free;
	FPanes.Free;
	inherited Destroy;
end;

function TPaneFrame.IsPaneVisible(anIndex: Integer): Boolean;
begin
	Result := (csDesigning in ComponentState) or Panes[anIndex].Visible; 
end;

procedure TPaneFrame.CMCtl3DChanged(var aMsg: TMessage);
var
	i: integer;
begin
	inherited;
	for i := 0 to FPanes.Count - 1 do
		Panes[i].Perform(CM_PARENTCTL3DCHANGED);
end;

procedure TPaneFrame.CMFontChanged(var aMsg: TMessage);
var
	i: integer;
begin
	inherited;
	for i := 0 to FPanes.Count - 1 do
		Panes[i].Perform(CM_PARENTFONTCHANGED);
end;

procedure TPaneFrame.UpdatePanes;
var
	i: integer;
	pr: TPaneRects;
begin
	UpdatePaneSizes;
	for i := 0 to FPanes.Count - 1 do
		if Panes[i].Control <> nil then begin
			Panes[i].GetRects(pr);
			Panes[i].Control.BoundsRect := pr.Control;
		end;
	Invalidate;
end;

procedure TPaneFrame.SetSplitType(aSplitType: TSplitType);
begin
	FSplitType := aSplitType;
	if not (csLoading in ComponentState) then 
		UpdatePanes;
end;

procedure TPaneFrame.Loaded;
begin
	inherited Loaded;
	UpdatePanes;
end;

function SkipNonDigits(const src: string; i: integer): integer;
begin
	while (i <= length(src)) and ((src[i] < '0') or (src[i] > '9')) do
		inc(i);
	result := i;
end;

function GetDigits(const src: string; var dst: string; i: integer): integer;
begin
	dst := '';
	while (i <= length(src)) and (src[i] >= '0') and (src[i] <= '9') do begin
		SetLength(dst, Length(dst) + 1);
		dst[length(dst)] := src[i];
		inc(i);
	end;
	result := i;
end;

procedure TPaneFrame.SetPercentsAsString(const aSrc: string);
var
	s: string;
	i: integer;
	paneIndex: integer;
begin
	paneIndex := 0;
	i := SkipNonDigits(aSrc, 1);
	while (paneIndex < FPanes.Count - 1) and (i <= length(aSrc)) do begin
		i := GetDigits(aSrc, s, i);
		Panes[paneIndex].FPercent := StrToInt(s);
		i := SkipNonDigits(aSrc, i);
		inc(paneIndex);
	end;
	UpdatePanes;
end;

function TPaneFrame.GetPercentsAsString: string;
var
	i: integer;
begin
	result := '';
	for i := 0 to FPanes.Count - 1 do
		if i = 0 then
			result := IntToStr(longint(Panes[0].FPercent))
		else
			result := result + ' ' + IntToStr(longint(Panes[i].FPercent))
end;

procedure TPaneFrame.SetName(const aValue: TComponentName);
var
	I: Integer;
	oldName, paneName, namePrefix: TComponentName;
	pane: TPane;
begin
	oldName := Name;
	inherited SetName(aValue);
	if (csDesigning in ComponentState) and (Name <> oldName) then
		{ In design mode the name of the panes should track the pane frame name }
		for i := 0 to FPanes.Count - 1 do begin
			pane := FPanes[i];
			if pane.Owner = Owner then begin
				paneName := pane.Name;
				namePrefix := paneName;
				if Length(namePrefix) > Length(oldName) then begin
					SetLength(namePrefix, Length(oldName));
					if CompareText(oldName, namePrefix) = 0 then begin
						System.Delete(paneName, 1, Length(oldName));
						System.Insert(aValue, paneName, 1);
						try
							pane.Name := paneName;
						except
							on e: EComponentError do Application.ShowException(e);
						end;
					end;
				end;
			end;
		end;
end;

procedure TPaneFrame.GetChildren(aProc: TGetChildProc; aRoot: TComponent);
var
	I: Integer;
	pane: TPane;
begin
	inherited GetChildren(aProc, aRoot);
	for I := 0 to FPanes.Count - 1 do
	begin
		pane := FPanes[I];
{			if pane.Owner = Writer.Root then Writer.WriteComponent(pane);}
		aProc(pane);
	end;
end;

procedure TPaneFrame.AddPane(aPane: TPane);
begin
	FPanes.Add(aPane);
	aPane.FPaneFrame := self;
	if not (csLoading in ComponentState) then
		Invalidate;
end;

procedure TPaneFrame.RemovePane(aPane: TPane);
begin
	aPane.FPaneFrame := nil;
	FPanes.Remove(aPane);
	if aPane = FSelected then
		FSelected := nil;
	if FPanes.Count > 0 then
		TPane(FPanes.Last).FPercent := 10000;
	if not (csLoading in ComponentState) then
		Invalidate;
end;

function TPaneFrame.GetPaneCount: Integer;
begin
	Result := FPanes.Count;
end;

function TPaneFrame.GetPane(anIndex: integer): TPane;
begin
	result := FPanes[anIndex];
end;

procedure TPaneFrame.SetSelected(aSelected: TPane);
begin
	FSelected := aSelected;

	if Assigned(FSelected) then
		ControlStyle := ControlStyle + [csAcceptsControls]
	else
		ControlStyle := ControlStyle - [csAcceptsControls];

	if not (csLoading in ComponentState) then
		Invalidate{Rect(Handle, nil, False)};
end;


function TPaneFrame.GetSplitterSize: Integer;
begin
	Result := GetSystemMetrics(IIF(SplitType = stVSplit, SM_CYFRAME, SM_CXFRAME)) - 1;
end;

procedure TPaneFrame.UpdatePaneSizes;
var
	i, aPos: Integer;
	aFrameSize, aSplitterSize: Integer;
	aLastPane: TPane;
begin
	aFrameSize := IIF(SplitType = stHSplit, Width, Height);
	aSplitterSize := GetSplitterSize;
	aPos := 0;
	aLastPane := nil;
	for i := 0 to FPanes.Count - 1 do
		if IsPaneVisible(i) then begin
			aLastPane := Panes[i];
			with aLastPane do begin
				FPos := aPos;
				FSize := Max(MulDiv(FPercent, aFrameSize, 10000) - aPos, FMinSize);
				Inc(aPos, FSize + aSplitterSize);
			end;
		end;
	if Assigned(aLastPane) then
		with aLastPane do 
			FSize := Max(aFrameSize - FPos, FMinSize);
end;

function TPaneFrame.GetSplitterRect(aPane: TPane; aSize: Integer): TRect;
begin
	Result := ClientRect;
	if aSize = 0 then
		aSize := aPane.FSize;
	if FSplitType = stHSplit then begin
		Result.Left := aPane.FPos + aSize;
		Result.Right := Result.Left + GetSplitterSize + 1;
		if aPane.FBorderStyle <> bsNone then begin
			Dec(Result.Left);
			Inc(Result.Right, IIF(aPane.FCtl3D, 1, 2));
		end;
	end
	else begin
		Result.Top := aPane.FPos + aSize;
		Result.Bottom := Result.Top + GetSplitterSize + 1;
		if aPane.FBorderStyle <> bsNone then begin
			Dec(Result.Top);
			Inc(Result.Bottom, IIF(aPane.FCtl3D, 1, 2));
		end;
	end;
end;

procedure TPaneFrame.Paint;
var
	i: integer;
	aR: TRect;
	pr: TPaneRects;
begin
	if FPanes.Count = 0 then begin
		aR := ClientRect;
		Canvas.Pen.Color := clWindowFrame;
		Canvas.Brush.Color := clWindowFrame;
		Canvas.Brush.Style := bsBDiagonal;
		Canvas.Rectangle(aR.Left, aR.Top, aR.Right, aR.Bottom);
	end
	else
		for i := 0 to FPanes.Count - 1 do
			if IsPaneVisible(i) then begin
				Panes[i].GetRects(pr);
				Panes[i].Paint(Canvas, pr);
				if i < FPanes.Count - 1 then begin
					Canvas.Brush.Color := clBtnFace;
					aR := pr.Splitter;
					if FSplitType = stHSplit then
						InflateRect(aR, -1, 0)
					else
						InflateRect(aR, 0, -1);
					Canvas.FillRect(aR);
				end;
			end;
end;

procedure ValidatePtr(const aPtr: IUnknown; const aMsg: string);
begin
	if aPtr = nil then
		raise Exception.Create(aMsg);
end;

procedure TPaneFrame.OnButtonTimer(aSender: TObject);
var
	aP: TPoint;
	aHitTest: TPaneHitTest;
	aPane: TPane;
begin
	aP := ScreenToClient(Mouse.CursorPos);
	GetHitTest(aP.X, aP.Y, aHitTest, aPane);
	if aHitTest = phtButton then
		if FState = pfsButtonDown then
			SetButtonState(FButtonPane, pbsDown)
		else
			SetButtonState(aPane, pbsUp)
	else
		SetButtonState(nil, pbsNormal);
end;

procedure TPaneFrame.SetButtonState(aPane: TPane; aState: TPaneButtonState);
var
	pr: TPaneRects;
begin
	if (aPane <> FButtonPane) or (aState <> FButtonState) then begin
		if Assigned(FButtonPane) and (aPane <> FButtonPane) then begin
			FButtonPane.GetRects(pr);
			InvalidateRect(WindowHandle, @pr.Button, False);
		end;
		FButtonPane := aPane;
		FButtonState := aState;
		if Assigned(aPane) then begin
			aPane.GetRects(pr);
			InvalidateRect(WindowHandle, @pr.Button, False);
			FButtonTimer.Enabled := True;
		end
		else
			FButtonTimer.Enabled := False;
	end;
end;

procedure TPaneFrame.GetHitTest(aX, aY: Integer;
	out aHitTest: TPaneHitTest; out aPane: TPane);
var
	i: Integer;
	pr: TPaneRects;
	p: TPoint;
begin
	p.X := aX;
	p.Y := aY;
	for i := 0 to FPanes.Count - 1 do begin
		aPane := FPanes[i];
		if IsPaneVisible(i) then begin
			aPane.GetRects(pr);
			if (i < FPanes.Count - 1) and PtInRect(pr.Splitter, p) then begin
				aHitTest := phtSplitter;
				Exit;
			end;
			if PtInRect(pr.Caption, p) then begin
				if not (csDesigning in ComponentState) and aPane.FCloseButton and PtInRect(pr.Button, p) then
					aHitTest := phtButton
				else
					aHitTest := phtCaption;
				Exit;
			end
		end;
	end;
	aHitTest := phtNone;
end;

procedure TPaneFrame.MouseDown(aButton: TMouseButton; aShift: TShiftState; aX, aY: Integer);
var
	aHitTest: TPaneHitTest;
	aPane: TPane;

	function GetSplitterMovementBounds(aPane: TPane): TRect;
	var
		aNextPane: TPane;
		i, aMin, aMax: Integer;
	begin
		Result := ClientRect;
		aNextPane := nil;
		for i := FPanes.IndexOf(aPane) + 1 to FPanes.Count - 1 do
			if IsPaneVisible(i) then begin
				aNextPane := Panes[i];
				break
			end;
		aMin := aPane.FPos + aPane.FMinSize;
		if Assigned(aNextPane) then
			aMax := aNextPane.FPos + aNextPane.FSize - aNextPane.FMinSize - GetSplitterSize
		else
			aMax := aMin;
		if FSplitType = stHSplit then begin
			Result.Left := aMin;
			Result.Right := aMax;
		end
		else begin
			Result.Top := aMin;
			Result.Bottom := aMax;
		end;
	end;

	procedure StartButtonDown;
	begin
		SetButtonState(aPane, pbsDown);
		FState := pfsButtonDown;
		MouseCapture := True;
	end;

	procedure StartDrag;
	var
		aR: TRect;
	begin
		FDragSplitter.Anchor := Point(aX, aY);
		FDragSplitter.LastPos := Point(aX, aY);
		FDragSplitter.Pane := aPane;
		aR := GetSplitterRect(aPane);
		FDragSplitter.SaveSize := aPane.FSize;
		aR := GetSplitterMovementBounds(aPane);
		WinProcs.ClientToScreen(Handle, aR.TopLeft);
		WinProcs.ClientToScreen(Handle, aR.BottomRight);
		ClipCursor(@aR);
		FState := pfsDragSplitter;
		RepaintDrag;
	end;

begin
	GetHitTest(aX, aY, aHitTest, aPane);
	if aButton = mbLeft then begin
		case aHitTest of
			phtCaption:
				if csDesigning in ComponentState then
					Designer_SelectComponent(ValidParentForm(Self), aPane)
				else
					aPane.Click;
			phtButton:
				if not (csDesigning in ComponentState) then
					StartButtonDown;
			phtSplitter:
				StartDrag;
		end;
	end;
end;

procedure TPaneFrame.MouseMove(aShift: TShiftState; aX, aY: Integer);
var
	aHitTest: TPaneHitTest;
	aPane: TPane;

	procedure NormalState;
	begin
		case aHitTest of
			phtSplitter: if FSplitType = stHSplit then Cursor := crHSplit else Cursor := crVSplit;
			phtButton:
				SetButtonState(aPane, pbsUp);
			else
				Cursor := crDefault;
		end;
	end;

	procedure ButtonDownState;
	begin
		if (aHitTest = phtButton) and (aPane = FButtonPane) then
			SetButtonState(aPane, pbsDown)
		else
			SetButtonState(aPane, pbsNormal)
	end;
	
	procedure DragSplitterState;
	var
		i: Integer;
	begin
		RepaintDrag;
		FDragSplitter.LastPos := Point(aX, aY);
		if FFullResize then begin
			ApplyDrag(Point(aX, aY));
			for i := 0 to FPanes.Count - 1 do
				with TPane(FPanes[i]) do
					if Visible then
						Update;
		end;
		RepaintDrag;
	end;

begin
	GetHitTest(aX, aY, aHitTest, aPane);
	case FState of
		pfsNormal: NormalState;
		pfsButtonDown: ButtonDownState;
		pfsDragSplitter: DragSplitterState;
	end;
end;

procedure TPaneFrame.MouseUp(
		aButton: TMouseButton;
		aShift: TShiftState;
		aX, aY: Integer);
var
	aHitTest: TPaneHitTest;
	aPane: TPane;
	
	procedure NormalState;
	begin
	end;

	procedure ButtonDownState;
	begin
		MouseCapture := False;
		if aHitTest = phtButton then begin
			if aPane = FButtonPane then
				FButtonPane.Visible := False;
			SetButtonState(aPane, pbsUp);
		end
		else
			SetButtonState(nil, pbsNormal);
		FState := pfsNormal;
	end;

	procedure DragSplitterState;
	begin
		RepaintDrag;
		ClipCursor(nil);
		ApplyDrag(Point(aX, aY));
		FState := pfsNormal;
		if Assigned(FOnResize) then
			FOnResize(self);
	end;

begin
	GetHitTest(aX, aY, aHitTest, aPane);
	case FState of
		pfsNormal: NormalState;
		pfsButtonDown: ButtonDownState;
		pfsDragSplitter: DragSplitterState;
	end;
end;

function LongPoint(aSmallPoint: TSmallPoint): TPoint;
begin
	Result.x := aSmallPoint.x;
	Result.y := aSmallPoint.y;
end;

procedure TPaneFrame.RepaintDrag;
var
	r: TRect;
	dc: HDC;
begin
	if not FFullResize then begin
		r := GetSplitterRect(FDragSplitter.Pane, FDragSplitter.SaveSize);
		if FSplitType = stHSplit then
			OffsetRect(r, FDragSplitter.LastPos.x - FDragSplitter.Anchor.x, 0)
		else
			OffsetRect(r, 0, FDragSplitter.LastPos.y - FDragSplitter.Anchor.y);
		WinProcs.ClientToScreen(Handle, r.TopLeft);
		WinProcs.ClientToScreen(Handle, r.BottomRight);
		dc := GetDC(0);
		DrawFocusRect(dc, r);
		ReleaseDC(0, dc);
	end;
end;

procedure TPaneFrame.ApplyDrag(aMouse: TPoint);
var
	aLength, aSize: Integer;
begin
	aSize := FDragSplitter.SaveSize;
	if FSplitType = stHSplit then begin
		aLength := Width;
		Inc(aSize, aMouse.x - FDragSplitter.Anchor.x)
	end
	else begin
		aLength := Height;
		Inc(aSize, aMouse.y - FDragSplitter.Anchor.y);
	end;
	FDragSplitter.Pane.FPercent := aSize * 10000 div aLength;
	UpdatePanes;
	FDragSplitter.LastPos := aMouse;
end;

procedure TPaneFrame.CMDesignHitTest(var aMsg: TCMDesignHitTest);
var
	i: integer;
	pr: TPaneRects;
begin
	if FState = pfsDragSplitter then begin
		aMsg.Result := 1;
		exit;
	end;
	aMsg.Result := 0;
	for i := 0 to FPanes.Count - 2 do
		if PtInRect(GetSplitterRect(FPanes[i]), LongPoint(aMsg.Pos)) then begin
			aMsg.Result := 1;
			break;
		end;
	for i := 0 to FPanes.Count - 1 do begin
		Panes[i].GetRects(pr);
		if PtInRect(pr.Caption, LongPoint(aMsg.Pos)) then begin
			aMsg.Result := 1;
			break;
		end;
	end;
end;

procedure TPaneFrame.WMSize(var aMsg: TWMSize);
begin
	inherited;
	UpdatePanes;
end;

procedure TPaneFrame.CreateParams(var Params: TCreateParams);
begin
	inherited;
	with Params, WindowClass do
		style := style and not (CS_VREDRAW or CS_HREDRAW);
end;

end.

