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