Skip to main content

freya_components/
chip.rs

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 state.
43    #[default]
44    Idle,
45    /// Mouse is hovering the chip.
46    Hovering,
47}
48
49// TODO: Add layout and style variants
50// TODO: Ability to hide/customize icon
51///
52/// Chip component.
53///
54/// ```rust
55/// # use freya::prelude::*;
56/// fn app() -> impl IntoElement {
57///     Chip::new().child("Chip")
58/// }
59/// ```
60///
61/// See the [interactive components demo](https://freyaui.dev/demo).
62#[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    /// Get the theme override for this component.
93    pub fn get_theme(&self) -> Option<&ChipThemePartial> {
94        self.theme.as_ref()
95    }
96
97    /// Set a theme override for this component.
98    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    /// Override the cursor icon shown when hovering over this component while enabled.
119    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}