defmodule Pdf.Component.Badge do @moduledoc """ Badge component for PDF documents. Renders a small circle or pill-shaped label, typically used to show counts, notifications, or status indicators overlaid on another element. Inspired by Material UI's Badge component. ## Examples # Simple notification badge doc |> Pdf.Component.Badge.render({120, 710}, %{content: "3"}) # Custom styled badge doc |> Pdf.Component.Badge.render({200, 700}, %{ content: "NEW", background: {0.18, 0.72, 0.45}, color: :white, variant: :pill }) """ @default_background {0.85, 0.26, 0.33} @default_color {1.0, 1.0, 1.0} @default_font "Helvetica" @default_font_size 8 @default_size 18 @doc """ Render a badge at `{x, y}` (center point). ## Style options - `:content` — text to display (default `""`) - `:background` — fill color (default red) - `:color` — text color (default white) - `:font` — font name (default `"Helvetica"`) - `:font_size` — font size (default `8`) - `:size` — diameter for dot/circle variant (default `18`) - `:variant` — `:dot` (no text), `:standard` (circle), or `:pill` (auto-width) - `:border` — border width (default `0`) - `:border_color` — border color (default `:white`) """ def render(doc, {x, y}, style \\ %{}) do content = Map.get(style, :content, "") bg = Map.get(style, :background, @default_background) color = Map.get(style, :color, @default_color) font = Map.get(style, :font, @default_font) font_size = Map.get(style, :font_size, @default_font_size) size = Map.get(style, :size, @default_size) variant = Map.get(style, :variant, :standard) border_w = Map.get(style, :border, 0) border_color = Map.get(style, :border_color, :white) case variant do :dot -> draw_dot(doc, {x, y}, size / 2, bg, border_w, border_color) :pill -> draw_pill(doc, {x, y}, content, bg, color, font, font_size, size, border_w, border_color) _standard -> draw_circle(doc, {x, y}, content, bg, color, font, font_size, size, border_w, border_color) end end defp draw_dot(doc, {cx, cy}, radius, bg, border_w, border_color) do size = radius * 2 x = cx - radius y = cy - radius doc |> Pdf.save_state() |> Pdf.set_fill_color(bg) |> Pdf.rounded_rectangle({x, y}, {size, size}, radius) |> Pdf.fill() |> maybe_border({x, y}, {size, size}, radius, border_w, border_color) |> Pdf.restore_state() end defp draw_circle(doc, {cx, cy}, content, bg, color, font, font_size, size, border_w, border_color) do radius = size / 2 x = cx - radius y = cy - radius doc |> Pdf.save_state() |> Pdf.set_fill_color(bg) |> Pdf.rounded_rectangle({x, y}, {size, size}, radius) |> Pdf.fill() |> maybe_border({x, y}, {size, size}, radius, border_w, border_color) |> draw_centered_text({cx, cy}, content, color, font, font_size) |> Pdf.restore_state() end defp draw_pill(doc, {cx, cy}, content, bg, color, font, font_size, height, border_w, border_color) do radius = height / 2 text_width = String.length(content) * font_size * 0.6 pill_width = max(height, text_width + height * 0.6) x = cx - pill_width / 2 y = cy - radius doc |> Pdf.save_state() |> Pdf.set_fill_color(bg) |> Pdf.rounded_rectangle({x, y}, {pill_width, height}, radius) |> Pdf.fill() |> maybe_border({x, y}, {pill_width, height}, radius, border_w, border_color) |> draw_centered_text({cx, cy}, content, color, font, font_size) |> Pdf.restore_state() end # ── Border ───────────────────────────────────────────────────── defp maybe_border(doc, _pos, _size, _radius, 0, _color), do: doc defp maybe_border(doc, {x, y}, {w, h}, radius, width, color) do doc |> Pdf.set_stroke_color(color) |> Pdf.set_line_width(width) |> Pdf.rounded_rectangle({x, y}, {w, h}, radius) |> Pdf.stroke() end # ── Text ─────────────────────────────────────────────────────── defp draw_centered_text(doc, _center, "", _color, _font, _font_size), do: doc defp draw_centered_text(doc, {cx, cy}, text, color, font, font_size) do text_w = String.length(text) * font_size * 0.52 tx = cx - text_w / 2 ty = cy - font_size * 0.35 doc |> Pdf.set_font(font, font_size) |> Pdf.set_fill_color(color) |> Pdf.text_at({tx, ty}, text) end end