Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ jobs:
for project in \
src/FlexRender.Core/FlexRender.Core.csproj \
src/FlexRender.Yaml/FlexRender.Yaml.csproj \
src/FlexRender.Xml/FlexRender.Xml.csproj \
src/FlexRender.Http/FlexRender.Http.csproj \
src/FlexRender.Skia.Render/FlexRender.Skia.Render.csproj \
src/FlexRender.ImageSharp.Render/FlexRender.ImageSharp.Render.csproj \
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ The release workflow (`.github/workflows/release.yml`) publishes all packages to

| Category | Packages |
|----------|----------|
| Core | `FlexRender.Core`, `FlexRender.Yaml`, `FlexRender.Http` |
| Core | `FlexRender.Core`, `FlexRender.Yaml`, `FlexRender.Xml`, `FlexRender.Http` |
| Renderers | `FlexRender.Skia.Render`, `FlexRender.ImageSharp.Render`, `FlexRender.Svg.Render` |
| QR providers | `FlexRender.QrCode.Skia.Render`, `FlexRender.QrCode.ImageSharp.Render`, `FlexRender.QrCode.Svg.Render` |
| Barcode providers | `FlexRender.Barcode.Skia.Render`, `FlexRender.Barcode.ImageSharp.Render`, `FlexRender.Barcode.Svg.Render` |
Expand Down
11 changes: 10 additions & 1 deletion src/FlexRender.Core/Layout/ColumnFlexLayoutStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,12 @@ internal static void LayoutColumnFlex(LayoutNode node, FlexElement flex, LayoutC
pos += m.Top.IsAuto ? spacePerAuto : m.Top.ResolvedPixels;
child.Y = pos;
pos += child.Height;
pos += m.Bottom.IsAuto ? spacePerAuto : m.Bottom.ResolvedPixels;
var autoBottom = m.Bottom.IsAuto ? spacePerAuto : m.Bottom.ResolvedPixels;
pos += autoBottom;

// Store resolved outer right/bottom margins for trailing-margin auto-size.
child.MarginBottom = Math.Max(0f, autoBottom);
child.MarginRight = Math.Max(0f, m.Right.IsAuto ? 0f : m.Right.ResolvedPixels);

// Cross axis auto margins override align-items (horizontal for column)
ApplyColumnCrossAxisMargins(child, m, flex, padding, crossAxisSize);
Expand Down Expand Up @@ -280,6 +285,10 @@ internal static void LayoutColumnFlex(LayoutNode node, FlexElement flex, LayoutC
var mLeft = Math.Max(0f, m.Left.ResolvedPixels);
var mRight = Math.Max(0f, m.Right.ResolvedPixels);

// Store resolved outer right/bottom margins for trailing-margin auto-size.
child.MarginBottom = mBottom;
child.MarginRight = mRight;

// Check for cross axis auto margins even when main axis has no auto margins
if (m.CrossAxisAutoCount(isColumn: true) > 0)
{
Expand Down
33 changes: 25 additions & 8 deletions src/FlexRender.Core/Layout/LayoutEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -401,8 +401,10 @@ private LayoutNode LayoutFlexElement(FlexElement flex, LayoutContext context)
MirrorRowXPositions(node, effectivePadding);
}

// Calculate height if not specified (skip for wrapped containers — they set height in LayoutWrappedFlex)
if (height == 0f && node.Children.Count > 0 && flex.Wrap.Value == FlexWrap.NoWrap)
// Compute auto height when the strategy did not set it — covers no-wrap and
// column-wrap. Row-wrap already sets height (its cross axis), so node.Height is
// non-zero there and this is skipped.
if (height == 0f && node.Children.Count > 0 && node.Height == 0f)
{
node.Height = LayoutHelpers.CalculateTotalHeight(node) + effectivePadding.Bottom;
}
Expand Down Expand Up @@ -594,6 +596,7 @@ private LayoutNode LayoutTextElement(TextElement text, LayoutContext context)
var totalHeight = contentHeight + padding.Vertical + border.Vertical;

var node = new LayoutNode(text, 0, 0, totalWidth, totalHeight);
node.ContentInset = CombineInset(padding, border);
node.TextLines = textLines;
node.ComputedLineHeight = computedLineHeight;
node.Baseline = padding.Top + border.Top.Width + textBaseline;
Expand Down Expand Up @@ -663,7 +666,7 @@ private static LayoutNode LayoutQrElement(QrElement qr, LayoutContext context)
var totalWidth = contentWidth + padding.Horizontal + border.Horizontal;
var totalHeight = contentHeight + padding.Vertical + border.Vertical;

return new LayoutNode(qr, 0, 0, totalWidth, totalHeight) { ComputedFontSize = context.FontSize };
return new LayoutNode(qr, 0, 0, totalWidth, totalHeight) { ComputedFontSize = context.FontSize, ContentInset = CombineInset(padding, border) };
}

/// <summary>
Expand All @@ -683,7 +686,7 @@ private static LayoutNode LayoutBarcodeElement(BarcodeElement barcode, LayoutCon
var totalWidth = contentWidth + padding.Horizontal + border.Horizontal;
var totalHeight = contentHeight + padding.Vertical + border.Vertical;

return new LayoutNode(barcode, 0, 0, totalWidth, totalHeight) { ComputedFontSize = context.FontSize };
return new LayoutNode(barcode, 0, 0, totalWidth, totalHeight) { ComputedFontSize = context.FontSize, ContentInset = CombineInset(padding, border) };
}

/// <summary>
Expand All @@ -702,7 +705,7 @@ private static LayoutNode LayoutImageElement(ImageElement image, LayoutContext c
var totalWidth = contentWidth + padding.Horizontal + border.Horizontal;
var totalHeight = contentHeight + padding.Vertical + border.Vertical;

return new LayoutNode(image, 0, 0, totalWidth, totalHeight) { ComputedFontSize = context.FontSize };
return new LayoutNode(image, 0, 0, totalWidth, totalHeight) { ComputedFontSize = context.FontSize, ContentInset = CombineInset(padding, border) };
}

/// <summary>
Expand All @@ -720,7 +723,7 @@ private static LayoutNode LayoutSvgElement(SvgElement svg, LayoutContext context
var totalWidth = contentWidth + padding.Horizontal + border.Horizontal;
var totalHeight = contentHeight + padding.Vertical + border.Vertical;

return new LayoutNode(svg, 0, 0, totalWidth, totalHeight) { ComputedFontSize = context.FontSize };
return new LayoutNode(svg, 0, 0, totalWidth, totalHeight) { ComputedFontSize = context.FontSize, ContentInset = CombineInset(padding, border) };
}

/// <summary>
Expand Down Expand Up @@ -750,7 +753,7 @@ private static LayoutNode LayoutSeparatorElement(SeparatorElement separator, Lay
var totalWidth = contentWidth + padding.Horizontal + border.Horizontal;
var totalHeight = contentHeight + padding.Vertical + border.Vertical;

return new LayoutNode(separator, 0, 0, totalWidth, totalHeight) { ComputedFontSize = context.FontSize };
return new LayoutNode(separator, 0, 0, totalWidth, totalHeight) { ComputedFontSize = context.FontSize, ContentInset = CombineInset(padding, border) };
}

/// <summary>
Expand All @@ -773,9 +776,23 @@ private static LayoutNode LayoutShapeElement(TemplateElement shape, LayoutContex
var totalWidth = contentWidth + padding.Horizontal + border.Horizontal;
var totalHeight = contentHeight + padding.Vertical + border.Vertical;

return new LayoutNode(shape, 0, 0, totalWidth, totalHeight) { ComputedFontSize = context.FontSize };
return new LayoutNode(shape, 0, 0, totalWidth, totalHeight) { ComputedFontSize = context.FontSize, ContentInset = CombineInset(padding, border) };
}

/// <summary>
/// Combines padding and border widths into a single content inset (per side).
/// This is the distance from a leaf element's box edge to its content area, used by
/// renderers to inset content while keeping the background and border on the full box.
/// </summary>
/// <param name="padding">The resolved padding values (already clamped to non-negative).</param>
/// <param name="border">The resolved border values for all four sides.</param>
/// <returns>The combined content inset per side.</returns>
private static PaddingValues CombineInset(PaddingValues padding, BorderValues border) => new(
padding.Top + border.Top.Width,
padding.Right + border.Right.Width,
padding.Bottom + border.Bottom.Width,
padding.Left + border.Left.Width);

/// <summary>
/// Checks whether any flow (non-absolute) child in the node has a non-default
/// <see cref="Parsing.Ast.TemplateElement.Order"/> value.
Expand Down
7 changes: 4 additions & 3 deletions src/FlexRender.Core/Layout/LayoutHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -229,8 +229,9 @@ internal static float CalculateTotalHeight(LayoutNode node)
foreach (var child in node.Children)
{
if (child.Element.Position.Value == Position.Absolute) continue;
if (child.Bottom > maxBottom)
maxBottom = child.Bottom;
var bottom = child.Bottom + child.MarginBottom;
if (bottom > maxBottom)
maxBottom = bottom;
}
return maxBottom;
}
Expand All @@ -249,7 +250,7 @@ internal static float CalculateTotalWidth(LayoutNode node)
foreach (var child in node.Children)
{
if (child.Element.Position.Value == Position.Absolute) continue;
var right = child.X + child.Width;
var right = child.X + child.Width + child.MarginRight;
if (right > maxRight)
maxRight = right;
}
Expand Down
24 changes: 24 additions & 0 deletions src/FlexRender.Core/Layout/LayoutNode.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using FlexRender.Layout.Units;
using FlexRender.Parsing.Ast;

