Skip to main content

freya_components/
segmented_button.rs

1use freya_core::prelude::*;
2use torin::size::Size;
3
4use crate::{
5    get_theme,
6    icons::tick::TickIcon,
7    theming::component_themes::{
8        ButtonSegmentTheme,
9        ButtonSegmentThemePartial,
10        SegmentedButtonTheme,
11        SegmentedButtonThemePartial,
12    },
13};
14
15/// Identifies the current status of the [`ButtonSegment`]s.
16#[derive(Debug, Default, PartialEq, Clone, Copy)]
17pub enum ButtonSegmentStatus {
18    /// Default state.
19    #[default]
20    Idle,
21    /// Pointer is hovering the button.
22    Hovering,
23}
24
25/// A segment button to be used within a [`SegmentedButton`].
26///
27/// # Example
28///
29/// ```rust
30/// # use freya::prelude::*;
31/// # use std::collections::HashSet;
32/// fn app() -> impl IntoElement {
33///     let mut selected = use_state(|| HashSet::from([1]));
34///     SegmentedButton::new().children((0..2).map(|i| {
35///         ButtonSegment::new()
36///             .key(i)
37///             .selected(selected.read().contains(&i))
38///             .on_press(move |_| {
39///                 if selected.read().contains(&i) {
40///                     selected.write().remove(&i);
41///                 } else {
42///                     selected.write().insert(i);
43///                 }
44///             })
45///             .child(format!("Option {i}"))
46///             .into()
47///     }))
48/// }
49/// ```
50#[derive(Clone, PartialEq)]
51pub struct ButtonSegment {
52    pub(crate) theme: Option<ButtonSegmentThemePartial>,
53    children: Vec<Element>,
54    on_press: Option<EventHandler<Event<PressEventData>>>,
55    selected: bool,
56    enabled: bool,
57    key: DiffKey,
58}
59
60impl Default for ButtonSegment {
61    fn default() -> Self {
62        Self::new()
63    }
64}
65
66impl ButtonSegment {
67    pub fn new() -> Self {
68        Self {
69            theme: None,
70            children: Vec::new(),
71            on_press: None,
72            selected: false,
73            enabled: true,
74            key: DiffKey::None,
75        }
76    }
77
78    /// Get the theme override for this component.
79    pub fn get_theme(&self) -> Option<&ButtonSegmentThemePartial> {
80        self.theme.as_ref()
81    }
82
83    /// Set a theme override for this component.
84    pub fn theme(mut self, theme: ButtonSegmentThemePartial) -> Self {
85        self.theme = Some(theme);
86        self
87    }
88
89    /// Whether this segment is currently selected.
90    pub fn is_selected(&self) -> bool {
91        self.selected
92    }
93
94    pub fn selected(mut self, selected: impl Into<bool>) -> Self {
95        self.selected = selected.into();
96        self
97    }
98
99    pub fn enabled(mut self, enabled: impl Into<bool>) -> Self {
100        self.enabled = enabled.into();
101        self
102    }
103
104    pub fn on_press(mut self, on_press: impl Into<EventHandler<Event<PressEventData>>>) -> Self {
105        self.on_press = Some(on_press.into());
106        self
107    }
108}
109
110impl ChildrenExt for ButtonSegment {
111    fn get_children(&mut self) -> &mut Vec<Element> {
112        &mut self.children
113    }
114}
115
116impl KeyExt for ButtonSegment {
117    fn write_key(&mut self) -> &mut DiffKey {
118        &mut self.key
119    }
120}
121
122impl Component for ButtonSegment {
123    fn render(&self) -> impl IntoElement {
124        let theme = get_theme!(&self.theme, button_segment);
125        let mut status = use_state(|| ButtonSegmentStatus::Idle);
126        let focus = use_focus();
127        let focus_status = use_focus_status(focus);
128
129        let ButtonSegmentTheme {
130            background,
131            hover_background,
132            disabled_background,
133            selected_background,
134            focus_background,
135            padding,
136            selected_padding,
137            width,
138            height,
139            color,
140            selected_icon_fill,
141        } = theme;
142
143        let enabled = use_reactive(&self.enabled);
144        use_drop(move || {
145            if status() == ButtonSegmentStatus::Hovering && enabled() {
146                Cursor::set(CursorIcon::default());
147            }
148        });
149
150        let on_press = self.on_press.clone();
151        let on_press = move |e: Event<PressEventData>| {
152            focus.request_focus();
153            if let Some(on_press) = &on_press {
154                on_press.call(e);
155            }
156        };
157
158        let on_pointer_enter = move |_| {
159            status.set(ButtonSegmentStatus::Hovering);
160            if enabled() {
161                Cursor::set(CursorIcon::Pointer);
162            } else {
163                Cursor::set(CursorIcon::NotAllowed);
164            }
165        };
166
167        let on_pointer_leave = move |_| {
168            if status() == ButtonSegmentStatus::Hovering {
169                Cursor::set(CursorIcon::default());
170                status.set(ButtonSegmentStatus::Idle);
171            }
172        };
173
174        let background = match status() {
175            _ if !self.enabled => disabled_background,
176            _ if self.selected => selected_background,
177            ButtonSegmentStatus::Hovering => hover_background,
178            ButtonSegmentStatus::Idle => background,
179        };
180
181        let padding = if self.selected {
182            selected_padding
183        } else {
184            padding
185        };
186        let background = if *focus_status.read() == FocusStatus::Keyboard {
187            focus_background
188        } else {
189            background
190        };
191
192        rect()
193            .a11y_id(focus.a11y_id())
194            .a11y_focusable(self.enabled)
195            .a11y_role(AccessibilityRole::Button)
196            .maybe(self.enabled, |rect| rect.on_press(on_press))
197            .on_pointer_enter(on_pointer_enter)
198            .on_pointer_leave(on_pointer_leave)
199            .horizontal()
200            .width(width)
201            .height(height)
202            .padding(padding)
203            .overflow(Overflow::Clip)
204            .color(color.mul_if(!self.enabled, 0.9))
205            .background(background.mul_if(!self.enabled, 0.9))
206            .center()
207            .spacing(4.)
208            .maybe_child(self.selected.then(|| {
209                TickIcon::new()
210                    .fill(selected_icon_fill)
211                    .width(Size::px(12.))
212                    .height(Size::px(12.))
213            }))
214            .children(self.children.clone())
215    }
216
217    fn render_key(&self) -> DiffKey {
218        self.key.clone().or(self.default_key())
219    }
220}
221
222/// A container for grouping [`ButtonSegment`]s together.
223///
224/// # Example
225///
226/// ```rust
227/// # use freya::prelude::*;
228/// # use std::collections::HashSet;
229/// fn app() -> impl IntoElement {
230///     let mut selected = use_state(|| HashSet::from([1]));
231///     SegmentedButton::new().children((0..2).map(|i| {
232///         ButtonSegment::new()
233///             .key(i)
234///             .selected(selected.read().contains(&i))
235///             .on_press(move |_| {
236///                 if selected.read().contains(&i) {
237///                     selected.write().remove(&i);
238///                 } else {
239///                     selected.write().insert(i);
240///                 }
241///             })
242///             .child(format!("Option {i}"))
243///             .into()
244///     }))
245/// }
246/// # use freya_testing::prelude::*;
247/// # launch_doc(|| {
248/// #   rect().center().expanded().child(app())
249/// # }, "./images/gallery_segmented_button.png").render();
250/// ```
251///
252/// # Preview
253/// ![SegmentedButton Preview][segmented_button]
254#[cfg_attr(feature = "docs",
255    doc = embed_doc_image::embed_image!("segmented_button", "images/gallery_segmented_button.png")
256)]
257#[derive(Clone, PartialEq)]
258pub struct SegmentedButton {
259    pub(crate) theme: Option<SegmentedButtonThemePartial>,
260    children: Vec<Element>,
261    key: DiffKey,
262}
263
264impl Default for SegmentedButton {
265    fn default() -> Self {
266        Self::new()
267    }
268}
269
270impl SegmentedButton {
271    pub fn new() -> Self {
272        Self {
273            theme: None,
274            children: Vec::new(),
275            key: DiffKey::None,
276        }
277    }
278
279    pub fn theme(mut self, theme: SegmentedButtonThemePartial) -> Self {
280        self.theme = Some(theme);
281        self
282    }
283}
284
285impl ChildrenExt for SegmentedButton {
286    fn get_children(&mut self) -> &mut Vec<Element> {
287        &mut self.children
288    }
289}
290
291impl KeyExt for SegmentedButton {
292    fn write_key(&mut self) -> &mut DiffKey {
293        &mut self.key
294    }
295}
296
297impl Component for SegmentedButton {
298    fn render(&self) -> impl IntoElement {
299        let theme = get_theme!(&self.theme, segmented_button);
300
301        let SegmentedButtonTheme {
302            background,
303            border_fill,
304            corner_radius,
305        } = theme;
306
307        rect()
308            .overflow(Overflow::Clip)
309            .background(background)
310            .border(
311                Border::new()
312                    .fill(border_fill)
313                    .width(1.)
314                    .alignment(BorderAlignment::Outer),
315            )
316            .corner_radius(corner_radius)
317            .horizontal()
318            .children(self.children.clone())
319    }
320
321    fn render_key(&self) -> DiffKey {
322        self.key.clone().or(self.default_key())
323    }
324}