diff --git a/QuestPDF.Examples/DynamicExamples.cs b/QuestPDF.Examples/DynamicExamples.cs new file mode 100644 index 0000000..7353198 --- /dev/null +++ b/QuestPDF.Examples/DynamicExamples.cs @@ -0,0 +1,169 @@ +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using QuestPDF.Elements; +using QuestPDF.Examples.Engine; +using QuestPDF.Fluent; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; + +namespace QuestPDF.Examples +{ + public class OrderItem + { + public string ItemName { get; set; } = Placeholders.Label(); + public int Price { get; set; } = Placeholders.Random.Next(1, 11) * 10; + public int Count { get; set; } = Placeholders.Random.Next(1, 11); + } + + public class OrdersTable : IDynamicComponent + { + private ICollection Items { get; } + private ICollection ItemsLeft { get; set; } + + public OrdersTable(ICollection items) + { + Items = items; + } + + public void Compose(DynamicContext context, IDynamicContainer container) + { + if (context.Operation == DynamicLayoutOperation.Reset) + { + ItemsLeft = new List(Items); + return; + } + + var header = ComposeHeader(context); + var sampleFooter = ComposeFooter(context, Enumerable.Empty()); + var decorationHeight = header.Size.Height + sampleFooter.Size.Height; + + var rows = GetItemsForPage(context, decorationHeight).ToList(); + var footer = ComposeFooter(context, rows.Select(x => x.Item)); + + if (ItemsLeft.Count > rows.Count) + container.HasMoreContent(); + + if (context.Operation == DynamicLayoutOperation.Draw) + ItemsLeft = ItemsLeft.Skip(rows.Count).ToList(); + + container.MinimalBox().Decoration(decoration => + { + decoration.Header().Element(header); + + decoration.Content().Box().Stack(stack => + { + foreach (var row in rows) + stack.Item().Element(row.Element); + }); + + decoration.Footer().Element(footer); + }); + } + + private IDynamicElement ComposeHeader(DynamicContext context) + { + return context.CreateElement(element => + { + element + .BorderBottom(1) + .BorderColor(Colors.Grey.Darken2) + .Padding(5) + .Row(row => + { + var textStyle = TextStyle.Default.SemiBold(); + + row.ConstantItem(30).Text("#", textStyle); + row.RelativeItem().Text("Item name", textStyle); + row.ConstantItem(50).AlignRight().Text("Count", textStyle); + row.ConstantItem(50).AlignRight().Text("Price", textStyle); + row.ConstantItem(50).AlignRight().Text("Total", textStyle); + }); + }); + } + + private IDynamicElement ComposeFooter(DynamicContext context, IEnumerable items) + { + var total = items.Sum(x => x.Count * x.Price); + + return context.CreateElement(element => + { + element + .Padding(5) + .AlignRight() + .Text($"Subtotal: {total}$", TextStyle.Default.Size(14).SemiBold()); + }); + } + + private IEnumerable<(OrderItem Item, IDynamicElement Element)> GetItemsForPage(DynamicContext context, float decorationHeight) + { + var totalHeight = decorationHeight; + var counter = Items.Count - ItemsLeft.Count + 1; + + foreach (var orderItem in ItemsLeft) + { + var element = context.CreateElement(content => + { + content + .BorderBottom(1) + .BorderColor(Colors.Grey.Lighten2) + .Padding(5) + .Row(row => + { + row.ConstantItem(30).Text(counter++); + row.RelativeItem().Text(orderItem.ItemName); + row.ConstantItem(50).AlignRight().Text(orderItem.Count); + row.ConstantItem(50).AlignRight().Text($"{orderItem.Price}$"); + row.ConstantItem(50).AlignRight().Text($"{orderItem.Count*orderItem.Price}$"); + }); + }); + + var elementHeight = element.Size.Height; + + if (totalHeight + elementHeight > context.AvailableSize.Height) + break; + + totalHeight += elementHeight; + yield return (orderItem, element); + } + } + } + + public static class DynamicExamples + { + [Test] + public static void Dynamic() + { + RenderingTest + .Create() + .PageSize(PageSizes.A5) + .ShowResults() + .Render(container => + { + var items = Enumerable.Range(0, 25).Select(x => new OrderItem()).ToList(); + + container + .Background(Colors.White) + .Padding(25) + .Decoration(decoration => + { + decoration + .Header() + .PaddingBottom(5) + .Text(text => + { + text.DefaultTextStyle(TextStyle.Default.SemiBold().FontColor(Colors.Blue.Darken2).FontSize(16)); + text.Span("Page "); + text.CurrentPageNumber(); + text.Span(" of "); + text.TotalPages(); + }); + + decoration + .Content() + .Dynamic(new OrdersTable(items)); + }); + }); + } + } +} \ No newline at end of file diff --git a/QuestPDF/Drawing/DocumentGenerator.cs b/QuestPDF/Drawing/DocumentGenerator.cs index 6e43689..6974470 100644 --- a/QuestPDF/Drawing/DocumentGenerator.cs +++ b/QuestPDF/Drawing/DocumentGenerator.cs @@ -164,7 +164,7 @@ namespace QuestPDF.Drawing return debuggingState; } - private static void ApplyDefaultTextStyle(this Element? content, TextStyle documentDefaultTextStyle) + internal static void ApplyDefaultTextStyle(this Element? content, TextStyle documentDefaultTextStyle) { if (content == null) return; @@ -186,6 +186,9 @@ namespace QuestPDF.Drawing return; } + if (content is DynamicHost dynamicHost) + dynamicHost.TextStyle = documentDefaultTextStyle; + var targetTextStyle = documentDefaultTextStyle; if (content is DefaultTextStyle defaultTextStyleElement) diff --git a/QuestPDF/Elements/Dynamic.cs b/QuestPDF/Elements/Dynamic.cs new file mode 100644 index 0000000..a62c8cb --- /dev/null +++ b/QuestPDF/Elements/Dynamic.cs @@ -0,0 +1,125 @@ +using System; +using QuestPDF.Drawing; +using QuestPDF.Drawing.Exceptions; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; + +namespace QuestPDF.Elements +{ + internal class DynamicHost : Element, IStateResettable + { + private IDynamicComponent Child { get; } + internal TextStyle TextStyle { get; set; } + + public DynamicHost(IDynamicComponent child) + { + Child = child; + } + + public void ResetState() + { + GetContent(Size.Zero, DynamicLayoutOperation.Reset); + } + + internal override SpacePlan Measure(Size availableSpace) + { + var content = GetContent(availableSpace, DynamicLayoutOperation.Measure); + var measurement = content.Measure(availableSpace); + + if (measurement.Type != SpacePlanType.FullRender) + throw new DocumentLayoutException("Dynamic component generated content that does not fit on a single page."); + + return content.HasMoreContent + ? SpacePlan.PartialRender(measurement) + : SpacePlan.FullRender(measurement); + } + + internal override void Draw(Size availableSpace) + { + GetContent(availableSpace, DynamicLayoutOperation.Draw).Draw(availableSpace); + } + + DynamicContainer GetContent(Size availableSize, DynamicLayoutOperation operation) + { + var context = new DynamicContext + { + PageNumber = PageContext.CurrentPage, + PageContext = PageContext, + Canvas = Canvas, + TextStyle = TextStyle, + + AvailableSize = availableSize, + Operation = operation + }; + + var container = new DynamicContainer(); + Child.Compose(context, container); + + container.VisitChildren(x => x?.Initialize(PageContext, Canvas)); + container.VisitChildren(x => (x as IStateResettable)?.ResetState()); + + return container; + } + } + + public enum DynamicLayoutOperation + { + Reset, + Measure, + Draw + } + + public class DynamicContext + { + internal IPageContext PageContext { get; set; } + internal ICanvas Canvas { get; set; } + internal TextStyle TextStyle { get; set; } + + public int PageNumber { get; internal set; } + public Size AvailableSize { get; internal set; } + public DynamicLayoutOperation Operation { get; internal set; } + + public IDynamicElement CreateElement(Action content) + { + var container = new DynamicElement(); + content(container); + + container.ApplyDefaultTextStyle(TextStyle); + container.VisitChildren(x => x?.Initialize(PageContext, Canvas)); + container.VisitChildren(x => (x as IStateResettable)?.ResetState()); + + container.Size = container.Measure(AvailableSize); + + return container; + } + } + + public interface IDynamicContainer : IContainer + { + + } + + internal class DynamicContainer : Container, IDynamicContainer + { + internal bool HasMoreContent { get; set; } + } + + public static class DynamicContainerExtensions + { + public static IDynamicContainer HasMoreContent(this IDynamicContainer container) + { + (container as DynamicContainer).HasMoreContent = true; + return container; + } + } + + public interface IDynamicElement : IElement + { + Size Size { get; } + } + + internal class DynamicElement : ContainerElement, IDynamicElement + { + public Size Size { get; internal set; } + } +} \ No newline at end of file diff --git a/QuestPDF/Fluent/DynamicComponentExtensions.cs b/QuestPDF/Fluent/DynamicComponentExtensions.cs new file mode 100644 index 0000000..c5335ca --- /dev/null +++ b/QuestPDF/Fluent/DynamicComponentExtensions.cs @@ -0,0 +1,18 @@ +using QuestPDF.Elements; +using QuestPDF.Infrastructure; + +namespace QuestPDF.Fluent +{ + public static class DynamicComponentExtensions + { + public static void Dynamic(this IContainer element) where TDynamic : IDynamicComponent, new() + { + element.Dynamic(new TDynamic()); + } + + public static void Dynamic(this IContainer element, IDynamicComponent dynamicElement) + { + element.Element(new DynamicHost(dynamicElement)); + } + } +} \ No newline at end of file diff --git a/QuestPDF/Infrastructure/IDynamicComponent.cs b/QuestPDF/Infrastructure/IDynamicComponent.cs new file mode 100644 index 0000000..d31cb9f --- /dev/null +++ b/QuestPDF/Infrastructure/IDynamicComponent.cs @@ -0,0 +1,9 @@ +using QuestPDF.Elements; + +namespace QuestPDF.Infrastructure +{ + public interface IDynamicComponent + { + void Compose(DynamicContext context, IDynamicContainer container); + } +} \ No newline at end of file