Packages

Reusable PDF components — Avatar, Badge, Card, Chip, Divider, Progress, Builder, StyledTable.

Current section

Files

Jump to
ex_pdf_components lib pdf layout stack.ex
Raw

lib/pdf/layout/stack.ex

defmodule Pdf.Layout.Stack do
@moduledoc """
Vertical stack layout — children flow top-to-bottom without individual `y` positions.
Like HTML block elements: only the stack origin uses `position`, children stack
automatically with wrap.
"""
alias Pdf.Layout.Flow
@doc """
Measure total height of stacked children in points.
"""
@spec measure(list, number, map, any) :: number
def measure(children, inner_width, style \\ %{}, doc \\ nil) do
gap = Map.get(style, :gap, 0)
{x, _} = Map.get(style, :position, {0, 0})
wrap_w = wrap_width(inner_width, x, style)
children
|> List.flatten()
|> Enum.with_index()
|> Enum.reduce(0, fn {child, index}, total ->
height = child_height(child, wrap_w, doc)
spacing = if index == 0, do: 0, else: gap
total + spacing + height
end)
end
@doc """
Render stacked children starting at absolute `{x, y}` on the document.
"""
@spec render(any(), {number(), number()}, map(), list(), (any(), any(), map() -> any())) :: any()
def render(doc, {x, y}, style, children, render_item) when is_function(render_item, 3) do
gap = Map.get(style, :gap, 0)
inner_w = Map.get(style, :inner_width, 300)
{ox, _} = Map.get(style, :position, {0, 0})
wrap_w = wrap_width(inner_w, ox, style)
{doc, _y} =
children
|> List.flatten()
|> Enum.reduce({doc, y}, fn child, {doc, current_y} ->
height = child_height(child, wrap_w, doc)
doc =
render_item.(doc, child, %{
x: x,
y: current_y,
width: wrap_w,
break_text: true
})
{doc, current_y - height - gap}
end)
doc
end
@doc """
Bottom edge relative to stack origin top (position y is negative down from box top).
"""
@spec extent(map, list, number, any) :: number
def extent(style, children, inner_width, doc \\ nil) do
{_, top} = Map.get(style, :position, {0, 0})
abs(top) + measure(children, inner_width, style, doc)
end
defp wrap_width(inner_width, x, style) do
case Map.get(style, :width) do
nil -> text_offset_width(inner_width, x || 0)
w -> Pdf.Dimension.resolve(w, inner_width)
end
end
defp text_offset_width(inner_width, x) do
x_off = if is_number(x), do: abs(trunc(x)), else: 0
max(inner_width - x_off, 1)
end
defp child_height(%{type: :text, props: props}, wrap_w, doc) do
style = Map.get(props, :style, %{})
Flow.measure_text(Map.get(props, :content, ""), style, wrap_w, doc)
end
defp child_height(%{text: text} = child, wrap_w, doc) do
style = Map.drop(child, [:text])
Flow.measure_text(text, style, wrap_w, doc)
end
defp child_height(_, _wrap_w, _doc), do: 0
end