dioxus_icon_component/lib.rs
1// Copyright (c) 2026 Xu Shaohua <shaohua@biofan.org>. All rights reserved.
2// Use of this source is governed by Apache-2.0 License
3// that can be found in the LICENSE file.
4
5use dioxus::prelude::*;
6
7/// This trait is used to override `IconProps`.
8///
9/// Implements this trait when adding a new real icon.
10pub trait IconShape: Clone + PartialEq + 'static {
11 /// Returns the SVG child elements (paths, circles, etc.) that define the icon shape.
12 ///
13 /// # Errors
14 ///
15 /// Returns `Err` if rendering the child elements fails.
16 fn child_elements(&self) -> Element;
17
18 /// Default title text for the SVG element.
19 const TITLE: Option<&'static str> = None;
20
21 /// Default width of the SVG element in pixels.
22 const WIDTH: Option<&'static str> = None;
23
24 /// Default height of the SVG element in pixels.
25 const HEIGHT: Option<&'static str> = None;
26
27 /// Default fill color of the SVG element.
28 const FILL: Option<&'static str> = Some("currentColor");
29
30 /// Default stroke color of the SVG element.
31 const STROKE: Option<&'static str> = None;
32
33 /// Default stroke width of the SVG element (e.g., "2").
34 const STROKE_WIDTH: Option<&'static str> = None;
35
36 /// Default stroke line cap style (e.g., "round", "butt", "square").
37 const STROKE_LINE_CAP: Option<&'static str> = None;
38
39 /// Default stroke line join style (e.g., "round", "miter", "bevel").
40 const STROKE_LINE_JOIN: Option<&'static str> = None;
41
42 /// Default view box string (e.g., "0 0 24 24").
43 const VIEW_BOX: Option<&'static str> = None;
44
45 /// Default XML namespace for the SVG element.
46 /// Falls back to `"http://www.w3.org/2000/svg"` if not set.
47 const XMLNS: Option<&'static str> = Some("http://www.w3.org/2000/svg");
48}
49
50/// Props for the `Icon` component.
51///
52/// All fields are optional except `icon`. When a field is not provided,
53/// the default value from the associated `IconShape` implementation is used.
54#[derive(Clone, PartialEq, Props)]
55pub struct IconProps<T: IconShape> {
56 /// The icon shape implementation that provides SVG child elements.
57 pub icon: T,
58
59 /// Optional title text rendered as a `<title>` element inside the SVG for accessibility.
60 #[props(default = None)]
61 pub title: Option<&'static str>,
62
63 /// Default width of the SVG element in pixels.
64 #[props(default = None)]
65 pub width: Option<&'static str>,
66
67 /// Default height of the SVG element in pixels.
68 #[props(default = None)]
69 pub height: Option<&'static str>,
70
71 /// Default fill color of the SVG element.
72 #[props(default = None)]
73 pub fill: Option<&'static str>,
74
75 /// Default stroke color of the SVG element.
76 #[props(default = None)]
77 pub stroke: Option<&'static str>,
78
79 /// Default stroke width of the SVG element (e.g., "2").
80 #[props(default = None)]
81 pub stroke_width: Option<&'static str>,
82
83 /// Default stroke line cap style (e.g., "round", "butt", "square").
84 #[props(default = None)]
85 pub stroke_line_cap: Option<&'static str>,
86
87 /// Default stroke line join style (e.g., "round", "miter", "bevel").
88 #[props(default = None)]
89 pub stroke_line_join: Option<&'static str>,
90
91 /// Additional HTML/SVG global attributes to spread onto the `<svg>` element.
92 /// Allows passing standard attributes like `id`, `aria-*`, event handlers, etc.
93 #[props(extends = GlobalAttributes)]
94 pub attributes: Vec<Attribute>,
95}
96
97/// # Errors
98///
99/// Returns `Err` if rendering the icon fails.
100#[allow(non_snake_case)]
101pub fn Icon<T: IconShape>(props: IconProps<T>) -> Element {
102 rsx! {
103 svg {
104 width: if props.width.is_some() {
105 props.width
106 } else {
107 T::WIDTH
108 },
109 height: if props.height.is_some() {
110 props.height
111 } else {
112 T::HEIGHT
113 },
114 fill: if props.fill.is_some() {
115 props.fill
116 } else {
117 T::FILL
118 },
119 stroke: if props.stroke.is_some() {
120 props.stroke
121 } else {
122 T::STROKE
123 },
124 stroke_width: if props.stroke_width.is_some() {
125 props.stroke_width
126 } else {
127 T::STROKE_WIDTH
128 },
129 stroke_linecap: if props.stroke_line_cap.is_some() {
130 props.stroke_line_cap
131 } else {
132 T::STROKE_LINE_CAP
133 },
134 stroke_linejoin: if props.stroke_line_join.is_some() {
135 props.stroke_line_join
136 } else {
137 T::STROKE_LINE_JOIN
138 },
139 view_box: T::VIEW_BOX,
140 xmlns: T::XMLNS,
141
142 ..props.attributes,
143
144 if let Some(title_text) = props.title {
145 title {{ title_text }}
146 } else if let Some(title_text) = T::TITLE {
147 title {{ title_text }}
148 }
149
150 {props.icon.child_elements()}
151 }
152 }
153}