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::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 state.
40    #[default]
41    Idle,
42    /// Mouse is hovering the chip.
43    Hovering,
44}
45
46// TODO: Add layout and style variants
47// TODO: Ability to hide/customize icon
48///
49/// Chip component.
50///
51/// ```rust
52/// # use freya::prelude::*;
53/// fn app() -> impl IntoElement {
54///     Chip::new().child("Chip")
55/// }
56/// # use freya_testing::prelude::*;
57/// # launch_doc(|| {
58/// #   rect().center().expanded().child(app())
59/// # }, "./images/gallery_chip.png").render();
60/// ```
61///
62/// # Preview
63/// ![Chip Preview][chip]
64#[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    key: DiffKey,
75}
76
77impl Default for Chip {
78    fn default() -> Self {
79        Self {
80            theme: None,
81            children: Vec::new(),
82            on_press: None,
83            selected: false,
84            enabled: true,
85            key: DiffKey::None,
86        }
87    }
88}
89
90impl Chip {
91    pub fn new() -> Self {
92        Self::default()
93    }
94
95    /// Get the theme override for this component.
96    pub fn get_theme(&self) -> Option<&ChipThemePartial> {
97        self.theme.as_ref()
98    }
99
100    /// Set a theme override for this component.
101    pub fn theme(mut self, theme: ChipThemePartial) -> Self {
102        self.theme = Some(theme);
103        self
104    }
105
106    pub fn selected(mut self, selected: impl Into<bool>) -> Self {
107        self.selected = selected.into();
108        self
109    }
110
111    pub fn enabled(mut self, enabled: impl Into<bool>) -> Self {
112        self.enabled = enabled.into();
113        self
114    }
115
116    pub fn on_press(mut self, handler: impl Into<EventHandler<Event<PressEventData>>>) -> Self {
117        self.on_press = Some(handler.into());
118        self
119    }
120}
121
122impl ChildrenExt for Chip {
123    fn get_children(&mut self) -> &mut Vec<Element> {
124        &mut self.children
125    }
126}
127
128impl KeyExt for Chip {
129    fn write_key(&mut self) -> &mut DiffKey {
130        &mut self.key
131    }
132}
133
134impl Component for Chip {
135    fn render(&self) -> impl IntoElement {
136        let theme = get_theme!(&self.theme, ChipThemePreference, "chip");
137        let mut status = use_state(|| ChipStatus::Idle);
138        let focus = use_focus();
139        let focus_status = use_focus_status(focus);
140
141        let ChipTheme {
142            background,
143            hover_background,
144            selected_background,
145            border_fill,
146            selected_border_fill,
147            hover_border_fill,
148            focus_border_fill,
149            padding,
150            margin,
151            corner_radius,
152            width,
153            height,
154            color,
155            hover_color,
156            selected_color,
157            hover_icon_fill,
158            selected_icon_fill,
159        } = theme;
160
161        let enabled = use_reactive(&self.enabled);
162        use_drop(move || {
163            if status() == ChipStatus::Hovering && enabled() {
164                Cursor::set(CursorIcon::default());
165            }
166        });
167
168        let on_press = self.on_press.clone();
169        let on_press = move |e: Event<PressEventData>| {
170            focus.request_focus();
171            if let Some(on_press) = &on_press {
172                on_press.call(e);
173            }
174        };
175
176        let on_pointer_enter = move |_| {
177            status.set(ChipStatus::Hovering);
178            if enabled() {
179                Cursor::set(CursorIcon::Pointer);
180            } else {
181                Cursor::set(CursorIcon::NotAllowed);
182            }
183        };
184
185        let on_pointer_leave = move |_| {
186            if status() == ChipStatus::Hovering {
187                Cursor::set(CursorIcon::default());
188                status.set(ChipStatus::Idle);
189            }
190        };
191
192        let background = match status() {
193            ChipStatus::Hovering if enabled() => hover_background,
194            _ if self.selected => selected_background,
195            _ => background,
196        };
197        let color = match status() {
198            ChipStatus::Hovering if enabled() => hover_color,
199            _ if self.selected => selected_color,
200            _ => color,
201        };
202        let border_fill = match status() {
203            ChipStatus::Hovering if enabled() => hover_border_fill,
204            _ if self.selected => selected_border_fill,
205            _ => border_fill,
206        };
207        let icon_fill = match status() {
208            ChipStatus::Hovering if self.selected && enabled() => Some(hover_icon_fill),
209            _ if self.selected => Some(selected_icon_fill),
210            _ => None,
211        };
212        let border = if self.enabled && focus_status() == FocusStatus::Keyboard {
213            Border::new()
214                .fill(focus_border_fill)
215                .width(2.)
216                .alignment(BorderAlignment::Inner)
217        } else {
218            Border::new()
219                .fill(border_fill.mul_if(!self.enabled, 0.9))
220                .width(1.)
221                .alignment(BorderAlignment::Inner)
222        };
223
224        rect()
225            .a11y_id(focus.a11y_id())
226            .a11y_focusable(self.enabled)
227            .a11y_role(AccessibilityRole::Button)
228            .maybe(self.enabled, |rect| rect.on_press(on_press))
229            .on_pointer_enter(on_pointer_enter)
230            .on_pointer_leave(on_pointer_leave)
231            .width(width)
232            .height(height)
233            .padding(padding)
234            .margin(margin)
235            .overflow(Overflow::Clip)
236            .border(border)
237            .corner_radius(corner_radius)
238            .color(color.mul_if(!self.enabled, 0.9))
239            .background(background.mul_if(!self.enabled, 0.9))
240            .center()
241            .horizontal()
242            .spacing(4.)
243            .maybe_child(icon_fill.map(|icon_fill| {
244                TickIcon::new()
245                    .fill(icon_fill)
246                    .width(Size::px(12.))
247                    .height(Size::px(12.))
248            }))
249            .children(self.children.clone())
250    }
251
252    fn render_key(&self) -> DiffKey {
253        self.key.clone().or(self.default_key())
254    }
255}