defmodule Pdf.Component.Rating do
@moduledoc """
Rating component for PDF documents.
Renders a star/score rating display with filled and empty indicators.
## Examples
doc |> Pdf.Component.Rating.render({50, 700}, %{value: 4, max: 5})
doc |> Pdf.Component.Rating.render({50, 700}, %{
value: 3.5,
max: 5,
filled_color: {0.95, 0.7, 0.0},
size: 16
})
"""
@default_size 14
@default_max 5
@default_filled_color {0.95, 0.7, 0.0}
@default_empty_color {0.82, 0.82, 0.82}
@default_font "Helvetica"
@default_gap 4
@default_label_color {0.3, 0.3, 0.3}
@doc """
Render a rating at `{x, y}`.
## Style options
- `:value` — current score (default `0`)
- `:max` — maximum score (default `5`)
- `:size` — star size (default `14`)
- `:filled_color` — filled star color (default gold)
- `:empty_color` — empty star color (default light gray)
- `:gap` — space between stars (default `4`)
- `:show_label` — show "3.5/5" text (default `false`)
- `:label_color` — label text color
"""
def render(doc, {x, y}, style \\ %{}) do
value = Map.get(style, :value, 0)
max = Map.get(style, :max, @default_max)
size = Map.get(style, :size, @default_size)
filled = Map.get(style, :filled_color, @default_filled_color)
empty = Map.get(style, :empty_color, @default_empty_color)
gap = Map.get(style, :gap, @default_gap)
font = Map.get(style, :font, @default_font)
show_label = Map.get(style, :show_label, false)
label_color = Map.get(style, :label_color, @default_label_color)
r = size / 2
doc =
Enum.reduce(0..(max - 1), doc, fn i, d ->
sx = x + i * (size + gap)
color = if i < trunc(value), do: filled, else: empty
d
|> Pdf.save_state()
|> Pdf.set_fill_color(color)
|> Pdf.rounded_rectangle({sx, y - size}, {size, size}, r)
|> Pdf.fill()
|> Pdf.restore_state()
end)
# Half star: overlay a partial fill if value has decimal
frac = value - trunc(value)
doc =
if frac > 0 do
half_i = trunc(value)
sx = x + half_i * (size + gap)
half_w = size * frac
doc
|> Pdf.save_state()
|> Pdf.set_fill_color(filled)
|> Pdf.rectangle({sx, y - size}, {half_w, size})
|> Pdf.fill()
|> Pdf.restore_state()
else
doc
end
if show_label do
label_x = x + max * (size + gap) + 4
label = if frac > 0, do: "#{value}/#{max}", else: "#{trunc(value)}/#{max}"
doc
|> Pdf.set_font(font, size - 2)
|> Pdf.set_fill_color(label_color)
|> Pdf.text_at({label_x, y - size + 2}, label)
else
doc
end
end
end