Skip to main content

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