From 0d86039c0c9763e95cbeba4a3c48c8aba3a6efd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Zi=C4=85bek?= Date: Sun, 2 May 2021 14:40:08 +0200 Subject: [PATCH] Implemented experimental, tree-based simple stack and row --- QuestPDF.UnitTests/GridTests.cs | 24 +++--- QuestPDF/Elements/Decoration.cs | 6 +- QuestPDF/Elements/Empty.cs | 2 + QuestPDF/Elements/Page.cs | 6 +- QuestPDF/Elements/SimpleDecoration.cs | 4 +- QuestPDF/Elements/SimpleRow.cs | 47 +++++++++++ QuestPDF/Elements/SimpleStack.cs | 71 ++++++++++++++++ QuestPDF/Elements/SimpleStackCache.cs | 29 +++++++ QuestPDF/Elements/TreeRow.cs | 94 +++++++++++++++++++++ QuestPDF/Elements/TreeStack.cs | 58 +++++++++++++ QuestPDF/Fluent/RowExtensions.cs | 12 ++- QuestPDF/Fluent/StackExtensions.cs | 15 +++- QuestPDF/Infrastructure/ContainerElement.cs | 2 +- 13 files changed, 345 insertions(+), 25 deletions(-) create mode 100644 QuestPDF/Elements/SimpleRow.cs create mode 100644 QuestPDF/Elements/SimpleStack.cs create mode 100644 QuestPDF/Elements/SimpleStackCache.cs create mode 100644 QuestPDF/Elements/TreeRow.cs create mode 100644 QuestPDF/Elements/TreeStack.cs diff --git a/QuestPDF.UnitTests/GridTests.cs b/QuestPDF.UnitTests/GridTests.cs index 7da42c4..5153e39 100644 --- a/QuestPDF.UnitTests/GridTests.cs +++ b/QuestPDF.UnitTests/GridTests.cs @@ -45,20 +45,20 @@ namespace QuestPDF.UnitTests { row.RelativeColumn(6).Element(childA); row.RelativeColumn(4).Element(childB); - row.RelativeColumn(2).Element(new Empty()); + row.RelativeColumn(2).Element(Empty.Instance); }); stack.Item().Row(row => { row.RelativeColumn(4).Element(childC); row.RelativeColumn(2).Element(childD); - row.RelativeColumn(6).Element(new Empty()); + row.RelativeColumn(6).Element(Empty.Instance); }); stack.Item().Row(row => { row.RelativeColumn(8).Element(childE); - row.RelativeColumn(4).Element(new Empty()); + row.RelativeColumn(4).Element(Empty.Instance); }); }); @@ -97,25 +97,25 @@ namespace QuestPDF.UnitTests { stack.Item().Row(row => { - row.RelativeColumn(1).Element(new Empty()); + row.RelativeColumn(1).Element(Empty.Instance); row.RelativeColumn(6).Element(childA); row.RelativeColumn(4).Element(childB); - row.RelativeColumn(1).Element(new Empty()); + row.RelativeColumn(1).Element(Empty.Instance); }); stack.Item().Row(row => { - row.RelativeColumn(3).Element(new Empty()); + row.RelativeColumn(3).Element(Empty.Instance); row.RelativeColumn(4).Element(childC); row.RelativeColumn(2).Element(childD); - row.RelativeColumn(3).Element(new Empty()); + row.RelativeColumn(3).Element(Empty.Instance); }); stack.Item().Row(row => { - row.RelativeColumn(2).Element(new Empty()); + row.RelativeColumn(2).Element(Empty.Instance); row.RelativeColumn(8).Element(childE); - row.RelativeColumn(2).Element(new Empty()); + row.RelativeColumn(2).Element(Empty.Instance); }); }); @@ -154,21 +154,21 @@ namespace QuestPDF.UnitTests { stack.Item().Row(row => { - row.RelativeColumn(2).Element(new Empty()); + row.RelativeColumn(2).Element(Empty.Instance); row.RelativeColumn(6).Element(childA); row.RelativeColumn(4).Element(childB); }); stack.Item().Row(row => { - row.RelativeColumn(6).Element(new Empty()); + row.RelativeColumn(6).Element(Empty.Instance); row.RelativeColumn(4).Element(childC); row.RelativeColumn(2).Element(childD); }); stack.Item().Row(row => { - row.RelativeColumn(4).Element(new Empty()); + row.RelativeColumn(4).Element(Empty.Instance); row.RelativeColumn(8).Element(childE); }); }); diff --git a/QuestPDF/Elements/Decoration.cs b/QuestPDF/Elements/Decoration.cs index bef5762..93b15c2 100644 --- a/QuestPDF/Elements/Decoration.cs +++ b/QuestPDF/Elements/Decoration.cs @@ -5,9 +5,9 @@ namespace QuestPDF.Elements { internal class Decoration : IComponent { - public Element Header { get; set; } = new Empty(); - public Element Content { get; set; } = new Empty(); - public Element Footer { get; set; } = new Empty(); + public Element Header { get; set; } = Empty.Instance; + public Element Content { get; set; } = Empty.Instance; + public Element Footer { get; set; } = Empty.Instance; public void Compose(IContainer container) { diff --git a/QuestPDF/Elements/Empty.cs b/QuestPDF/Elements/Empty.cs index 9719912..4b4ad6c 100644 --- a/QuestPDF/Elements/Empty.cs +++ b/QuestPDF/Elements/Empty.cs @@ -5,6 +5,8 @@ namespace QuestPDF.Elements { internal class Empty : Element { + internal static Empty Instance { get; } = new Empty(); + internal override ISpacePlan Measure(Size availableSpace) { return new FullRender(Size.Zero); diff --git a/QuestPDF/Elements/Page.cs b/QuestPDF/Elements/Page.cs index b0e5176..9ebe953 100644 --- a/QuestPDF/Elements/Page.cs +++ b/QuestPDF/Elements/Page.cs @@ -5,9 +5,9 @@ namespace QuestPDF.Elements { internal class Page : IComponent { - public Element Header { get; set; } = new Empty(); - public Element Content { get; set; } = new Empty(); - public Element Footer { get; set; } = new Empty(); + public Element Header { get; set; } = Empty.Instance; + public Element Content { get; set; } = Empty.Instance; + public Element Footer { get; set; } = Empty.Instance; public void Compose(IContainer container) { diff --git a/QuestPDF/Elements/SimpleDecoration.cs b/QuestPDF/Elements/SimpleDecoration.cs index 7c36bf5..496d3f7 100644 --- a/QuestPDF/Elements/SimpleDecoration.cs +++ b/QuestPDF/Elements/SimpleDecoration.cs @@ -12,8 +12,8 @@ namespace QuestPDF.Elements internal class SimpleDecoration : Element { - public Element DecorationElement { get; set; } = new Empty(); - public Element ContentElement { get; set; } = new Empty(); + public Element DecorationElement { get; set; } = Empty.Instance; + public Element ContentElement { get; set; } = Empty.Instance; public DecorationType Type { get; set; } internal override ISpacePlan Measure(Size availableSpace) diff --git a/QuestPDF/Elements/SimpleRow.cs b/QuestPDF/Elements/SimpleRow.cs new file mode 100644 index 0000000..4f53b41 --- /dev/null +++ b/QuestPDF/Elements/SimpleRow.cs @@ -0,0 +1,47 @@ +using System; +using QuestPDF.Drawing.SpacePlan; +using QuestPDF.Infrastructure; + +namespace QuestPDF.Elements +{ + internal class SimpleRow : Element + { + internal Element Left { get; set; } + internal Element Right { get; set; } + + internal override ISpacePlan Measure(Size availableSpace) + { + var leftMeasurement = Left.Measure(new Size(availableSpace.Width, availableSpace.Height)) as Size; + + if (leftMeasurement == null) + return new Wrap(); + + var rightMeasurement = Right.Measure(new Size(availableSpace.Width - leftMeasurement.Width, availableSpace.Height)) as Size; + + if (rightMeasurement == null) + return new Wrap(); + + var totalWidth = leftMeasurement.Width + rightMeasurement.Width; + var totalHeight = Math.Max(leftMeasurement.Height, rightMeasurement.Height); + + var targetSize = new Size(totalWidth, totalHeight); + + if (leftMeasurement is PartialRender || rightMeasurement is PartialRender) + return new PartialRender(targetSize); + + return new FullRender(targetSize); + } + + internal override void Draw(ICanvas canvas, Size availableSpace) + { + var leftMeasurement = Left.Measure(new Size(availableSpace.Width, availableSpace.Height)); + var leftWidth = (leftMeasurement as Size)?.Width ?? 0; + + Left.Draw(canvas, new Size(leftWidth, availableSpace.Height)); + + canvas.Translate(new Position(leftWidth, 0)); + Right.Draw(canvas, new Size(availableSpace.Width - leftWidth, availableSpace.Height)); + canvas.Translate(new Position(-leftWidth, 0)); + } + } +} \ No newline at end of file diff --git a/QuestPDF/Elements/SimpleStack.cs b/QuestPDF/Elements/SimpleStack.cs new file mode 100644 index 0000000..e223a65 --- /dev/null +++ b/QuestPDF/Elements/SimpleStack.cs @@ -0,0 +1,71 @@ +using System; +using QuestPDF.Drawing.SpacePlan; +using QuestPDF.Infrastructure; + +namespace QuestPDF.Elements +{ + internal class SimpleStack : Element + { + internal Element Current { get; set; } = Empty.Instance; + internal Element Next { get; set; } = Empty.Instance; + + private bool IsFirstRendered { get; set; } = false; + + internal override ISpacePlan Measure(Size availableSpace) + { + var firstElement = IsFirstRendered ? Empty.Instance : Current; + var firstSize = firstElement.Measure(availableSpace) as Size; + + if (firstSize == null) + return new Wrap(); + + if (firstSize is PartialRender partialRender) + return partialRender; + + var spaceForSecond = new Size(availableSpace.Width, availableSpace.Height - firstSize.Height); + + var secondSize = Next.Measure(spaceForSecond) as Size; + + if (secondSize == null) + return new PartialRender(firstSize); + + var totalWidth = Math.Max(firstSize.Width, secondSize.Width); + var totalHeight = firstSize.Height + secondSize.Height; + + var targetSize = new Size(totalWidth, totalHeight); + + if (secondSize is PartialRender) + return new PartialRender(targetSize); + + return new FullRender(targetSize); + } + + internal override void Draw(ICanvas canvas, Size availableSpace) + { + var firstElement = IsFirstRendered ? Empty.Instance : Current; + + var firstMeasurement = firstElement.Measure(availableSpace); + + if (firstMeasurement is FullRender) + IsFirstRendered = true; + + var firstSize = firstMeasurement as Size; + + if (firstSize != null) + firstElement.Draw(canvas, firstSize); + + var firstHeight = firstSize?.Height ?? 0; + + var spaceForSecond = new Size(availableSpace.Width, availableSpace.Height - firstHeight); + + var secondMeasurement = Next?.Measure(spaceForSecond); + + if (secondMeasurement is Wrap) + return; + + canvas.Translate(new Position(0, firstHeight)); + Next.Draw(canvas, secondMeasurement as Size); + canvas.Translate(new Position(0, -firstHeight)); + } + } +} \ No newline at end of file diff --git a/QuestPDF/Elements/SimpleStackCache.cs b/QuestPDF/Elements/SimpleStackCache.cs new file mode 100644 index 0000000..b84d28c --- /dev/null +++ b/QuestPDF/Elements/SimpleStackCache.cs @@ -0,0 +1,29 @@ +using System; +using QuestPDF.Drawing.SpacePlan; +using QuestPDF.Infrastructure; + +namespace QuestPDF.Elements +{ + internal class SimpleStackWithCache : SimpleStack + { + private ISpacePlan? Cache { get; set; } + private Size? CacheForSize { get; set; } + + internal override ISpacePlan Measure(Size availableSpace) + { + if (Cache != null && CacheForSize != null && CacheForSize.Equals(availableSpace)) + return Cache; + + CacheForSize = availableSpace; + Cache = base.Measure(availableSpace); + return Cache; + } + + internal override void Draw(ICanvas canvas, Size availableSpace) + { + base.Draw(canvas, availableSpace); + Cache = null; + CacheForSize = null; + } + } +} \ No newline at end of file diff --git a/QuestPDF/Elements/TreeRow.cs b/QuestPDF/Elements/TreeRow.cs new file mode 100644 index 0000000..cc2ea1b --- /dev/null +++ b/QuestPDF/Elements/TreeRow.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using QuestPDF.Drawing.SpacePlan; +using QuestPDF.Fluent; +using QuestPDF.Infrastructure; + +namespace QuestPDF.Elements +{ + internal class TreeRow : Element + { + public ICollection Children { get; internal set; } = new List(); + public float Spacing { get; set; } = 0; + + public Element Compose(float availableWidth) + { + var elements = ReduceRows(AddSpacing(Children)); + return BuildTree(elements.ToArray()); + + ICollection ReduceRows(ICollection elements) + { + var constantWidth = elements + .Where(x => x is ConstantRowElement) + .Cast() + .Sum(x => x.Width); + + var relativeWidth = elements + .Where(x => x is RelativeRowElement) + .Cast() + .Sum(x => x.Width); + + var widthPerRelativeUnit = (availableWidth - constantWidth) / relativeWidth; + + return elements + .Select(x => + { + if (x is RelativeRowElement r) + return new ConstantRowElement(r.Width * widthPerRelativeUnit) + { + Child = x.Child + }; + + return x; + }) + .Select(x => new Constrained + { + MinWidth = x.Width, + MaxWidth = x.Width, + Child = x.Child + }) + .Cast() + .ToList(); + } + + ICollection AddSpacing(ICollection elements) + { + if (Spacing < Size.Epsilon) + return elements; + + return elements + .SelectMany(x => new[] { new ConstantRowElement(Spacing), x }) + .Skip(1) + .ToList(); + } + + Element BuildTree(Span elements) + { + if (elements.IsEmpty) + return Empty.Instance; + + if (elements.Length == 1) + return elements[0]; + + var half = elements.Length / 2; + + return new SimpleRow + { + Left = BuildTree(elements.Slice(0, half)), + Right = BuildTree(elements.Slice(half)) + }; + } + } + + internal override ISpacePlan Measure(Size availableSpace) + { + return Compose(availableSpace.Width).Measure(availableSpace); + } + + internal override void Draw(ICanvas canvas, Size availableSpace) + { + Compose(availableSpace.Width).Draw(canvas, availableSpace); + } + } +} \ No newline at end of file diff --git a/QuestPDF/Elements/TreeStack.cs b/QuestPDF/Elements/TreeStack.cs new file mode 100644 index 0000000..c097c47 --- /dev/null +++ b/QuestPDF/Elements/TreeStack.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using QuestPDF.Fluent; +using QuestPDF.Infrastructure; +using IComponent = QuestPDF.Infrastructure.IComponent; +using IContainer = QuestPDF.Infrastructure.IContainer; + +namespace QuestPDF.Elements +{ + internal class TreeStack : IComponent + { + public ICollection Children { get; internal set; } = new List(); + public float Spacing { get; set; } = 0; + + public void Compose(IContainer container) + { + var elements = AddSpacing(Children); + + container + .PaddingBottom(-Spacing) + .Element(BuildTree(elements.ToArray())); + + ICollection AddSpacing(ICollection elements) + { + if (Spacing < Size.Epsilon) + return elements; + + return elements + .Select(x => new Padding + { + Bottom = Spacing, + Child = x + }) + .Cast() + .ToList(); + } + + Element BuildTree(Span elements) + { + if (elements.IsEmpty) + return Empty.Instance; + + if (elements.Length == 1) + return elements[0]; + + var half = elements.Length / 2; + + return new SimpleStack + { + Current = BuildTree(elements.Slice(0, half)), + Next = BuildTree(elements.Slice(half)) + }; + } + } + } +} \ No newline at end of file diff --git a/QuestPDF/Fluent/RowExtensions.cs b/QuestPDF/Fluent/RowExtensions.cs index de24a37..ec9e8af 100644 --- a/QuestPDF/Fluent/RowExtensions.cs +++ b/QuestPDF/Fluent/RowExtensions.cs @@ -33,9 +33,18 @@ namespace QuestPDF.Fluent } internal Element CreateRow() + { + return new TreeRow + { + Children = Elements, + Spacing = RowSpacing + }; + } + + internal Element CreateRow2() { if (Elements.Count == 0) - return new Empty(); + return Empty.Instance; if (RowSpacing <= Size.Epsilon) return new Row @@ -67,6 +76,7 @@ namespace QuestPDF.Fluent var descriptor = new RowDescriptor(); handler(descriptor); element.Element(descriptor.CreateRow()); + //element.Element(descriptor.CreateRow2()); } } } \ No newline at end of file diff --git a/QuestPDF/Fluent/StackExtensions.cs b/QuestPDF/Fluent/StackExtensions.cs index 1197163..4723884 100644 --- a/QuestPDF/Fluent/StackExtensions.cs +++ b/QuestPDF/Fluent/StackExtensions.cs @@ -28,10 +28,19 @@ namespace QuestPDF.Fluent handler?.Invoke(Item()); } - internal Element CreateStack() + internal IComponent CreateStack() + { + return new TreeStack + { + Children = Items, + Spacing = StackSpacing + }; + } + + internal Element CreateStack2() { if (Items.Count == 0) - return new Empty(); + return Empty.Instance; if (StackSpacing <= Size.Epsilon) return new Stack @@ -67,7 +76,7 @@ namespace QuestPDF.Fluent { var descriptor = new StackDescriptor(); handler(descriptor); - element.Element(descriptor.CreateStack()); + element.Component(descriptor.CreateStack()); } } } \ No newline at end of file diff --git a/QuestPDF/Infrastructure/ContainerElement.cs b/QuestPDF/Infrastructure/ContainerElement.cs index 831670d..8c9b4c2 100644 --- a/QuestPDF/Infrastructure/ContainerElement.cs +++ b/QuestPDF/Infrastructure/ContainerElement.cs @@ -5,7 +5,7 @@ namespace QuestPDF.Infrastructure { internal abstract class ContainerElement : Element, IContainer { - internal Element? Child { get; set; } = new Empty(); + internal Element? Child { get; set; } = Empty.Instance; IElement? IContainer.Child {