Skip to main content

lib/pdf/component/step_indicator.ex

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