defmodule Pdf.Component.Chip do @moduledoc """ Chip component for PDF documents. Renders a compact rounded label/tag element, useful for displaying categories, tags, status indicators, or filter selections. Inspired by Material UI's Chip component. ## Examples # Simple chip doc |> Pdf.Component.Chip.render({50, 400}, %{label: "Elixir"}) # Outlined chip doc |> Pdf.Component.Chip.render({50, 400}, %{ label: "Active", variant: :outlined, color: {0.18, 0.72, 0.45} }) # Filled chip with custom colors doc |> Pdf.Component.Chip.render({50, 400}, %{ label: "Priority", background: {0.85, 0.26, 0.33}, color: :white }) """ @default_height 24 @default_background {0.92, 0.92, 0.92} @default_color {0.2, 0.2, 0.2} @default_font "Helvetica" @default_font_size 10 @default_padding_h 10 @pill :pill @doc """ Render a chip at `{x, y}` (top-left corner). Returns `{doc, width}` — the document and the rendered chip width. ## Style options - `:label` — text to display (required) - `:variant` — `:filled` (default) or `:outlined` - `:background` — fill color for filled variant (default light gray) - `:color` — text/border color (default dark gray) - `:font` — font name (default `"Helvetica"`) - `:font_size` — font size (default `10`) - `:height` — chip height in points (default `24`) - `:padding_h` — horizontal padding (default `10`) - `:border` — border width for outlined variant (default `1`) - `:opacity` — overall opacity 0.0–1.0 (default `1.0`, fully opaque) - `:background_opacity` — fill opacity only (default inherits `:opacity`) - `:text_opacity` — label opacity only (default inherits `:opacity`) - `:border_radius` — corner radius in points, or `:pill` for fully rounded (default `:pill`) """ def render(doc, {x, y}, style \\ %{}) do label = Map.get(style, :label, "") variant = Map.get(style, :variant, :filled) 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) height = Map.get(style, :height, @default_height) padding_h = Map.get(style, :padding_h, @default_padding_h) border_w = Map.get(style, :border, 1) base_opacity = Map.get(style, :opacity, 1.0) bg_opacity = Map.get(style, :background_opacity, base_opacity) text_opacity = Map.get(style, :text_opacity, 1.0) border_radius = Map.get(style, :border_radius, @pill) # Measure text width using real font metrics doc = Pdf.set_font(doc, font, font_size) font_module = doc.current.current_font.module text_width = Pdf.Font.text_width(font_module, label, font_size) chip_width = text_width + padding_h * 2 radius = if border_radius == @pill, do: height / 2, else: border_radius # Position: {x, y} is top-left, PDF y is bottom-left bx = x by = y - height doc = Pdf.save_state(doc) doc = case variant do :outlined -> doc |> Pdf.set_stroke_opacity(bg_opacity) |> Pdf.set_stroke_color(color) |> Pdf.set_line_width(border_w) |> Pdf.rounded_rectangle({bx, by}, {chip_width, height}, radius) |> Pdf.stroke() _filled -> doc |> Pdf.set_fill_opacity(bg_opacity) |> Pdf.set_fill_color(bg) |> Pdf.rounded_rectangle({bx, by}, {chip_width, height}, radius) |> Pdf.fill() end # Center text vertically tx = bx + padding_h ty = by + (height - font_size) / 2 + font_size * 0.15 doc = doc |> Pdf.set_fill_opacity(text_opacity) |> Pdf.text_at({tx, ty}, label, color: color) |> Pdf.restore_state() {doc, chip_width} end end