diff --git a/Examples/Markdown4DStudioFMX/Markdown4DStudioFMX.Main.pas b/Examples/Markdown4DStudioFMX/Markdown4DStudioFMX.Main.pas
index 19635cd..5c3726e 100644
--- a/Examples/Markdown4DStudioFMX/Markdown4DStudioFMX.Main.pas
+++ b/Examples/Markdown4DStudioFMX/Markdown4DStudioFMX.Main.pas
@@ -197,6 +197,12 @@ TMarkdown4DStudioFMXForm = class(TForm, IPadEditorView, IPadShell)
procedure HandleDocumentHandedOver(const FileName: string);
procedure SetViewMode(const Mode: TPadViewMode);
procedure ApplyViewMode;
+ function AvailableSplitWidth: Single;
+ procedure ApplySplitEditorWidth(const DesiredWidth: Single);
+ procedure HandleSplitterMouseUp(Sender: TObject; Button: TMouseButton;
+ Shift: TShiftState; X, Y: Single);
+ procedure HandleTocSplitterMouseUp(Sender: TObject; Button: TMouseButton;
+ Shift: TShiftState; X, Y: Single);
procedure ShowFindBar;
procedure ShowReplaceBar;
procedure CloseFindBar;
@@ -313,6 +319,7 @@ implementation
Markdown4D.Ast.Interfaces,
Markdown4DStudio.Text,
Markdown4DStudio.Outline,
+ Markdown4DStudio.SplitLayout,
Markdown4DStudio.SessionSync,
Markdown4DStudio.Workspace,
Markdown4DStudio.LinkPolicy,
@@ -742,6 +749,7 @@ procedure TMarkdown4DStudioFMXForm.BuildTocPanel;
FTocSplitter.Parent := Self;
FTocSplitter.Align := TAlignLayout.Left;
FTocSplitter.Width := SplitterWidth;
+ FTocSplitter.OnMouseUp := HandleTocSplitterMouseUp;
end;
procedure TMarkdown4DStudioFMXForm.BuildEditorAndPreview;
@@ -757,6 +765,8 @@ procedure TMarkdown4DStudioFMXForm.BuildEditorAndPreview;
FMainSplitter.Parent := Self;
FMainSplitter.Align := TAlignLayout.Left;
FMainSplitter.Width := SplitterWidth;
+ // FMX splitters have no moved event, so a finished drag is caught here.
+ FMainSplitter.OnMouseUp := HandleSplitterMouseUp;
FPreview := TMarkdownViewer.Create(Self);
FPreview.Parent := Self;
@@ -1166,7 +1176,11 @@ procedure TMarkdown4DStudioFMXForm.Resize;
LayoutTitleBar;
if FZenActive then
- UpdateZenPadding;
+ UpdateZenPadding
+ else if FViewMode = TPadViewMode.Split then
+ // A narrower window must take room from the editor rather than from the
+ // preview, which would otherwise be squeezed to nothing.
+ ApplySplitEditorWidth(FEditor.Width);
if (FPalette <> nil) and FPalette.Visible then
FPalette.Position.X := (ClientWidth - FPalette.Width) / 2;
@@ -1185,6 +1199,62 @@ procedure TMarkdown4DStudioFMXForm.SetViewMode(const Mode: TPadViewMode);
ApplyViewMode;
end;
+function TMarkdown4DStudioFMXForm.AvailableSplitWidth: Single;
+begin
+ // What the editor and preview actually have to share.
+ Result := ClientWidth - FMainSplitter.Width;
+
+ if FTocPanel.Visible then
+ Result := Result - FTocPanel.Width;
+ if FTocSplitter.Visible then
+ Result := Result - FTocSplitter.Width;
+end;
+
+procedure TMarkdown4DStudioFMXForm.ApplySplitEditorWidth(const DesiredWidth: Single);
+begin
+ // Entering split view asks for the remembered width; a resize or a contents
+ // pane toggle asks for the width the editor already has, so that a splitter
+ // drag is kept and only trimmed when it no longer fits.
+
+ // The contents pane gives way first, so the two halves keep their minimum for
+ // as long as the window allows.
+ if FTocPanel.Visible then
+ FTocPanel.Width := TPadSplitLayout.ClampSidePanelWidth(Round(FTocPanel.Width),
+ Round(ClientWidth), Round(FTocSplitter.Width + FMainSplitter.Width),
+ MinPaneWidth, MinTocPanelWidth);
+
+ const Available = Round(AvailableSplitWidth);
+
+ // Re-stated on every width change, because a splitter told to honour a
+ // minimum the window cannot give lets a drag collapse the other pane.
+ FMainSplitter.MinSize := TPadSplitLayout.EffectiveMinPaneWidth(Available, MinPaneWidth);
+
+ FEditor.Width := TPadSplitLayout.ClampEditorWidth(Round(DesiredWidth), Available,
+ MinPaneWidth);
+end;
+
+procedure TMarkdown4DStudioFMXForm.HandleSplitterMouseUp(Sender: TObject;
+ Button: TMouseButton; Shift: TShiftState; X, Y: Single);
+begin
+ // A drag is the user stating a preference, so remember it. It still has to
+ // pass the clamp, because on a narrow window the splitter cannot enforce a
+ // minimum the window is too small to give.
+ ApplySplitEditorWidth(FEditor.Width);
+ FSplitEditorWidth := FEditor.Width;
+end;
+
+procedure TMarkdown4DStudioFMXForm.HandleTocSplitterMouseUp(Sender: TObject;
+ Button: TMouseButton; Shift: TShiftState; X, Y: Single);
+begin
+ // Widening the contents pane takes room from the two halves, so it stops
+ // where they would lose their minimum. Only split view reserves room for
+ // two panes; the single pane modes have nothing to protect.
+ if FZenActive or (FViewMode <> TPadViewMode.Split) then
+ Exit;
+
+ ApplySplitEditorWidth(FEditor.Width);
+end;
+
procedure TMarkdown4DStudioFMXForm.ApplyViewMode;
begin
case FViewMode of
@@ -1192,7 +1262,7 @@ procedure TMarkdown4DStudioFMXForm.ApplyViewMode;
begin
FEditor.Visible := True;
FEditor.Align := TAlignLayout.Left;
- FEditor.Width := FSplitEditorWidth;
+ ApplySplitEditorWidth(FSplitEditorWidth);
FMainSplitter.Visible := True;
FPreview.Visible := True;
end;
@@ -1639,6 +1709,10 @@ procedure TMarkdown4DStudioFMXForm.ToggleTocPane;
const ShowToc = not FTocPanel.Visible;
FTocPanel.Visible := ShowToc;
FTocSplitter.Visible := ShowToc;
+
+ // Showing the contents pane takes the same room the two halves share.
+ if (not FZenActive) and (FViewMode = TPadViewMode.Split) then
+ ApplySplitEditorWidth(FEditor.Width);
end;
procedure TMarkdown4DStudioFMXForm.HandleFindClick(Sender: TObject);
diff --git a/Examples/Markdown4DStudioFMX/Markdown4DStudioFMX.dpr b/Examples/Markdown4DStudioFMX/Markdown4DStudioFMX.dpr
index 36c90d5..afc96cf 100644
--- a/Examples/Markdown4DStudioFMX/Markdown4DStudioFMX.dpr
+++ b/Examples/Markdown4DStudioFMX/Markdown4DStudioFMX.dpr
@@ -21,6 +21,7 @@ uses
Markdown4DStudio.Defines in '..\Shared\Markdown4DStudio.Defines.pas',
Markdown4DStudio.Text in '..\Shared\Markdown4DStudio.Text.pas',
Markdown4DStudio.Outline in '..\Shared\Markdown4DStudio.Outline.pas',
+ Markdown4DStudio.SplitLayout in '..\Shared\Markdown4DStudio.SplitLayout.pas',
Markdown4DStudioFMX.Defines in 'Markdown4DStudioFMX.Defines.pas',
Markdown4DStudio.Shell in '..\Shared\Markdown4DStudio.Shell.pas',
Markdown4DStudio.CommandLine in '..\Shared\Markdown4DStudio.CommandLine.pas',
diff --git a/Examples/Markdown4DStudioVCL/Markdown4DStudioVCL.Main.pas b/Examples/Markdown4DStudioVCL/Markdown4DStudioVCL.Main.pas
index a6d90c2..4ab84e2 100644
--- a/Examples/Markdown4DStudioVCL/Markdown4DStudioVCL.Main.pas
+++ b/Examples/Markdown4DStudioVCL/Markdown4DStudioVCL.Main.pas
@@ -245,6 +245,10 @@ TPadChrome = record
function BuildCommandActions: TPadCommandActions;
procedure SetViewMode(const Mode: TPadViewMode);
procedure ApplyViewMode;
+ function AvailableSplitWidth: Integer;
+ procedure ApplySplitEditorWidth(const DesiredWidth: Integer);
+ procedure HandleSplitterMoved(Sender: TObject);
+ procedure HandleTocSplitterMoved(Sender: TObject);
procedure EnforceTopBarOrder;
procedure EnforceLeftPaneOrder;
procedure ShowFindBar;
@@ -298,6 +302,7 @@ implementation
Markdown4DStudio.SessionSync,
Markdown4DStudio.Text,
Markdown4DStudio.Outline,
+ Markdown4DStudio.SplitLayout,
Markdown4DStudio.Workspace,
Markdown4DStudio.LinkPolicy,
Markdown4DStudio.SingleInstance,
@@ -368,6 +373,9 @@ procedure TMarkdown4DStudioVCLForm.ConfigureControls;
begin
BuildReplaceControls;
lblFindCount.Caption := EmptyFindCaption;
+
+ splMain.OnMoved := HandleSplitterMoved;
+ splToc.OnMoved := HandleTocSplitterMoved;
end;
procedure TMarkdown4DStudioVCLForm.BuildToolbar;
@@ -893,6 +901,10 @@ procedure TMarkdown4DStudioVCLForm.ToggleTocPane;
pnlToc.Visible := ShowToc;
splToc.Visible := ShowToc;
+ // Showing the contents pane takes the same room the two halves share.
+ if (not FZenActive) and (FViewMode = TPadViewMode.Split) then
+ ApplySplitEditorWidth(mdEditor.Width);
+
EnforceLeftPaneOrder;
end;
@@ -1716,6 +1728,59 @@ procedure TMarkdown4DStudioVCLForm.SetViewMode(const Mode: TPadViewMode);
ApplyViewMode;
end;
+function TMarkdown4DStudioVCLForm.AvailableSplitWidth: Integer;
+begin
+ // What the editor and preview actually have to share.
+ Result := ClientWidth - splMain.Width;
+
+ if pnlToc.Visible then
+ Dec(Result, pnlToc.Width);
+ if splToc.Visible then
+ Dec(Result, splToc.Width);
+end;
+
+procedure TMarkdown4DStudioVCLForm.ApplySplitEditorWidth(const DesiredWidth: Integer);
+begin
+ // Entering split view asks for the remembered width; a resize or a contents
+ // pane toggle asks for the width the editor already has, so that a splitter
+ // drag is kept and only trimmed when it no longer fits.
+
+ // The contents pane gives way first, so the two halves keep their minimum for
+ // as long as the window allows.
+ if pnlToc.Visible then
+ pnlToc.Width := TPadSplitLayout.ClampSidePanelWidth(pnlToc.Width, ClientWidth,
+ splToc.Width + splMain.Width, MinPaneWidth, MinTocPanelWidth);
+
+ const Available = AvailableSplitWidth;
+
+ // Re-stated on every width change, because a splitter told to honour a
+ // minimum the window cannot give lets a drag collapse the other pane.
+ splMain.MinSize := TPadSplitLayout.EffectiveMinPaneWidth(Available, MinPaneWidth);
+
+ mdEditor.Width := TPadSplitLayout.ClampEditorWidth(DesiredWidth, Available,
+ MinPaneWidth);
+end;
+
+procedure TMarkdown4DStudioVCLForm.HandleSplitterMoved(Sender: TObject);
+begin
+ // A drag is the user stating a preference, so remember it. It still has to
+ // pass the clamp, because on a narrow window the splitter cannot enforce a
+ // minimum the window is too small to give.
+ ApplySplitEditorWidth(mdEditor.Width);
+ FSplitEditorWidth := mdEditor.Width;
+end;
+
+procedure TMarkdown4DStudioVCLForm.HandleTocSplitterMoved(Sender: TObject);
+begin
+ // Widening the contents pane takes room from the two halves, so it stops
+ // where they would lose their minimum. Only split view reserves room for
+ // two panes; the single pane modes have nothing to protect.
+ if FZenActive or (FViewMode <> TPadViewMode.Split) then
+ Exit;
+
+ ApplySplitEditorWidth(mdEditor.Width);
+end;
+
procedure TMarkdown4DStudioVCLForm.ApplyViewMode;
begin
case FViewMode of
@@ -1736,7 +1801,7 @@ procedure TMarkdown4DStudioVCLForm.ApplyViewMode;
begin
mdEditor.Align := alLeft;
mdEditor.Visible := True;
- mdEditor.Width := FSplitEditorWidth;
+ ApplySplitEditorWidth(FSplitEditorWidth);
splMain.Visible := True;
mdPreview.Visible := True;
end;
@@ -2058,7 +2123,11 @@ procedure TMarkdown4DStudioVCLForm.HandleResize(Sender: TObject);
LayoutTitleBar;
if FZenActive then
- UpdateZenPadding;
+ UpdateZenPadding
+ else if FViewMode = TPadViewMode.Split then
+ // A narrower window must take room from the editor rather than from the
+ // preview, which would otherwise be squeezed to nothing.
+ ApplySplitEditorWidth(mdEditor.Width);
if FPalette <> nil then
FPalette.Left := (ClientWidth - FPalette.Width) div 2;
diff --git a/Examples/Markdown4DStudioVCL/Markdown4DStudioVCL.dpr b/Examples/Markdown4DStudioVCL/Markdown4DStudioVCL.dpr
index ee14acb..e90b375 100644
--- a/Examples/Markdown4DStudioVCL/Markdown4DStudioVCL.dpr
+++ b/Examples/Markdown4DStudioVCL/Markdown4DStudioVCL.dpr
@@ -19,6 +19,7 @@ uses
Markdown4DStudio.Defines in '..\Shared\Markdown4DStudio.Defines.pas',
Markdown4DStudio.Text in '..\Shared\Markdown4DStudio.Text.pas',
Markdown4DStudio.Outline in '..\Shared\Markdown4DStudio.Outline.pas',
+ Markdown4DStudio.SplitLayout in '..\Shared\Markdown4DStudio.SplitLayout.pas',
Markdown4DStudioVCL.Defines in 'Markdown4DStudioVCL.Defines.pas',
Markdown4DStudio.Shell in '..\Shared\Markdown4DStudio.Shell.pas',
Markdown4DStudio.CommandLine in '..\Shared\Markdown4DStudio.CommandLine.pas',
diff --git a/Examples/Shared/Markdown4DStudio.Defines.pas b/Examples/Shared/Markdown4DStudio.Defines.pas
index 6116a62..a75fb86 100644
--- a/Examples/Shared/Markdown4DStudio.Defines.pas
+++ b/Examples/Shared/Markdown4DStudio.Defines.pas
@@ -55,6 +55,12 @@ interface
SeparatorWidth = 1;
FindEditWidth = 160;
TocPanelWidth = 240;
+ // Neither half of the split view may be squeezed below this, whether by a
+ // remembered width, a window resize or a splitter drag.
+ MinPaneWidth = 300;
+ // The contents pane gives way first to protect those two, but never below
+ // this; past that point the two halves share what is left between them.
+ MinTocPanelWidth = 120;
TickIntervalMilliseconds = 100;
FindBarHeight = 32;
FindBarEditWidth = 240;
diff --git a/Examples/Shared/Markdown4DStudio.SplitLayout.pas b/Examples/Shared/Markdown4DStudio.SplitLayout.pas
new file mode 100644
index 0000000..eb506a7
--- /dev/null
+++ b/Examples/Shared/Markdown4DStudio.SplitLayout.pas
@@ -0,0 +1,88 @@
+unit Markdown4DStudio.SplitLayout;
+
+// Geometry for the editor/preview split, deliberately free of any framework type
+// so the rule that neither pane may collapse is testable without a form and is
+// shared by the VCL and FMX studios.
+
+interface
+
+type
+ TPadSplitLayout = record
+ public
+ ///
+ /// Editor width to apply for the window as it is now.
+ ///
+ ///
+ /// Editor width remembered from the last time split view was active. It is
+ /// an absolute pixel value, so it can outlive the window size it was taken
+ /// at and ask for more room than there is.
+ ///
+ ///
+ /// What the editor and the preview share, with the contents pane and every
+ /// splitter already subtracted.
+ ///
+ ///
+ /// The least either pane may become. When the available width cannot hold
+ /// two of these, the space is shared evenly instead.
+ ///
+ class function ClampEditorWidth(const SavedWidth, AvailableWidth,
+ MinPaneWidth: Integer): Integer; static;
+
+ ///
+ /// The minimum a splitter can actually enforce for the window as it is
+ /// now. A splitter asked to honour a minimum the window cannot give lets a
+ /// drag hand the whole area to one pane, so below two minimums the answer
+ /// is half of what there is.
+ ///
+ class function EffectiveMinPaneWidth(const AvailableWidth,
+ MinPaneWidth: Integer): Integer; static;
+
+ ///
+ /// How wide a side panel, such as the contents pane, may be dragged before
+ /// it starts eating into the two halves of the split.
+ ///
+ /// The whole client width.
+ /// Width taken by the splitters themselves.
+ ///
+ /// How far the side panel may be asked to give way. Past that point it has
+ /// done all it can and the two panes share what is left between them. A
+ /// panel already narrower than this is never widened.
+ ///
+ class function ClampSidePanelWidth(const DesiredWidth, TotalWidth,
+ ReservedWidth, MinPaneWidth, MinSidePanelWidth: Integer): Integer; static;
+ end;
+
+implementation
+
+uses
+ System.Math;
+
+class function TPadSplitLayout.ClampEditorWidth(const SavedWidth, AvailableWidth,
+ MinPaneWidth: Integer): Integer;
+begin
+ // Too narrow to honour both minimums: share what there is rather than hand the
+ // whole area to one pane and leave the other invisible.
+ if AvailableWidth <= 2 * MinPaneWidth then
+ Exit(Max(0, AvailableWidth div 2));
+
+ Result := EnsureRange(SavedWidth, MinPaneWidth, AvailableWidth - MinPaneWidth);
+end;
+
+class function TPadSplitLayout.EffectiveMinPaneWidth(const AvailableWidth,
+ MinPaneWidth: Integer): Integer;
+begin
+ Result := Min(MinPaneWidth, Max(0, AvailableWidth div 2));
+end;
+
+class function TPadSplitLayout.ClampSidePanelWidth(const DesiredWidth, TotalWidth,
+ ReservedWidth, MinPaneWidth, MinSidePanelWidth: Integer): Integer;
+begin
+ // What the side panel could take while both panes still keep their minimum.
+ const Allowed = Max(0, TotalWidth - ReservedWidth - 2 * MinPaneWidth);
+
+ // Never widen the panel, only ask it to give way, and never past its own
+ // minimum: below that the two panes share what is left instead.
+ Result := Min(DesiredWidth, Max(Allowed, MinSidePanelWidth));
+end;
+
+end.
diff --git a/Tests/Markdown4D.Tests.dpr b/Tests/Markdown4D.Tests.dpr
index 5456fa0..ebbae24 100644
--- a/Tests/Markdown4D.Tests.dpr
+++ b/Tests/Markdown4D.Tests.dpr
@@ -89,6 +89,7 @@ uses
Markdown4DStudio.Defines in '..\Examples\Shared\Markdown4DStudio.Defines.pas',
Markdown4DStudio.Text in '..\Examples\Shared\Markdown4DStudio.Text.pas',
Markdown4DStudio.Outline in '..\Examples\Shared\Markdown4DStudio.Outline.pas',
+ Markdown4DStudio.SplitLayout in '..\Examples\Shared\Markdown4DStudio.SplitLayout.pas',
Markdown4DStudio.CommandLine in '..\Examples\Shared\Markdown4DStudio.CommandLine.pas',
Markdown4DStudio.Shell in '..\Examples\Shared\Markdown4DStudio.Shell.pas',
Markdown4DStudio.EditorView in '..\Examples\Shared\Markdown4DStudio.EditorView.pas',
@@ -105,6 +106,7 @@ uses
Markdown4DStudio.Controller.Tests in 'Markdown4DStudio.Controller.Tests.pas',
Markdown4DStudio.HtmlExport.Tests in 'Markdown4DStudio.HtmlExport.Tests.pas',
Markdown4DStudio.LinkPolicy.Tests in 'Markdown4DStudio.LinkPolicy.Tests.pas',
+ Markdown4DStudio.SplitLayout.Tests in 'Markdown4DStudio.SplitLayout.Tests.pas',
Markdown4D.Text.UrlSafety.Tests in 'Markdown4D.Text.UrlSafety.Tests.pas',
Markdown4D.Viewer.ImageSettings.Tests in 'Markdown4D.Viewer.ImageSettings.Tests.pas',
StreamingMarkdown.Demo.Tests in 'StreamingMarkdown.Demo.Tests.pas',
diff --git a/Tests/Markdown4DStudio.SplitLayout.Tests.pas b/Tests/Markdown4DStudio.SplitLayout.Tests.pas
new file mode 100644
index 0000000..db76273
--- /dev/null
+++ b/Tests/Markdown4DStudio.SplitLayout.Tests.pas
@@ -0,0 +1,164 @@
+unit Markdown4DStudio.SplitLayout.Tests;
+
+interface
+
+uses
+ DUnitX.TestFramework,
+ Markdown4DStudio.SplitLayout;
+
+type
+ [TestFixture]
+ TPadSplitLayoutTests = class
+ private
+ const
+ // A pane narrower than this is not worth showing, so the split always
+ // leaves at least this much for each side.
+ MinPane = 300;
+ // The contents pane yields to protect those two, but not below this.
+ MinSide = 120;
+
+ public
+ [Test]
+ procedure ClampEditorWidth_SavedWidthLeavesRoomForBothPanes_KeepsSavedWidth;
+
+ [Test]
+ procedure ClampEditorWidth_SavedWidthWouldCollapsePreview_ReservesPreviewMinimum;
+
+ [Test]
+ procedure ClampEditorWidth_SavedWidthBelowMinimum_RaisesToMinimum;
+
+ [Test]
+ procedure ClampEditorWidth_AvailableTooSmallForTwoPanes_SplitsEvenly;
+
+ [Test]
+ procedure ClampEditorWidth_WidthSavedAtWiderWindow_StillShowsPreview;
+
+ [Test]
+ procedure ClampEditorWidth_NoAvailableWidth_ReturnsZero;
+
+ [Test]
+ procedure EffectiveMinPaneWidth_RoomForBothPanes_ReturnsRequestedMinimum;
+
+ [Test]
+ procedure EffectiveMinPaneWidth_NotEnoughRoom_ReturnsHalfOfWhatThereIs;
+
+ [Test]
+ procedure EffectiveMinPaneWidth_NoAvailableWidth_ReturnsZero;
+
+ [Test]
+ procedure ClampSidePanelWidth_LeavesRoomForBothPanes_KeepsRequestedWidth;
+
+ [Test]
+ procedure ClampSidePanelWidth_WouldStarveThePanes_StopsAtWhatTheyNeed;
+
+ [Test]
+ procedure ClampSidePanelWidth_WindowTooSmallForBothPanes_StopsAtItsOwnMinimum;
+
+ [Test]
+ procedure ClampSidePanelWidth_AlreadyNarrowerThanItsMinimum_IsLeftAlone;
+ end;
+
+implementation
+
+procedure TPadSplitLayoutTests.ClampEditorWidth_SavedWidthLeavesRoomForBothPanes_KeepsSavedWidth;
+begin
+ // 480 for the editor leaves 480 for the preview, so nothing needs adjusting.
+ const Width = TPadSplitLayout.ClampEditorWidth(480, 960, MinPane);
+
+ Assert.AreEqual(480, Width);
+end;
+
+procedure TPadSplitLayoutTests.ClampEditorWidth_SavedWidthWouldCollapsePreview_ReservesPreviewMinimum;
+begin
+ // The editor asks for everything; the preview must still get its minimum.
+ const Width = TPadSplitLayout.ClampEditorWidth(960, 960, MinPane);
+
+ Assert.AreEqual(660, Width);
+end;
+
+procedure TPadSplitLayoutTests.ClampEditorWidth_SavedWidthBelowMinimum_RaisesToMinimum;
+begin
+ const Width = TPadSplitLayout.ClampEditorWidth(20, 960, MinPane);
+
+ Assert.AreEqual(MinPane, Width);
+end;
+
+procedure TPadSplitLayoutTests.ClampEditorWidth_AvailableTooSmallForTwoPanes_SplitsEvenly;
+begin
+ // Below two minimums there is no way to honour both, so share what there is.
+ const Width = TPadSplitLayout.ClampEditorWidth(500, 500, MinPane);
+
+ Assert.AreEqual(250, Width);
+end;
+
+procedure TPadSplitLayoutTests.ClampEditorWidth_WidthSavedAtWiderWindow_StillShowsPreview;
+begin
+ // Regression for the collapsing split view: an editor width remembered at a
+ // 1200 wide window is applied after the window shrinks to 900. Unclamped it
+ // swallowed the whole area and the preview disappeared, which made split view
+ // look exactly like editor only.
+ const Width = TPadSplitLayout.ClampEditorWidth(838, 650, MinPane);
+
+ Assert.AreEqual(350, Width);
+ Assert.IsTrue(650 - Width >= MinPane, 'preview pane must keep its minimum width');
+end;
+
+procedure TPadSplitLayoutTests.ClampEditorWidth_NoAvailableWidth_ReturnsZero;
+begin
+ // A window too small to lay out at all must not produce a negative width.
+ const Width = TPadSplitLayout.ClampEditorWidth(480, 0, MinPane);
+
+ Assert.AreEqual(0, Width);
+end;
+
+procedure TPadSplitLayoutTests.EffectiveMinPaneWidth_RoomForBothPanes_ReturnsRequestedMinimum;
+begin
+ Assert.AreEqual(MinPane, TPadSplitLayout.EffectiveMinPaneWidth(960, MinPane));
+end;
+
+procedure TPadSplitLayoutTests.EffectiveMinPaneWidth_NotEnoughRoom_ReturnsHalfOfWhatThereIs;
+begin
+ // A splitter cannot be asked to honour a minimum the window cannot give, or a
+ // drag ends up handing the whole area to one pane.
+ Assert.AreEqual(250, TPadSplitLayout.EffectiveMinPaneWidth(500, MinPane));
+ Assert.AreEqual(224, TPadSplitLayout.EffectiveMinPaneWidth(448, MinPane));
+end;
+
+procedure TPadSplitLayoutTests.EffectiveMinPaneWidth_NoAvailableWidth_ReturnsZero;
+begin
+ Assert.AreEqual(0, TPadSplitLayout.EffectiveMinPaneWidth(0, MinPane));
+end;
+
+procedure TPadSplitLayoutTests.ClampSidePanelWidth_LeavesRoomForBothPanes_KeepsRequestedWidth;
+begin
+ // 1100 wide, 8 for the two splitters, so the contents pane may grow to 492
+ // before the editor and preview lose their 300 each.
+ Assert.AreEqual(240, TPadSplitLayout.ClampSidePanelWidth(240, 1100, 8, MinPane, MinSide));
+ Assert.AreEqual(492, TPadSplitLayout.ClampSidePanelWidth(492, 1100, 8, MinPane, MinSide));
+end;
+
+procedure TPadSplitLayoutTests.ClampSidePanelWidth_WouldStarveThePanes_StopsAtWhatTheyNeed;
+begin
+ // Regression: dragging the contents divider right used to squeeze the preview
+ // down to a sliver, because nothing bounded how far the contents pane grew.
+ Assert.AreEqual(492, TPadSplitLayout.ClampSidePanelWidth(800, 1100, 8, MinPane, MinSide));
+ // The same rule makes the contents pane give way when the window narrows.
+ Assert.AreEqual(192, TPadSplitLayout.ClampSidePanelWidth(484, 800, 8, MinPane, MinSide));
+end;
+
+procedure TPadSplitLayoutTests.ClampSidePanelWidth_WindowTooSmallForBothPanes_StopsAtItsOwnMinimum;
+begin
+ // Past this point the contents pane has given all it can, and the two panes
+ // share what is left between them instead.
+ Assert.AreEqual(MinSide, TPadSplitLayout.ClampSidePanelWidth(484, 700, 8, MinPane, MinSide));
+ Assert.AreEqual(MinSide, TPadSplitLayout.ClampSidePanelWidth(484, 400, 8, MinPane, MinSide));
+end;
+
+procedure TPadSplitLayoutTests.ClampSidePanelWidth_AlreadyNarrowerThanItsMinimum_IsLeftAlone;
+begin
+ // A contents pane the user dragged narrow is never widened by the clamp.
+ Assert.AreEqual(60, TPadSplitLayout.ClampSidePanelWidth(60, 1100, 8, MinPane, MinSide));
+ Assert.AreEqual(60, TPadSplitLayout.ClampSidePanelWidth(60, 400, 8, MinPane, MinSide));
+end;
+
+end.