namespace FlexRender.Layout;
Expand Down Expand Up @@ -67,6 +68,29 @@ public sealed class LayoutNode
/// </summary>
public LayoutDiagnostics? Diagnostics { get; set; }

/// <summary>
/// The content inset (padding + border) for each side, in pixels. This is the distance
/// from the element's box edge to its content area. Renderers subtract this inset when
/// drawing leaf content so that padding insets content on all sides, while the background
/// and border keep using the full box. Defaults to <see cref="PaddingValues.Zero"/>;
/// container (flex) nodes leave it zero because their children are already offset by padding.
/// </summary>
public PaddingValues ContentInset { get; set; } = PaddingValues.Zero;

/// <summary>
/// The resolved right (outer) margin in pixels. Used by auto-size calculations to include
/// a trailing child's right margin in the parent's content extent. The left/top margins are
/// already baked into <see cref="X"/>/<see cref="Y"/> by the flex strategies. Defaults to 0.
/// </summary>
public float MarginRight { get; set; }

/// <summary>
/// The resolved bottom (outer) margin in pixels. Used by auto-size calculations to include
/// a trailing child's bottom margin in the parent's content extent. The left/top margins are
/// already baked into <see cref="X"/>/<see cref="Y"/> by the flex strategies. Defaults to 0.
/// </summary>
public float MarginBottom { get; set; }

