1use freya_core::prelude::*;
2use torin::{
3 gaps::Gaps,
4 size::Size,
5};
6
7use crate::{
8 define_theme,
9 get_theme,
10};
11
12define_theme! {
13 for = Button;
14 theme_field = theme_layout;
15
16 %[component]
17 pub ButtonLayout {
18 %[fields]
19 margin: Gaps,
20 corner_radius: CornerRadius,
21 width: Size,
22 height: Size,
23 padding: Gaps,
24 }
25}
26
27define_theme! {
28 for = Button;
29 theme_field = theme_colors;
30
31 %[component]
32 pub ButtonColors {
33 %[fields]
34 background: Color,
35 hover_background: Color,
36 border_fill: Color,
37 focus_border_fill: Color,
38 color: Color,
39 }
40}
41
42#[derive(Clone, PartialEq)]
43pub enum ButtonStyleVariant {
44 Normal,
45 Filled,
46 Outline,
47 Flat,
48}
49
50#[derive(Clone, PartialEq)]
51pub enum ButtonLayoutVariant {
52 Normal,
53 Compact,
54 Expanded,
55}
56
57#[derive(Clone, PartialEq)]
105pub struct Button {
106 pub(crate) theme_colors: Option<ButtonColorsThemePartial>,
107 pub(crate) theme_layout: Option<ButtonLayoutThemePartial>,
108 elements: Vec<Element>,
109 on_press: Option<EventHandler<Event<PressEventData>>>,
110 on_secondary_down: Option<EventHandler<Event<PressEventData>>>,
111 on_pointer_down: Option<EventHandler<Event<PointerEventData>>>,
112 key: DiffKey,
113 style_variant: ButtonStyleVariant,
114 layout_variant: ButtonLayoutVariant,
115 enabled: bool,
116 focusable: bool,
117 cursor_icon: Option<CursorIcon>,
118}
119
120impl Default for Button {
121 fn default() -> Self {
122 Self::new()
123 }
124}
125
126impl ChildrenExt for Button {
127 fn children_mut(&mut self) -> &mut Vec<Element> {
128 &mut self.elements
129 }
130}
131
132impl KeyExt for Button {
133 fn key_mut(&mut self) -> &mut DiffKey {
134 &mut self.key
135 }
136}
137
138impl Button {
139 pub fn new() -> Self {
140 Self {
141 theme_colors: None,
142 theme_layout: None,
143 style_variant: ButtonStyleVariant::Normal,
144 layout_variant: ButtonLayoutVariant::Normal,
145 on_press: None,
146 on_secondary_down: None,
147 on_pointer_down: None,
148 elements: Vec::default(),
149 enabled: true,
150 focusable: true,
151 cursor_icon: None,
152 key: DiffKey::None,
153 }
154 }
155
156 pub fn get_layout_variant(&self) -> &ButtonLayoutVariant {
157 &self.layout_variant
158 }
159
160 pub fn get_theme_layout(&self) -> Option<&ButtonLayoutThemePartial> {
161 self.theme_layout.as_ref()
162 }
163
164 pub fn enabled(mut self, enabled: impl Into<bool>) -> Self {
165 self.enabled = enabled.into();
166 self
167 }
168
169 pub fn focusable(mut self, focusable: impl Into<bool>) -> Self {
170 self.focusable = focusable.into();
171 self
172 }
173
174 pub fn style_variant(mut self, style_variant: impl Into<ButtonStyleVariant>) -> Self {
175 self.style_variant = style_variant.into();
176 self
177 }
178
179 pub fn layout_variant(mut self, layout_variant: impl Into<ButtonLayoutVariant>) -> Self {
180 self.layout_variant = layout_variant.into();
181 self
182 }
183
184 pub fn on_press(mut self, on_press: impl Into<EventHandler<Event<PressEventData>>>) -> Self {
185 self.on_press = Some(on_press.into());
186 self
187 }
188
189 pub fn on_secondary_down(
190 mut self,
191 on_secondary_down: impl Into<EventHandler<Event<PressEventData>>>,
192 ) -> Self {
193 self.on_secondary_down = Some(on_secondary_down.into());
194 self
195 }
196
197 pub fn on_pointer_down(
198 mut self,
199 on_pointer_down: impl Into<EventHandler<Event<PointerEventData>>>,
200 ) -> Self {
201 self.on_pointer_down = Some(on_pointer_down.into());
202 self
203 }
204
205 pub fn theme_colors(mut self, theme: ButtonColorsThemePartial) -> Self {
206 self.theme_colors = Some(theme);
207 self
208 }
209
210 pub fn theme_layout(mut self, theme: ButtonLayoutThemePartial) -> Self {
211 self.theme_layout = Some(theme);
212 self
213 }
214
215 pub fn compact(self) -> Self {
217 self.layout_variant(ButtonLayoutVariant::Compact)
218 }
219
220 pub fn expanded(self) -> Self {
222 self.layout_variant(ButtonLayoutVariant::Expanded)
223 }
224
225 pub fn filled(self) -> Self {
227 self.style_variant(ButtonStyleVariant::Filled)
228 }
229
230 pub fn outline(self) -> Self {
232 self.style_variant(ButtonStyleVariant::Outline)
233 }
234
235 pub fn flat(self) -> Self {
237 self.style_variant(ButtonStyleVariant::Flat)
238 }
239
240 pub fn cursor(mut self, cursor_icon: impl Into<Option<CursorIcon>>) -> Self {
242 self.cursor_icon = cursor_icon.into();
243 self
244 }
245}
246
247impl CornerRadiusExt for Button {
248 fn with_corner_radius(self, corner_radius: f32) -> Self {
249 self.corner_radius(corner_radius)
250 }
251}
252
253impl Component for Button {
254 fn render(&self) -> impl IntoElement {
255 let mut hovering = use_state(|| false);
256 let a11y_id = use_a11y();
257 let focus = use_focus(a11y_id);
258
259 let enabled = use_reactive(&self.enabled);
260
261 let theme_colors = match self.style_variant {
262 ButtonStyleVariant::Normal => {
263 get_theme!(&self.theme_colors, ButtonColorsThemePreference, "button")
264 }
265 ButtonStyleVariant::Outline => get_theme!(
266 &self.theme_colors,
267 ButtonColorsThemePreference,
268 "outline_button"
269 ),
270 ButtonStyleVariant::Filled => get_theme!(
271 &self.theme_colors,
272 ButtonColorsThemePreference,
273 "filled_button"
274 ),
275 ButtonStyleVariant::Flat => get_theme!(
276 &self.theme_colors,
277 ButtonColorsThemePreference,
278 "flat_button"
279 ),
280 };
281 let theme_layout = match self.layout_variant {
282 ButtonLayoutVariant::Normal => get_theme!(
283 &self.theme_layout,
284 ButtonLayoutThemePreference,
285 "button_layout"
286 ),
287 ButtonLayoutVariant::Compact => get_theme!(
288 &self.theme_layout,
289 ButtonLayoutThemePreference,
290 "compact_button_layout"
291 ),
292 ButtonLayoutVariant::Expanded => get_theme!(
293 &self.theme_layout,
294 ButtonLayoutThemePreference,
295 "expanded_button_layout"
296 ),
297 };
298
299 let border = if focus() == Focus::Keyboard {
300 Border::new()
301 .fill(theme_colors.focus_border_fill)
302 .width(2.)
303 .alignment(BorderAlignment::Inner)
304 } else {
305 Border::new()
306 .fill(theme_colors.border_fill.mul_if(!self.enabled, 0.9))
307 .width(1.)
308 .alignment(BorderAlignment::Inner)
309 };
310 let background = if enabled() && hovering() {
311 theme_colors.hover_background
312 } else {
313 theme_colors.background
314 };
315
316 rect()
317 .overflow(Overflow::Clip)
318 .a11y_id(a11y_id)
319 .a11y_enabled(self.enabled)
320 .a11y_focusable(self.enabled && self.focusable)
321 .a11y_role(AccessibilityRole::Button)
322 .background(background.mul_if(!self.enabled, 0.9))
323 .border(border)
324 .padding(theme_layout.padding)
325 .corner_radius(theme_layout.corner_radius)
326 .width(theme_layout.width)
327 .height(theme_layout.height)
328 .color(theme_colors.color.mul_if(!self.enabled, 0.9))
329 .center()
330 .maybe(self.enabled, |rect| {
331 rect.map(self.on_pointer_down.clone(), |rect, on_pointer_down| {
332 rect.on_pointer_down(move |e: Event<PointerEventData>| {
333 on_pointer_down.call(e);
334 })
335 })
336 .map(self.on_secondary_down.clone(), |el, on_secondary_down| {
337 el.on_secondary_down(move |e: Event<PressEventData>| {
338 on_secondary_down.call(e);
339 })
340 })
341 .on_all_press({
342 let on_press = self.on_press.clone();
343 move |e: Event<PressEventData>| {
344 a11y_id.request_focus();
345 match e.data() {
346 PressEventData::Mouse(data) => {
347 if data.button == Some(MouseButton::Left)
348 && let Some(handler) = &on_press
349 {
350 handler.call(e);
351 }
352 }
353 PressEventData::Touch(_)
354 | PressEventData::Keyboard(_)
355 | PressEventData::Accessibility(_) => {
356 if let Some(handler) = &on_press {
357 handler.call(e);
358 }
359 }
360 }
361 }
362 })
363 .on_pointer_over(move |_| {
364 hovering.set(true);
365 })
366 })
367 .on_pointer_out(move |_| hovering.set_if_modified(false))
368 .cursor(if self.enabled {
369 self.cursor_icon
370 } else {
371 CursorIcon::NotAllowed.into()
372 })
373 .children(self.elements.clone())
374 }
375
376 fn render_key(&self) -> DiffKey {
377 self.key.clone().or(self.default_key())
378 }
379}