defmodule Pdf.Component.Progress do
@moduledoc """
Progress bar component for PDF documents.
Renders a horizontal progress/percentage bar with track and fill,
optional label, and rounded or square ends.
Inspired by Material UI's LinearProgress component.
## Examples
# Simple progress bar
doc |> Pdf.Component.Progress.render({50, 400}, %{width: 200, value: 75})
# Styled progress bar with label
doc |> Pdf.Component.Progress.render({50, 400}, %{
width: 300,
value: 42,
color: {0.18, 0.72, 0.45},
show_label: true,
height: 16,
border_radius: :rounded
})
"""
@default_width 200
@default_height 8
@default_color {0.23, 0.53, 0.88}
@default_track_color {0.92, 0.92, 0.92}
@default_font "Helvetica"
@doc """
Render a progress bar at `{x, y}` (top-left corner).
## Style options
- `:width` — total bar width (default `200`)
- `:height` — bar height (default `8`)
- `:value` — progress percentage 0-100 (default `0`)
- `:color` — fill color (default blue)
- `:track_color` — background track color (default light gray)
- `:border_radius` — `:rounded` (default), `:square`, or number
- `:show_label` — show percentage text (default `false`)
- `:label_color` — text color for label (default dark gray)
- `:font` — font name (default `"Helvetica"`)
"""
def render(doc, {x, y}, style \\ %{}) do
width = Map.get(style, :width, @default_width)
height = Map.get(style, :height, @default_height)
value = Map.get(style, :value, 0) |> max(0) |> min(100)
color = Map.get(style, :color, @default_color)
track_color = Map.get(style, :track_color, @default_track_color)
border_radius = resolve_radius(Map.get(style, :border_radius, :rounded), height)
show_label = Map.get(style, :show_label, false)
label_color = Map.get(style, :label_color, {0.3, 0.3, 0.3})
font = Map.get(style, :font, @default_font)
# PDF coords: y is bottom-left
by = y - height
fill_width = width * value / 100
doc = doc
|> Pdf.save_state()
# Track (background)
|> Pdf.set_fill_color(track_color)
|> Pdf.rounded_rectangle({x, by}, {width, height}, border_radius)
|> Pdf.fill()
# Fill (progress)
doc = if fill_width > 0 do
# Clamp radius so it doesn't exceed fill width
fill_radius = min(border_radius, fill_width / 2)
doc
|> Pdf.set_fill_color(color)
|> Pdf.rounded_rectangle({x, by}, {fill_width, height}, fill_radius)
|> Pdf.fill()
else
doc
end
doc = Pdf.restore_state(doc)
# Label
if show_label do
label = "#{round(value)}%"
font_size = min(height * 0.75, 10)
label_x = x + width + 6
label_y = by + (height - font_size) / 2 + font_size * 0.15
doc
|> Pdf.save_state()
|> Pdf.set_font(font, font_size)
|> Pdf.set_fill_color(label_color)
|> Pdf.text_at({label_x, label_y}, label)
|> Pdf.restore_state()
else
doc
end
end
defp resolve_radius(:rounded, height), do: height / 2
defp resolve_radius(:square, _height), do: 0
defp resolve_radius(n, _height) when is_number(n), do: n
defp resolve_radius(_, height), do: height / 2
end