267 lines
8.9 KiB
C#
267 lines
8.9 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using QuestPDF.Drawing;
|
|
using QuestPDF.Infrastructure;
|
|
|
|
namespace QuestPDF.Elements
|
|
{
|
|
internal class InlinedElement : Container
|
|
{
|
|
|
|
}
|
|
|
|
internal enum InlinedAlignment
|
|
{
|
|
Left,
|
|
Center,
|
|
Right,
|
|
Justify,
|
|
SpaceAround
|
|
}
|
|
|
|
internal struct InlinedMeasurement
|
|
{
|
|
public Element Element { get; set; }
|
|
public SpacePlan Size { get; set; }
|
|
}
|
|
|
|
internal class Inlined : Element, IStateResettable, IContentDirectionAware
|
|
{
|
|
public ContentDirection ContentDirection { get; set; }
|
|
|
|
public List<InlinedElement> Elements { get; internal set; } = new List<InlinedElement>();
|
|
private Queue<InlinedElement> ChildrenQueue { get; set; }
|
|
|
|
internal float VerticalSpacing { get; set; }
|
|
internal float HorizontalSpacing { get; set; }
|
|
|
|
internal InlinedAlignment? ElementsAlignment { get; set; }
|
|
internal VerticalAlignment BaselineAlignment { get; set; }
|
|
|
|
public void ResetState()
|
|
{
|
|
ChildrenQueue = new Queue<InlinedElement>(Elements);
|
|
}
|
|
|
|
internal override IEnumerable<Element?> GetChildren()
|
|
{
|
|
return Elements;
|
|
}
|
|
|
|
internal override SpacePlan Measure(Size availableSpace)
|
|
{
|
|
SetDefaultAlignment();
|
|
|
|
if (!ChildrenQueue.Any())
|
|
return SpacePlan.FullRender(Size.Zero);
|
|
|
|
var lines = Compose(availableSpace);
|
|
|
|
if (!lines.Any())
|
|
return SpacePlan.Wrap();
|
|
|
|
var lineSizes = lines
|
|
.Select(line =>
|
|
{
|
|
var size = GetLineSize(line);
|
|
|
|
var widthWithSpacing = size.Width + (line.Count - 1) * HorizontalSpacing;
|
|
return new Size(widthWithSpacing, size.Height);
|
|
})
|
|
.ToList();
|
|
|
|
var width = lineSizes.Max(x => x.Width);
|
|
var height = lineSizes.Sum(x => x.Height) + (lines.Count - 1) * VerticalSpacing;
|
|
var targetSize = new Size(width, height);
|
|
|
|
var isPartiallyRendered = lines.Sum(x => x.Count) != ChildrenQueue.Count;
|
|
|
|
if (isPartiallyRendered)
|
|
return SpacePlan.PartialRender(targetSize);
|
|
|
|
return SpacePlan.FullRender(targetSize);
|
|
}
|
|
|
|
internal override void Draw(Size availableSpace)
|
|
{
|
|
SetDefaultAlignment();
|
|
|
|
var lines = Compose(availableSpace);
|
|
var topOffset = 0f;
|
|
|
|
foreach (var line in lines)
|
|
{
|
|
var height = line.Max(x => x.Size.Height);
|
|
|
|
DrawLine(line);
|
|
|
|
topOffset += height + VerticalSpacing;
|
|
Canvas.Translate(new Position(0, height + VerticalSpacing));
|
|
}
|
|
|
|
Canvas.Translate(new Position(0, -topOffset));
|
|
lines.SelectMany(x => x).ToList().ForEach(x => ChildrenQueue.Dequeue());
|
|
|
|
if (!ChildrenQueue.Any())
|
|
ResetState();
|
|
|
|
void DrawLine(ICollection<InlinedMeasurement> lineMeasurements)
|
|
{
|
|
var lineSize = GetLineSize(lineMeasurements);
|
|
|
|
var elementOffset = ElementOffset();
|
|
var leftOffset = AlignOffset();
|
|
|
|
foreach (var measurement in lineMeasurements)
|
|
{
|
|
var size = (Size)measurement.Size;
|
|
var baselineOffset = BaselineOffset(size, lineSize.Height);
|
|
|
|
if (size.Height == 0)
|
|
size = new Size(size.Width, lineSize.Height);
|
|
|
|
var offset = ContentDirection == ContentDirection.LeftToRight
|
|
? new Position(leftOffset, baselineOffset)
|
|
: new Position(availableSpace.Width - size.Width - leftOffset, baselineOffset);
|
|
|
|
Canvas.Translate(offset);
|
|
measurement.Element.Draw(size);
|
|
Canvas.Translate(offset.Reverse());
|
|
|
|
leftOffset += size.Width + elementOffset;
|
|
}
|
|
|
|
float ElementOffset()
|
|
{
|
|
var difference = availableSpace.Width - lineSize.Width;
|
|
|
|
if (lineMeasurements.Count == 1)
|
|
return 0;
|
|
|
|
return ElementsAlignment switch
|
|
{
|
|
InlinedAlignment.Justify => difference / (lineMeasurements.Count - 1),
|
|
InlinedAlignment.SpaceAround => difference / (lineMeasurements.Count + 1),
|
|
_ => HorizontalSpacing
|
|
};
|
|
}
|
|
|
|
float AlignOffset()
|
|
{
|
|
var emptySpace = availableSpace.Width - lineSize.Width - (lineMeasurements.Count - 1) * HorizontalSpacing;
|
|
|
|
return ElementsAlignment switch
|
|
{
|
|
InlinedAlignment.Left => ContentDirection == ContentDirection.LeftToRight ? 0 : emptySpace,
|
|
InlinedAlignment.Justify => 0,
|
|
InlinedAlignment.SpaceAround => elementOffset,
|
|
InlinedAlignment.Center => emptySpace / 2,
|
|
InlinedAlignment.Right => ContentDirection == ContentDirection.LeftToRight ? emptySpace : 0,
|
|
_ => 0
|
|
};
|
|
}
|
|
|
|
float BaselineOffset(Size elementSize, float lineHeight)
|
|
{
|
|
var difference = lineHeight - elementSize.Height;
|
|
|
|
return BaselineAlignment switch
|
|
{
|
|
VerticalAlignment.Top => 0,
|
|
VerticalAlignment.Middle => difference / 2,
|
|
_ => difference
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
void SetDefaultAlignment()
|
|
{
|
|
if (ElementsAlignment.HasValue)
|
|
return;
|
|
|
|
ElementsAlignment = ContentDirection == ContentDirection.LeftToRight
|
|
? InlinedAlignment.Left
|
|
: InlinedAlignment.Right;
|
|
}
|
|
|
|
Size GetLineSize(ICollection<InlinedMeasurement> measurements)
|
|
{
|
|
var width = measurements.Sum(x => x.Size.Width);
|
|
var height = measurements.Max(x => x.Size.Height);
|
|
|
|
return new Size(width, height);
|
|
}
|
|
|
|
// list of lines, each line is a list of elements
|
|
private ICollection<ICollection<InlinedMeasurement>> Compose(Size availableSize)
|
|
{
|
|
var queue = new Queue<InlinedElement>(ChildrenQueue);
|
|
var result = new List<ICollection<InlinedMeasurement>>();
|
|
|
|
var topOffset = 0f;
|
|
|
|
while (true)
|
|
{
|
|
var line = GetNextLine();
|
|
|
|
if (!line.Any())
|
|
break;
|
|
|
|
var height = line.Max(x => x.Size.Height);
|
|
|
|
if (topOffset + height > availableSize.Height + Size.Epsilon)
|
|
break;
|
|
|
|
topOffset += height + VerticalSpacing;
|
|
result.Add(line);
|
|
}
|
|
|
|
return result;
|
|
|
|
ICollection<InlinedMeasurement> GetNextLine()
|
|
{
|
|
var result = new List<InlinedMeasurement>();
|
|
var leftOffset = GetInitialAlignmentOffset();
|
|
|
|
while (true)
|
|
{
|
|
if (!queue.Any())
|
|
break;
|
|
|
|
var element = queue.Peek();
|
|
var size = element.Measure(new Size(availableSize.Width, Size.Max.Height));
|
|
|
|
if (size.Type == SpacePlanType.Wrap)
|
|
break;
|
|
|
|
if (leftOffset + size.Width > availableSize.Width + Size.Epsilon)
|
|
break;
|
|
|
|
queue.Dequeue();
|
|
leftOffset += size.Width + HorizontalSpacing;
|
|
|
|
result.Add(new InlinedMeasurement
|
|
{
|
|
Element = element,
|
|
Size = size
|
|
});
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
float GetInitialAlignmentOffset()
|
|
{
|
|
// this method makes sure that the spacing between elements is no lesser than configured
|
|
|
|
return ElementsAlignment switch
|
|
{
|
|
InlinedAlignment.SpaceAround => HorizontalSpacing * 2,
|
|
_ => 0
|
|
};
|
|
}
|
|
}
|
|
}
|
|
} |