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///     }))
73/// }
74/// ```
75#[derive(Clone, PartialEq)]
76pub struct ButtonSegment {
77    pub(crate) theme: Option<ButtonSegmentThemePartial>,
78    children: Vec<Element>,
79    on_press: Option<EventHandler<Event<PressEventData>>>,
80    selected: bool,
81    enabled: bool,
82    cursor_icon: CursorIcon,
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            cursor_icon: CursorIcon::default(),
101            key: DiffKey::None,
102        }
103    }
104
105    /// Get the theme override for this component.
106    pub fn get_theme(&self) -> Option<&ButtonSegmentThemePartial> {
107        self.theme.as_ref()
108    }
109
110    /// Set a theme override for this component.
111    pub fn theme(mut self, theme: ButtonSegmentThemePartial) -> Self {
112        self.theme = Some(theme);
113        self
114    }
115
116    /// Whether this segment is currently selected.
117    pub fn is_selected(&self) -> bool {
118        self.selected
119    }
120
121    pub fn selected(mut self, selected: impl Into<bool>) -> Self {
122        self.selected = selected.into();
123        self
124    }
125
126    pub fn enabled(mut self, enabled: impl Into<bool>) -> Self {
127        self.enabled = enabled.into();
128        self
129    }
130
131    pub fn on_press(mut self, on_press: impl Into<EventHandler<Event<PressEventData>>>) -> Self {
132        self.on_press = Some(on_press.into());
133        self
134    }
135
136    /// Override the cursor icon shown when hovering over this component while enabled.
137    pub fn cursor_icon(mut self, cursor_icon: impl Into<CursorIcon>) -> Self {
138        self.cursor_icon = cursor_icon.into();
139        self
140    }
141}
142
143impl ChildrenExt for ButtonSegment {
144    fn get_children(&mut self) -> &mut Vec<Element> {
145        &mut self.children
146    }
147}
148
149impl KeyExt for ButtonSegment {
150    fn write_key(&mut self) -> &mut DiffKey {
151        &mut self.key
152    }
153}
154
155impl Component for ButtonSegment {
156    fn render(&self) -> impl IntoElement {
157        let theme = get_theme!(&self.theme, ButtonSegmentThemePreference, "button_segment");
158        let mut status = use_state(|| ButtonSegmentStatus::Idle);
159        let a11y_id = use_a11y();
160        let focus = use_focus(a11y_id);
161
162        let ButtonSegmentTheme {
163            background,
164            hover_background,
165            disabled_background,
166            selected_background,
167            focus_background,
168            padding,
169            selected_padding,
170            width,
171            height,
172            color,
173            selected_icon_fill,
174        } = theme;
175
176        let on_press = self.on_press.clone();
177        let on_press = move |e: Event<PressEventData>| {
178            a11y_id.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        };
187
188        let on_pointer_leave = move |_| {
189            if status() == ButtonSegmentStatus::Hovering {
190                status.set(ButtonSegmentStatus::Idle);
191            }
192        };
193
194        let background = match status() {
195            _ if !self.enabled => disabled_background,
196            _ if self.selected => selected_background,
197            ButtonSegmentStatus::Hovering => hover_background,
198            ButtonSegmentStatus::Idle => background,
199        };
200
201        let padding = if self.selected {
202            selected_padding
203        } else {
204            padding
205        };
206        let background = if *focus.read() == Focus::Keyboard {
207            focus_background
208        } else {
209            background
210        };
211
212        rect()
213            .a11y_id(a11y_id)
214            .a11y_focusable(self.enabled)
215            .a11y_role(AccessibilityRole::Button)
216            .maybe(self.enabled, |rect| rect.on_press(on_press))
217            .on_pointer_enter(on_pointer_enter)
218            .on_pointer_leave(on_pointer_leave)
219            .cursor(if self.enabled {
220                self.cursor_icon
221            } else {
222                CursorIcon::NotAllowed
223            })
224            .horizontal()
225            .width(width)
226            .height(height)
227            .padding(padding)
228            .overflow(Overflow::Clip)
229            .color(color.mul_if(!self.enabled, 0.9))
230            .background(background.mul_if(!self.enabled, 0.9))
231            .center()
232            .spacing(4.)
233            .maybe_child(self.selected.then(|| {
234                TickIcon::new()
235                    .fill(selected_icon_fill)
236                    .width(Size::px(12.))
237                    .height(Size::px(12.))
238            }))
239            .children(self.children.clone())
240    }
241
242    fn render_key(&self) -> DiffKey {
243        self.key.clone().or(self.default_key())
244    }
245}
246
247/// A container for grouping [`ButtonSegment`]s together.
248///
249/// # Example
250///
251/// ```rust
252/// # use freya::prelude::*;
253/// # use std::collections::HashSet;
254/// fn app() -> impl IntoElement {
255///     let mut selected = use_state(|| HashSet::from([1]));
256///     SegmentedButton::new().children((0..2).map(|i| {
257///         ButtonSegment::new()
258///             .key(i)
259///             .selected(selected.read().contains(&i))
260///             .on_press(move |_| {
261///                 if selected.read().contains(&i) {
262///                     selected.write().remove(&i);
263///                 } else {
264///                     selected.write().insert(i);
265///                 }
266///             })
267///             .child(format!("Option {i}"))
268///     }))
269/// }
270/// # use freya_testing::prelude::*;
271/// # launch_doc(|| {
272/// #   rect().center().expanded().child(app())
273/// # }, "./images/gallery_segmented_button.png").render();
274/// ```
275///
276/// # Preview
277/// ![SegmentedButton Preview][segmented_button]
278#[cfg_attr(feature = "docs",
279    doc = embed_doc_image::embed_image!("segmented_button", "images/gallery_segmented_button.png")
280)]
281#[derive(Clone, PartialEq)]
282pub struct SegmentedButton {
283    pub(crate) theme: Option<SegmentedButtonThemePartial>,
284    children: Vec<Element>,
285    key: DiffKey,
286}
287
288impl Default for SegmentedButton {
289    fn default() -> Self {
290        Self::new()
291    }
292}
293
294impl SegmentedButton {
295    pub fn new() -> Self {
296        Self {
297            theme: None,
298            children: Vec::new(),
299            key: DiffKey::None,
300        }
301    }
302
303    pub fn theme(mut self, theme: SegmentedButtonThemePartial) -> Self {
304        self.theme = Some(theme);
305        self
306    }
307}
308
309impl ChildrenExt for SegmentedButton {
310    fn get_children(&mut self) -> &mut Vec<Element> {
311        &mut self.children
312    }
313}
314
315impl KeyExt for SegmentedButton {
316    fn write_key(&mut self) -> &mut DiffKey {
317        &mut self.key
318    }
319}
320
321impl Component for SegmentedButton {
322    fn render(&self) -> impl IntoElement {
323        let theme = get_theme!(
324            &self.theme,
325            SegmentedButtonThemePreference,
326            "segmented_button"
327        );
328
329        let SegmentedButtonTheme {
330            background,
331            border_fill,
332            corner_radius,
333        } = theme;
334
335        rect()
336            .overflow(Overflow::Clip)
337            .background(background)
338            .border(
339                Border::new()
340                    .fill(border_fill)
341                    .width(1.)
342                    .alignment(BorderAlignment::Outer),
343            )
344            .corner_radius(corner_radius)
345            .horizontal()
346            .children(self.children.clone())
347    }
348
349    fn render_key(&self) -> DiffKey {
350        self.key.clone().or(self.default_key())
351    }
352}