Packages

Native desktop GUI framework for Elixir, powered by Iced

Current section

Files

Jump to
plushie examples widgets star_rating.ex
Raw

examples/widgets/star_rating.ex

defmodule StarRating do
@moduledoc """
Canvas-based star rating widget.
Renders 5 stars as a radio group. Interactive by default (click to
rate, hover to preview, Tab/arrow to navigate, Enter/Space to select).
Pass `readonly: true` for a display-only version.
# Interactive (full size)
StarRating.render("my-rating", model.rating,
hover: model.hover_star, theme_progress: p)
# Read-only (small, for review display)
StarRating.render("review-stars", 4, readonly: true, scale: 0.5)
Events:
- `canvas_element_click` with element_id `"star-0"` through `"star-4"`
- `canvas_element_enter`/`canvas_element_leave` for hover
- `canvas_element_focused` with element_id for keyboard focus
"""
import Plushie.Canvas.Shape, only: [move_to: 2, line_to: 2, close: 0]
@doc "Renders a star rating canvas widget."
def render(id, rating, opts \\ []) do
import Plushie.UI
hover = Keyword.get(opts, :hover)
theme_progress = Keyword.get(opts, :theme_progress, 0.0)
readonly = Keyword.get(opts, :readonly, false)
scale = Keyword.get(opts, :scale, 1.0)
outer_r = 13 * scale
inner_r = 5 * scale
size = round(30 * scale)
gap = round(2 * scale)
display = hover || rating
width = 5 * size + 4 * gap
commands = star_commands(outer_r, inner_r)
if readonly do
canvas id,
width: width,
height: size,
alt: "#{rating} out of 5 stars" do
layer "stars" do
for i <- 0..4 do
group x: i * (size + gap) + size / 2, y: size / 2 do
path(commands, fill: star_color(i < rating, false, theme_progress))
end
end
end
end
else
canvas id,
width: width,
height: size,
alt: "Star rating",
role: "radiogroup" do
layer "stars" do
for i <- 0..4 do
filled = i < display
preview = hover != nil and i < hover and i >= rating
group "star-#{i}",
x: i * (size + gap) + size / 2,
y: size / 2,
on_click: true,
on_hover: true,
cursor: "pointer",
focus_style: %{stroke: %{color: "#3b82f6", width: 2 * scale}},
show_focus_ring: false,
a11y: %{
role: :radio,
label: "#{i + 1} star#{if i == 0, do: "", else: "s"}",
selected: rating >= i + 1,
position_in_set: i + 1,
size_of_set: 5
} do
path(commands, fill: star_color(filled, preview, theme_progress))
end
end
end
end
end
end
defp star_commands(outer_r, inner_r) do
points =
for i <- 0..9 do
angle = i * :math.pi() / 5 - :math.pi() / 2
r = if rem(i, 2) == 0, do: outer_r, else: inner_r
{r * :math.cos(angle), r * :math.sin(angle)}
end
[{fx, fy} | rest] = points
[move_to(fx, fy) | Enum.map(rest, fn {x, y} -> line_to(x, y) end)] ++ [close()]
end
defp star_color(true, false, _progress), do: "#f59e0b"
defp star_color(_, true, _progress), do: "#fcd34d"
defp star_color(false, false, progress) do
r = round(209 + (74 - 209) * progress)
g = round(213 + (74 - 213) * progress)
b = round(219 + (94 - 219) * progress)
"#" <> hex(r) <> hex(g) <> hex(b)
end
defp hex(n), do: n |> Integer.to_string(16) |> String.pad_leading(2, "0")
end