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.