/// <summary>Right edge (X + Width).</summary>
public float Right => X + Width;

Expand Down
11 changes: 10 additions & 1 deletion src/FlexRender.Core/Layout/RowFlexLayoutStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,12 @@ internal static void LayoutRowFlex(LayoutNode node, FlexElement flex, LayoutCont
pos += m.Left.IsAuto ? spacePerAuto : m.Left.ResolvedPixels;
child.X = pos;
pos += child.Width;
pos += m.Right.IsAuto ? spacePerAuto : m.Right.ResolvedPixels;
var autoRight = m.Right.IsAuto ? spacePerAuto : m.Right.ResolvedPixels;
pos += autoRight;

// Store resolved outer right/bottom margins for trailing-margin auto-size.
child.MarginRight = Math.Max(0f, autoRight);
child.MarginBottom = Math.Max(0f, m.Bottom.IsAuto ? 0f : m.Bottom.ResolvedPixels);

// Cross axis auto margins override align-items (vertical for row)
ApplyRowCrossAxisMargins(child, m, flex, padding, crossAxisSize, hasExplicitHeight);
Expand Down Expand Up @@ -302,6 +307,10 @@ internal static void LayoutRowFlex(LayoutNode node, FlexElement flex, LayoutCont
var mTop = Math.Max(0f, m.Top.ResolvedPixels);
var mBottom = Math.Max(0f, m.Bottom.ResolvedPixels);

// Store resolved outer right/bottom margins for trailing-margin auto-size.
child.MarginRight = mRight;
child.MarginBottom = mBottom;

// Add child margin to X position
child.X = x + mLeft;

Expand Down
5 changes: 5 additions & 0 deletions src/FlexRender.Core/Layout/WrappedFlexLayoutStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,11 @@ private static void ResolveFlexForLine(LayoutNode node, FlexElement flex, Layout
var child = lineChildren[i];
var childMargin = PaddingParser.Parse(child.Element.Margin.Value, context.ContainerWidth, context.FontSize).ClampNegatives();

// Record resolved trailing margins so CalculateTotalHeight/Width (used for
// auto-sizing the container) accounts for them, matching the non-wrap strategies.
child.MarginRight = childMargin.Right;
child.MarginBottom = childMargin.Bottom;

if (isColumn)
{
child.Y = pos + childMargin.Top;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,16 +202,22 @@ private void DrawElement(
var effectiveFontSize = node.ComputedFontSize > 0 ? node.ComputedFontSize : _baseFontSize;
var rotation = RotationHelper.ParseRotation(element.Rotate.Value);

// Content is inset by padding + border so it sits inside the box on all sides.
// The background (drawn by the caller) keeps using the full box.
var inset = node.ContentInset;
var cw = Math.Max(0f, width - inset.Horizontal);
var ch = Math.Max(0f, height - inset.Vertical);

if (RotationHelper.HasRotation(rotation))
{
DrawWithRotation(ctx, x, y, width, height, rotation, bufferCtx =>
{
DrawElementContent(bufferCtx, element, 0, 0, width, height, effectiveFontSize, imageCache);
DrawElementContent(bufferCtx, element, inset.Left, inset.Top, cw, ch, effectiveFontSize, imageCache);
});
}
else
{
DrawElementContent(ctx, element, x, y, width, height, effectiveFontSize, imageCache);
DrawElementContent(ctx, element, x + inset.Left, y + inset.Top, cw, ch, effectiveFontSize, imageCache);
}
}

Expand Down
40 changes: 24 additions & 16 deletions src/FlexRender.Skia.Render/Rendering/RenderingEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -292,31 +292,39 @@ private void DrawElement(
// Draw borders between background and content
DrawBorders(canvas, element, x, y, width, height, borderRadius, effectiveFontSize);

// Content (text, bitmaps, shapes) is inset by padding + border so it sits inside the
// box on all sides. Background and border above keep using the full box (x, y, w, h).
var inset = node.ContentInset;
var cx = x + inset.Left;
var cy = y + inset.Top;
var cw = Math.Max(0f, width - inset.Horizontal);
var ch = Math.Max(0f, height - inset.Vertical);

switch (element)
{
case TextElement text:
var bounds = new SKRect(x, y, x + width, y + height);
var bounds = new SKRect(cx, cy, cx + cw, cy + ch);
_textRenderer.DrawText(canvas, text, bounds, effectiveFontSize, renderOptions, direction, node.TextLines, node.ComputedLineHeight);
break;

case QrElement qr when _qrProvider is not null:
using (var bitmap = GetSkiaBitmap(_qrProvider, qr, (int)width, (int)height))
using (var bitmap = GetSkiaBitmap(_qrProvider, qr, (int)cw, (int)ch))
{
DrawBitmapWithRotation(canvas, bitmap, element, x, y, width, height);
DrawBitmapWithRotation(canvas, bitmap, element, cx, cy, cw, ch);
}
break;

case BarcodeElement barcode when _barcodeProvider is not null:
using (var bitmap = GetSkiaBitmap(_barcodeProvider, barcode, (int)width, (int)height))
using (var bitmap = GetSkiaBitmap(_barcodeProvider, barcode, (int)cw, (int)ch))
{
DrawBitmapWithRotation(canvas, bitmap, element, x, y, width, height);
DrawBitmapWithRotation(canvas, bitmap, element, cx, cy, cw, ch);
}
break;

case SvgElement svg when _svgProvider is not null:
using (var bitmap = GetSkiaBitmap(_svgProvider, svg, (int)width, (int)height))
using (var bitmap = GetSkiaBitmap(_svgProvider, svg, (int)cw, (int)ch))
{
DrawBitmapWithRotation(canvas, bitmap, element, x, y, width, height);
DrawBitmapWithRotation(canvas, bitmap, element, cx, cy, cw, ch);
}
break;

Expand All @@ -325,38 +333,38 @@ private void DrawElement(
image,
imageCache,
renderOptions.Antialiasing,
layoutWidth: (int)width,
layoutHeight: (int)height))
layoutWidth: (int)cw,
layoutHeight: (int)ch))
{
DrawBitmapWithRotation(canvas, bitmap, element, x, y, width, height);
DrawBitmapWithRotation(canvas, bitmap, element, cx, cy, cw, ch);
}
break;

case RectElement rect:
ShapeRenderer.DrawRect(canvas, rect, x, y, width, height, effectiveFontSize, renderOptions.Antialiasing);
ShapeRenderer.DrawRect(canvas, rect, cx, cy, cw, ch, effectiveFontSize, renderOptions.Antialiasing);
break;

case CircleElement circle:
ShapeRenderer.DrawCircle(canvas, circle, x, y, width, height, renderOptions.Antialiasing);
ShapeRenderer.DrawCircle(canvas, circle, cx, cy, cw, ch, renderOptions.Antialiasing);
break;

case EllipseElement ellipse:
ShapeRenderer.DrawEllipse(canvas, ellipse, x, y, width, height, renderOptions.Antialiasing);
ShapeRenderer.DrawEllipse(canvas, ellipse, cx, cy, cw, ch, renderOptions.Antialiasing);
break;

case DrawElement drawEl:
ShapeRenderer.DrawShapes(canvas, drawEl, x, y, width, height, renderOptions.Antialiasing);
ShapeRenderer.DrawShapes(canvas, drawEl, cx, cy, cw, ch, renderOptions.Antialiasing);
break;

case ChartElement chart:
ChartRenderer.Draw(
canvas, chart, x, y, width, height,
canvas, chart, cx, cy, cw, ch,
_fontManager?.GetTypeface("main"),
renderOptions.Antialiasing);
break;

case SeparatorElement separator:
DrawSeparator(canvas, separator, x, y, width, height, renderOptions.Antialiasing);
DrawSeparator(canvas, separator, cx, cy, cw, ch, renderOptions.Antialiasing);
break;

case FlexElement:
Expand Down
Loading