Skip to main content

freya_components/
segmented_button.rs

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