1use freya_core::prelude::*;
2use torin::{
3 gaps::Gaps,
4 size::Size,
5};
6
7use crate::{
8 define_theme,
9 get_theme,
10 icons::tick::TickIcon,
11};
12
13define_theme! {
14 %[component]
15 pub Chip {
16 %[fields]
17 background: Color,
18 hover_background: Color,
19 selected_background: Color,
20 border_fill: Color,
21 selected_border_fill: Color,
22 hover_border_fill: Color,
23 focus_border_fill: Color,
24 margin: f32,
25 corner_radius: CornerRadius,
26 width: Size,
27 height: Size,
28 padding: Gaps,
29 color: Color,
30 hover_color: Color,
31 selected_color: Color,
32 selected_icon_fill: Color,
33 hover_icon_fill: Color,
34 }
35}
36
37#[derive(Debug, Default, PartialEq, Clone, Copy)]
38pub enum ChipStatus {
39 #[default]
41 Idle,
42 Hovering,
44}
45
46#[cfg_attr(feature = "docs",
65 doc = embed_doc_image::embed_image!("chip", "images/gallery_chip.png"),
66)]
67#[derive(Clone, PartialEq)]
68pub struct Chip {
69 pub(crate) theme: Option<ChipThemePartial>,
70 children: Vec<Element>,
71 on_press: Option<EventHandler<Event<PressEventData>>>,
72 selected: bool,
73 enabled: bool,
74 cursor_icon: CursorIcon,
75 key: DiffKey,
76}
77
78impl Default for Chip {
79 fn default() -> Self {
80 Self {
81 theme: None,
82 children: Vec::new(),
83 on_press: None,
84 selected: false,
85 enabled: true,
86 cursor_icon: CursorIcon::default(),
87 key: DiffKey::None,
88 }
89 }
90}
91
92impl Chip {
93 pub fn new() -> Self {
94 Self::default()
95 }
96
97 pub fn get_theme(&self) -> Option<&ChipThemePartial> {
99 self.theme.as_ref()
100 }
101
102 pub fn theme(mut self, theme: ChipThemePartial) -> Self {
104 self.theme = Some(theme);
105 self
106 }
107
108 pub fn selected(mut self, selected: impl Into<bool>) -> Self {
109 self.selected = selected.into();
110 self
111 }
112
113 pub fn enabled(mut self, enabled: impl Into<bool>) -> Self {
114 self.enabled = enabled.into();
115 self
116 }
117
118 pub fn on_press(mut self, handler: impl Into<EventHandler<Event<PressEventData>>>) -> Self {
119 self.on_press = Some(handler.into());
120 self
121 }
122
123 pub fn cursor_icon(mut self, cursor_icon: impl Into<CursorIcon>) -> Self {
125 self.cursor_icon = cursor_icon.into();
126 self
127 }
128}
129
130impl ChildrenExt for Chip {
131 fn get_children(&mut self) -> &mut Vec<Element> {
132 &mut self.children
133 }
134}
135
136impl KeyExt for Chip {
137 fn write_key(&mut self) -> &mut DiffKey {
138 &mut self.key
139 }
140}
141
142impl Component for Chip {
143 fn render(&self) -> impl IntoElement {
144 let theme = get_theme!(&self.theme, ChipThemePreference, "chip");
145 let mut status = use_state(|| ChipStatus::Idle);
146 let a11y_id = use_a11y();
147 let focus = use_focus(a11y_id);
148
149 let ChipTheme {
150 background,
151 hover_background,
152 selected_background,
153 border_fill,
154 selected_border_fill,
155 hover_border_fill,
156 focus_border_fill,
157 padding,
158 margin,
159 corner_radius,
160 width,
161 height,
162 color,
163 hover_color,
164 selected_color,
165 hover_icon_fill,
166 selected_icon_fill,
167 } = theme;
168
169 let enabled = use_reactive(&self.enabled);
170 let cursor_icon = self.cursor_icon;
171 use_drop(move || {
172 if status() == ChipStatus::Hovering && enabled() {
173 Cursor::set(CursorIcon::default());
174 }
175 });
176
177 let on_press = self.on_press.clone();
178 let on_press = move |e: Event<PressEventData>| {
179 a11y_id.request_focus();
180 if let Some(on_press) = &on_press {
181 on_press.call(e);
182 }
183 };
184
185 let on_pointer_enter = move |_| {
186 status.set(ChipStatus::Hovering);
187 if enabled() {
188 Cursor::set(cursor_icon);
189 } else {
190 Cursor::set(CursorIcon::NotAllowed);
191 }
192 };
193
194 let on_pointer_leave = move |_| {
195 if status() == ChipStatus::Hovering {
196 Cursor::set(CursorIcon::default());
197 status.set(ChipStatus::Idle);
198 }
199 };
200
201 let background = match status() {
202 ChipStatus::Hovering if enabled() => hover_background,
203 _ if self.selected => selected_background,
204 _ => background,
205 };
206 let color = match status() {
207 ChipStatus::Hovering if enabled() => hover_color,
208 _ if self.selected => selected_color,
209 _ => color,
210 };
211 let border_fill = match status() {
212 ChipStatus::Hovering if enabled() => hover_border_fill,
213 _ if self.selected => selected_border_fill,
214 _ => border_fill,
215 };
216 let icon_fill = match status() {
217 ChipStatus::Hovering if self.selected && enabled() => Some(hover_icon_fill),
218 _ if self.selected => Some(selected_icon_fill),
219 _ => None,
220 };
221 let border = if self.enabled && focus() == Focus::Keyboard {
222 Border::new()
223 .fill(focus_border_fill)
224 .width(2.)
225 .alignment(BorderAlignment::Inner)
226 } else {
227 Border::new()
228 .fill(border_fill.mul_if(!self.enabled, 0.9))
229 .width(1.)
230 .alignment(BorderAlignment::Inner)
231 };
232
233 rect()
234 .a11y_id(a11y_id)
235 .a11y_focusable(self.enabled)
236 .a11y_role(AccessibilityRole::Button)
237 .maybe(self.enabled, |rect| rect.on_press(on_press))
238 .on_pointer_enter(on_pointer_enter)
239 .on_pointer_leave(on_pointer_leave)
240 .width(width)
241 .height(height)
242 .padding(padding)
243 .margin(margin)
244 .overflow(Overflow::Clip)
245 .border(border)
246 .corner_radius(corner_radius)
247 .color(color.mul_if(!self.enabled, 0.9))
248 .background(background.mul_if(!self.enabled, 0.9))
249 .center()
250 .horizontal()
251 .spacing(4.)
252 .maybe_child(icon_fill.map(|icon_fill| {
253 TickIcon::new()
254 .fill(icon_fill)
255 .width(Size::px(12.))
256 .height(Size::px(12.))
257 }))
258 .children(self.children.clone())
259 }
260
261 fn render_key(&self) -> DiffKey {
262 self.key.clone().or(self.default_key())
263 }
264}