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    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    /// Get the theme override for this component.
98    pub fn get_theme(&self) -> Option<&ChipThemePartial> {
99        self.theme.as_ref()
100    }
101
102    /// Set a theme override for this component.
103    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    /// Override the cursor icon shown when hovering over this component while enabled.
124    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
171        let on_press = self.on_press.clone();
172        let on_press = move |e: Event<PressEventData>| {
173            a11y_id.request_focus();
174            if let Some(on_press) = &on_press {
175                on_press.call(e);
176            }
177        };
178
179        let on_pointer_enter = move |_| {
180            status.set(ChipStatus::Hovering);
181        };
182
183        let on_pointer_leave = move |_| {
184            if status() == ChipStatus::Hovering {
185                status.set(ChipStatus::Idle);
186            }
187        };
188
189        let background = match status() {
190            ChipStatus::Hovering if enabled() => hover_background,
191            _ if self.selected => selected_background,
192            _ => background,
193        };
194        let color = match status() {
195            ChipStatus::Hovering if enabled() => hover_color,
196            _ if self.selected => selected_color,
197            _ => color,
198        };
199        let border_fill = match status() {
200            ChipStatus::Hovering if enabled() => hover_border_fill,
201            _ if self.selected => selected_border_fill,
202            _ => border_fill,
203        };
204        let icon_fill = match status() {
205            ChipStatus::Hovering if self.selected && enabled() => Some(hover_icon_fill),
206            _ if self.selected => Some(selected_icon_fill),
207            _ => None,
208        };
209        let border = if self.enabled && focus() == Focus::Keyboard {
210            Border::new()
211                .fill(focus_border_fill)
212                .width(2.)
213                .alignment(BorderAlignment::Inner)
214        } else {
215            Border::new()
216                .fill(border_fill.mul_if(!self.enabled, 0.9))
217                .width(1.)
218                .alignment(BorderAlignment::Inner)
219        };
220
221        rect()
222            .a11y_id(a11y_id)
223            .a11y_focusable(self.enabled)
224            .a11y_role(AccessibilityRole::Button)
225            .maybe(self.enabled, |rect| rect.on_press(on_press))
226            .on_pointer_enter(on_pointer_enter)
227            .on_pointer_leave(on_pointer_leave)
228            .cursor(if self.enabled {
229                self.cursor_icon
230            } else {
231                CursorIcon::NotAllowed
232            })
233            .width(width)
234            .height(height)
235            .padding(padding)
236            .margin(margin)
237            .overflow(Overflow::Clip)
238            .border(border)
239            .corner_radius(corner_radius)
240            .color(color.mul_if(!self.enabled, 0.9))
241            .background(background.mul_if(!self.enabled, 0.9))
242            .center()
243            .horizontal()
244            .spacing(4.)
245            .maybe_child(icon_fill.map(|icon_fill| {
246                TickIcon::new()
247                    .fill(icon_fill)
248                    .width(Size::px(12.))
249                    .height(Size::px(12.))
250            }))
251            .children(self.children.clone())
252    }
253
254    fn render_key(&self) -> DiffKey {
255        self.key.clone().or(self.default_key())
256    }
257}