defmodule Pdf.Component.StepIndicator do
@moduledoc """
Step indicator component for PDF documents.
Renders numbered steps with a connecting line, showing progress
through a multi-step process (wizard-style).
## Examples
doc |> Pdf.Component.StepIndicator.render({50, 700}, %{width: 450}, [
%{label: "Account", status: :completed},
%{label: "Profile", status: :active},
%{label: "Review", status: :pending},
%{label: "Done", status: :pending}
])
"""
@default_font "Helvetica"
@default_step_size 24
@default_completed_color {0.2, 0.7, 0.3}
@default_active_color {0.2, 0.5, 0.9}
@default_pending_color {0.75, 0.75, 0.75}
@default_line_color {0.85, 0.85, 0.85}
@doc """
Render a step indicator at `{x, y}`.
## Style options
- `:width` — total width (default `400`)
- `:step_size` — circle diameter (default `24`)
- `:completed_color` — completed step color
- `:active_color` — active step color
- `:pending_color` — pending step color
- `:font` — font name
## Steps format
List of maps: `%{label: "Step name", status: :completed | :active | :pending}`
"""
def render(doc, {x, y}, style \\ %{}, steps) do
width = Map.get(style, :width, 400)
step_size = Map.get(style, :step_size, @default_step_size)
completed = Map.get(style, :completed_color, @default_completed_color)
active = Map.get(style, :active_color, @default_active_color)
pending = Map.get(style, :pending_color, @default_pending_color)
line_color = Map.get(style, :line_color, @default_line_color)
font = Map.get(style, :font, @default_font)
count = length(steps)
spacing = if count > 1, do: (width - step_size) / (count - 1), else: 0
r = step_size / 2
center_y = y - r
# Connecting line
doc =
if count > 1 do
doc
|> Pdf.save_state()
|> Pdf.set_stroke_color(line_color)
|> Pdf.set_line_width(2)
|> Pdf.line({x + r, center_y}, {x + r + spacing * (count - 1), center_y})
|> Pdf.stroke()
|> Pdf.restore_state()
else
doc
end
# Steps
steps
|> Enum.with_index()
|> Enum.reduce(doc, fn {step, i}, d ->
cx = x + r + i * spacing
status = Map.get(step, :status, :pending)
label = Map.get(step, :label, "")
color = case status do
:completed -> completed
:active -> active
_ -> pending
end
# Circle background
d =
d
|> Pdf.save_state()
|> Pdf.set_fill_color(color)
|> Pdf.rounded_rectangle({cx - r, center_y - r}, {step_size, step_size}, r)
|> Pdf.fill()
|> Pdf.restore_state()
# Number or checkmark
text = if status == :completed, do: "ok", else: "#{i + 1}"
text_x = cx - String.length(text) * 3.5
d =
d
|> Pdf.set_font(font, 10, bold: true)
|> Pdf.set_fill_color({1.0, 1.0, 1.0})
|> Pdf.text_at({text_x, center_y - 4}, text)
# Label below
if label != "" do
label_w = String.length(label) * 5
d
|> Pdf.set_font(font, 8)
|> Pdf.set_fill_color(color)
|> Pdf.text_at({cx - label_w / 2, center_y - r - 14}, label)
else
d
end
end)
end
end